前言--踩坑过程
一时间心血来潮,想用科大讯飞的api来做一个语音实时转文字,也是走了很多弯路,边写边查边生成,最后算是完成了。功能实现了但是没有做UI。
本来想试试光靠不要服务端光靠前端直接调用科大讯飞的api来实现,但是发现太慢了,四五秒才蹦出来一个字。
然后没办法,搭建了一个服务端,一开始用的是直接用上传的文件来做,但是也还是很慢,当然可能是我代码写得烂。
后面网上搜了一下,试着把上传的文件保存为pcm文件,然后读取pcm,快了特别多。
还有流式传输,一开始思路错了,我以为的是分段截取然后上传,但是这样识别的正确率简直是不堪入目。后面使用不暂停录音来截取而是直接上传目前已经录入的。
注册讯飞应用获取免费服务
自行注册,如果一天500免费额度不够可以去买一个五万的免费的,一年内。
使用socket.io搭建服务
前面还有创建react项目我就跳过了。
下载socket.io和recorder
npm i js-audio-recorder
npm i socket.io-client
用户端搭建一个连接ws和recorder(录音的),同时加入房间。
import Recorder from 'js-audio-recorder';
import { useEffect, useState } from "react";
import io from 'socket.io-client';
const [roomId, setrooId] = useState('') // 定义 roomId 状态,初始值为空字符串
const [ws, setWs] = useState(null) // 定义 ws 状态,用于存储 WebSocket 连接,初始值为 null
const [recorder, setrecorder] = useState(null) // 定义 recorder 状态,用于存储录音器实例,初始值为 null
useEffect(() => {
// 创建新的 WebSocket 连接
const socketIo = io(url);
setWs(socketIo); // 保存 WebSocket 连接实例到 ws 状态中
const roomid = new Date().getTime() // 获取当前时间的时间戳,作为房间 ID
setrooId(roomid) // 保存房间 ID 到 roomId 状态中
socketIo.emit('joinRoom', roomid) // 发送 joinRoom 事件,附带房间 ID,通知服务器加入房间
socketIo.on('value', val => {
setTemp(val); // 当从服务器接收到 value 事件时,更新 temp 状态
})
setrecorder(new Recorder({
bitRate: 16, // 设置录音比特率为 16 kbps
sampleRate: 16000, // 设置录音采样率为 16000 Hz
bufferSize: 8192, // 设置录音缓冲区大小为 8192 字节
}))
// 清理函数,在组件卸载时断开 WebSocket 连接
return () => {
socketIo.disconnect();
};
}, [url]); // useEffect 钩子依赖 url,当 url 改变时重新执行
服务端
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const { Server } = require('socket.io');
const cors = require('cors');
// 创建 Express 应用
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: '*',
methods: ['GET', 'POST']
}
});
// 配置 Socket.IO 事件
io.on('connection', (socket) => {
console.log('a user connected');
socket.on('disconnect', () => {
console.log('user disconnected');
});
socket.on('joinRoom', (roomId) => {
socket.join(roomId)
})
});
// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
基本服务就起了。
录音并采用流式传输传递音频数据
现在开始就是要开始录音并且传递出去了。获取的Blob格式不适合用于传输,所以这里就转化成 base64 编码来传输。
注意这里的流式传输,最开始我写的是每隔两秒就结束录音然后截取发过去再重新启动录音,但是效果不好,这里采用的是200毫秒发送一次,但是不暂停录音器。这样就很有效果了。
const startR = () => {
console.log('开始');
recorder.start(); // 开始录音
//流式传输**************
const sendAudioData = () => {
const pcmBlob = recorder.getPCMBlob(); // 获取 PCM 格式的音频 Blob 数据
console.log(pcmBlob);
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result; // 将 Blob 数据转换为 ArrayBuffer
const base64 = arrayBufferToBase64(arrayBuffer); // 将 ArrayBuffer 转换为 base64 编码
// 发送 base64 编码的音频数据到 WebSocket 服务器
ws.emit('other', {
roomId: roomId,
value: base64 // 传递 base64 编码的音频数据
});
};
reader.readAsArrayBuffer(pcmBlob); // 将 Blob 数据读取为 ArrayBuffer
};
const intervalId = setInterval(sendAudioData, 200); // 每 200 毫秒调用一次 sendAudioData 函数,发送音频数据
setT(intervalId); // 保存定时器 ID 到 T 状态中
//流式传输**************
};
// 停止录音的函数
const stopR = () => {
clearInterval(T); // 清除定时器
setT(null); // 将 T 状态重置为 null
recorder.stop(); // 停止录音
};
// 将 ArrayBuffer 转换为 base64 编码的函数
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary); // 将二进制字符串转换为 base64 编码
}
return (
<>
<div className='Box'>
<button onClick={startR}>开始音频录制</button>
<button onClick={stopR}>结束音频录制</button>
</div>
<div>{temp}</div>
</>
);
服务端连接星火服务端并传递音频
语音听写(流式版)WebAPI 文档 | 讯飞开放平台文档中心 (xfyun.cn)
具体的方式我是借鉴了一下这个博客
科大讯飞语音接口调用实现语音识别_科大讯飞语音识别接口-CSDN博客
我最开始是直接把上传的文件直接遍历每一帧来上传,但是发现很慢,而读取文件的形式能反应很快,然后我就选择先把上传获得的转存为pcm文件,然后再进行同样的操作。
const fs = require('fs');
const path = require('path');
//收到消息重新存为文件
socket.on('other', ({ value, roomId }) => {
// 解码 Base64 数据
const buffer = Buffer.from(value, 'base64');
// 定义文件路径(例如:在 `public` 文件夹下)
const filePath = path.join(__dirname, 'public', `${roomId}.pcm`);
// 写入文件
fs.writeFile(filePath, buffer, (err) => {
if (err) {
console.error('写入 PCM 文件失败:', err);
} else {
console.log('PCM 文件已成功保存:', filePath);
let url = './public/' + roomId + '.pcm'
SpeechToText(roomId, url, roomId, io)
}
});
});
然后后面的和博客的差不多
const CryptoJS = require('crypto-js');
// 系统配置
const config = {
hostUrl: "wss://iat-api.xfyun.cn/v2/iat",
host: "iat-api.xfyun.cn",
appid: "",//看控制台
apiSecret: "",//看控制台
apiKey: "",//看控制台
uri: "/v2/iat",
highWaterMark: 1280
};
// 帧定义
const FRAME = {
STATUS_FIRST_FRAME: 0,
STATUS_CONTINUE_FRAME: 1,
STATUS_LAST_FRAME: 2
};
const SpeechToText = (roomId, url, name, io) => {
console.log(url)
// 获取当前时间 RFC1123格式
let date = (new Date().toUTCString())
// 设置当前临时状态为初始化
let status = FRAME.STATUS_FIRST_FRAME
// 记录本次识别用sid
let currentSid = ""
// 识别结果
let iatResult = []
let str = ""
let wssUrl = config.hostUrl + "?authorization=" + getAuthStr(date) + "&date=" + date + "&host=" + config.host
let ws = new WebSocket(wssUrl)
// 连接建立完毕,读取数据进行识别
ws.on('open', (event) => {
console.log("websocket connect!")
var readerStream = fs.createReadStream(url, {
highWaterMark: config.highWaterMark
});
readerStream.on('data', function (chunk) {
// console.log(chunk)
send(chunk)
});
// 最终帧发送结束
readerStream.on('end', function () {
status = FRAME.STATUS_LAST_FRAME
send("")
});
})
ws.on('message', (data, err) => {
if (err) {
console.log(`err:${err}`);
return;
}
let res = JSON.parse(data);
if (res.code != 0) {
console.log(`error code ${res.code}, reason ${res.message}`);
return;
}
if (res.data.status == 2) {
// 识别完成
console.log("最终识别结果");
currentSid = res.sid;
ws.close();
} else {
// 识别中
// console.log("中间识别结果");
}
iatResult[res.data.result.sn] = res.data.result;
if (res.data.result.pgs == 'rpl') {
// 处理动态修正
res.data.result.rg.forEach(i => {
iatResult[i] = null;
});
// console.log("【动态修正】");
}
str = ""
// 逐字打印
iatResult.forEach(i => {
if (i != null) {
i.ws.forEach(j => {
j.cw.forEach(k => {
console.log(k.w); // 打印每个字
str += k.w
});
});
}
});
console.log('完整语句是:' + str)
});
// 资源释放
ws.on('close', () => {
console.log(`本次识别sid:${currentSid}`)
io.to(roomId).emit('value', str)
console.log('connect close!')
})
// 建连错误
ws.on('error', (err) => {
console.log("websocket connect err: " + err)
})
// 鉴权签名
function getAuthStr(date) {
let signatureOrigin = `host: ${config.host}\ndate: ${date}\nGET ${config.uri} HTTP/1.1`
let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret)
let signature = CryptoJS.enc.Base64.stringify(signatureSha)
let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`
let authStr = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authorizationOrigin))
return authStr
}
// 传输数据
function send(data) {
let frame = "";
let frameDataSection = {
"status": status,
"format": "audio/L16;rate=16000",
"audio": data.toString('base64'),
"encoding": "raw"
}
switch (status) {
case FRAME.STATUS_FIRST_FRAME:
frame = {
// 填充common
common: {
app_id: config.appid
},
//填充business
business: {
language: "zh_cn",
domain: "iat",
accent: "mandarin",
dwa: "wpgs" // 可选参数,动态修正
},
//填充data
data: frameDataSection
}
status = FRAME.STATUS_CONTINUE_FRAME;
break;
case FRAME.STATUS_CONTINUE_FRAME:
case FRAME.STATUS_LAST_FRAME:
//填充frame
frame = {
data: frameDataSection
}
break;
}
ws.send(JSON.stringify(frame))
}
}
然后就可以实现基本的功能了!
完整代码
用户端
import Recorder from 'js-audio-recorder';
import { useEffect, useState } from "react";
import io from 'socket.io-client';
function App() {
// 导入 React 库中的 useState 和 useEffect 钩子
const [roomId, setrooId] = useState('') // 定义 roomId 状态,初始值为空字符串
const [ws, setWs] = useState(null) // 定义 ws 状态,用于存储 WebSocket 连接,初始值为 null
const [recorder, setrecorder] = useState(null) // 定义 recorder 状态,用于存储录音器实例,初始值为 null
const url = 'http://127.0.0.1:3000' // 定义 WebSocket 服务器的 URL
const [T, setT] = useState(null) // 定义 T 状态,用于存储定时器 ID,初始值为 null
const [temp, setTemp] = useState("") // 定义 temp 状态,用于存储从服务器接收到的值,初始值为空字符串
useEffect(() => {
// 创建新的 WebSocket 连接
const socketIo = io(url);
setWs(socketIo); // 保存 WebSocket 连接实例到 ws 状态中
const T = new Date().getTime() // 获取当前时间的时间戳,作为房间 ID
setrooId(T) // 保存房间 ID 到 roomId 状态中
socketIo.emit('joinRoom', T) // 发送 joinRoom 事件,附带房间 ID,通知服务器加入房间
socketIo.on('value', val => {
setTemp(val); // 当从服务器接收到 value 事件时,更新 temp 状态
})
setrecorder(new Recorder({
bitRate: 16, // 设置录音比特率为 16 kbps
sampleRate: 16000, // 设置录音采样率为 16000 Hz
bufferSize: 8192, // 设置录音缓冲区大小为 8192 字节
}))
// 清理函数,在组件卸载时断开 WebSocket 连接
return () => {
socketIo.disconnect();
};
}, [url]); // useEffect 钩子依赖 url,当 url 改变时重新执行
// 开始录音的函数
const startR = () => {
console.log('开始');
recorder.start(); // 开始录音
const sendAudioData = () => {
const pcmBlob = recorder.getPCMBlob(); // 获取 PCM 格式的音频 Blob 数据
console.log(pcmBlob);
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result; // 将 Blob 数据转换为 ArrayBuffer
const base64 = arrayBufferToBase64(arrayBuffer); // 将 ArrayBuffer 转换为 base64 编码
// 发送 base64 编码的音频数据到 WebSocket 服务器
ws.emit('other', {
roomId: roomId,
value: base64 // 传递 base64 编码的音频数据
});
};
reader.readAsArrayBuffer(pcmBlob); // 将 Blob 数据读取为 ArrayBuffer
};
const intervalId = setInterval(sendAudioData, 200); // 每 200 毫秒调用一次 sendAudioData 函数,发送音频数据
setT(intervalId); // 保存定时器 ID 到 T 状态中
};
// 停止录音的函数
const stopR = () => {
clearInterval(T); // 清除定时器
setT(null); // 将 T 状态重置为 null
recorder.stop(); // 停止录音
};
// 将 ArrayBuffer 转换为 base64 编码的函数
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary); // 将二进制字符串转换为 base64 编码
}
return (
<>
<div className='Box'>
<button onClick={startR}>开始音频录制</button>
<button onClick={stopR}>结束音频录制</button>
</div>
<div>{temp}</div>
</>
);
}
export default App;
服务端
const express = require('express');
const http = require('http');
const CryptoJS = require('crypto-js');
const WebSocket = require('ws');
const { Server } = require('socket.io');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
// 创建 Express 应用
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: '*',
methods: ['GET', 'POST']
}
});
// 使用 CORS 中间件
app.use(cors({
origin: '*'
}));
// 系统配置
const config = {
hostUrl: "wss://iat-api.xfyun.cn/v2/iat",
host: "iat-api.xfyun.cn",
appid: "",//看控制台
apiSecret: "",//看控制台
apiKey: "",//看控制台
uri: "/v2/iat",
highWaterMark: 1280
};
// 帧定义
const FRAME = {
STATUS_FIRST_FRAME: 0,
STATUS_CONTINUE_FRAME: 1,
STATUS_LAST_FRAME: 2
};
const SpeechToText = (roomId, url, name, io) => {
console.log(url)
// 获取当前时间 RFC1123格式
let date = (new Date().toUTCString())
// 设置当前临时状态为初始化
let status = FRAME.STATUS_FIRST_FRAME
// 记录本次识别用sid
let currentSid = ""
// 识别结果
let iatResult = []
let str = ""
let wssUrl = config.hostUrl + "?authorization=" + getAuthStr(date) + "&date=" + date + "&host=" + config.host
let ws = new WebSocket(wssUrl)
// 连接建立完毕,读取数据进行识别
ws.on('open', (event) => {
console.log("websocket connect!")
var readerStream = fs.createReadStream(url, {
highWaterMark: config.highWaterMark
});
readerStream.on('data', function (chunk) {
// console.log(chunk)
send(chunk)
});
// 最终帧发送结束
readerStream.on('end', function () {
status = FRAME.STATUS_LAST_FRAME
send("")
});
})
ws.on('message', (data, err) => {
if (err) {
console.log(`err:${err}`);
return;
}
let res = JSON.parse(data);
if (res.code != 0) {
console.log(`error code ${res.code}, reason ${res.message}`);
return;
}
if (res.data.status == 2) {
// 识别完成
console.log("最终识别结果");
currentSid = res.sid;
ws.close();
} else {
// 识别中
// console.log("中间识别结果");
}
iatResult[res.data.result.sn] = res.data.result;
if (res.data.result.pgs == 'rpl') {
// 处理动态修正
res.data.result.rg.forEach(i => {
iatResult[i] = null;
});
// console.log("【动态修正】");
}
str = ""
// 逐字打印
iatResult.forEach(i => {
if (i != null) {
i.ws.forEach(j => {
j.cw.forEach(k => {
console.log(k.w); // 打印每个字
str += k.w
});
});
}
});
console.log('完整语句是:' + str)
});
// 资源释放
ws.on('close', () => {
console.log(`本次识别sid:${currentSid}`)
io.to(roomId).emit('value', str)
console.log('connect close!')
})
// 建连错误
ws.on('error', (err) => {
console.log("websocket connect err: " + err)
})
// 鉴权签名
function getAuthStr(date) {
let signatureOrigin = `host: ${config.host}\ndate: ${date}\nGET ${config.uri} HTTP/1.1`
let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret)
let signature = CryptoJS.enc.Base64.stringify(signatureSha)
let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`
let authStr = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authorizationOrigin))
return authStr
}
// 传输数据
function send(data) {
let frame = "";
let frameDataSection = {
"status": status,
"format": "audio/L16;rate=16000",
"audio": data.toString('base64'),
"encoding": "raw"
}
switch (status) {
case FRAME.STATUS_FIRST_FRAME:
frame = {
// 填充common
common: {
app_id: config.appid
},
//填充business
business: {
language: "zh_cn",
domain: "iat",
accent: "mandarin",
dwa: "wpgs" // 可选参数,动态修正
},
//填充data
data: frameDataSection
}
status = FRAME.STATUS_CONTINUE_FRAME;
break;
case FRAME.STATUS_CONTINUE_FRAME:
case FRAME.STATUS_LAST_FRAME:
//填充frame
frame = {
data: frameDataSection
}
break;
}
ws.send(JSON.stringify(frame))
}
}
// 配置 Socket.IO 事件
io.on('connection', (socket) => {
console.log('a user connected');
socket.on('disconnect', () => {
console.log('user disconnected');
});
socket.on('joinRoom', (roomId) => {
socket.join(roomId)
})
socket.on('other', ({ value, roomId }) => {
// 解码 Base64 数据
const buffer = Buffer.from(value, 'base64');
// 定义文件路径(例如:在 `public` 文件夹下)
const filePath = path.join(__dirname, 'public', `${roomId}.pcm`);
// 写入文件
fs.writeFile(filePath, buffer, (err) => {
if (err) {
console.error('写入 PCM 文件失败:', err);
} else {
console.log('PCM 文件已成功保存:', filePath);
let url = './public/' + roomId + '.pcm'
SpeechToText(roomId, url, roomId, io)
}
});
});
});
// 设置静态文件目录(可选)
// app.use(express.static('public'));
// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
标签:node,console,log,FRAME,react,let,const,roomId,听写
From: https://blog.csdn.net/m0_73459939/article/details/140926019