使用react+node调用科大讯飞api实现实时语音听写(流式版)

前言--踩坑过程

一时间心血来潮,想用科大讯飞的api来做一个语音实时转文字,也是走了很多弯路,边写边查边生成,最后算是完成了。功能实现了但是没有做UI。

本来想试试光靠不要服务端光靠前端直接调用科大讯飞的api来实现,但是发现太慢了,四五秒才蹦出来一个字。

然后没办法,搭建了一个服务端,一开始用的是直接用上传的文件来做,但是也还是很慢,当然可能是我代码写得烂。

后面网上搜了一下,试着把上传的文件保存为pcm文件,然后读取pcm,快了特别多。

还有流式传输,一开始思路错了,我以为的是分段截取然后上传,但是这样识别的正确率简直是不堪入目。后面使用不暂停录音来截取而是直接上传目前已经录入的。

注册讯飞应用获取免费服务

控制台-讯飞开放平台 (xfyun.cn)

自行注册,如果一天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}`);
});

  • 24
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于ReactNode的云课堂系统设计与实现如下: 设计方案: 1.前端部分使用React进行开发,通过组件化的方式搭建系统各个页面,包括学生和教师的登录页面、课程列表页面、课程详情页面等。前端使用React的虚拟DOM和状态管理机制,提高页面渲染效率和用户交互体验。 2.后端部分使用Node作为服务器端语言,搭建服务端应用,提供数据接口和业务逻辑处理。使用Express框架处理HTTP请求,通过路由控制不同请求的处理逻辑。同时,使用MongoDB数据库存储用户信息、课程信息等。 3.系统采用前后端分离的架构,前后端通过接口进行数据交互。前端发送请求到后端获取数据,后端经过处理后返回相应的数据给前端进行展示。 实现步骤: 1.创建React项目,搭建基本的项目架构和路由配置,实现用户登录页面和注册页面。 2.在后端使用Express创建服务器,配置路由,实现用户注册和登录接口,将用户信息存储在MongoDB中。 3.开发课程列表页面,通过前端请求后端接口获取课程列表数据,并进行展示。 4.实现课程详情页面,通过前端发送请求获取具体的课程详情数据,包括课程名称、教师信息、课程介绍等。 5.开发学生选课功能,前端通过请求后端接口获取可选课程列表,用户选择后将选课信息存储到数据库中。 6.开发教师端功能,教师可以管理课程信息,包括创建课程、编辑课程、删除课程等。 7.完善系统功能,如学生查看已选课程、教师查看已开课程等。 总结: 基于ReactNode的云课堂系统设计与实现需要充分发挥React的组件化和状态管理特性,同时利用Node的高效处理请求和Express的简洁路由配置,通过前后端分离架构实现系统的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值