共计 10199 个字符,预计需要花费 26 分钟才能阅读完成。
volute 是什么?
volute(蜗壳) 是一个应用 Raspberry Pi+Node.js 制作的语音助手.
什么是树莓派?
树莓派(英语:Raspberry Pi)是基于 Linux 的单片机电脑,由英国树莓派基金会开发,目标是以高价硬件及自由软件促成学校的根本计算机科学教育。
树莓派每一代均应用博通(Broadcom)出产的 ARM 架构处理器,现在生产的机型内存在 2GB 和 8GB 之间,次要应用 SD 卡或者 TF 卡作为存储媒体,装备 USB 接口、HDMI 的视频输入(反对声音输入)和 RCA 端子输入,内置 Ethernet/WLAN/Bluetooth 网络链接的形式(根据型号决定),并且可应用多种操作系统。产品线型号分为 A 型、B 型、Zero 型和 ComputeModule 计算卡。
简略的说, 这是一台能够放到口袋里的电脑!!
什么是 Node.js?
Node.js 是一个能执行 Javascript 的环境, 一个事件驱动 I/O 的 Javascript 环境, 基于 Google 的 V8 引擎.
什么是人机对话零碎 ?
人机对话(Human-Machine Conversation)是指让机器了解和使用自然语言实现人机通信的技术。
对话零碎大抵可分为 5 个根本模块:语音辨认(ASR)、天然语音了解(NLU)、对话治理(DM)、自然语言生成(NLG)、语音合成(TTS)。
- 语音辨认(ASR): 实现语音到文本的转换,将用户谈话的声音转化为语音。
- 自然语言了解(NLU): 实现对文本的语义解析,提取要害信息,进行用意辨认与实体辨认。
- 对话治理(DM): 负责对话状态保护、数据库查问、上下文治理等。
- 自然语言生成(NLG): 生成相应的自然语言文本。
- 语音合成(TTS): 将生成的文本转换为语音。
资料筹备
- 树莓派 4B 主板
- 树莓派 5V3A TYPE C 接口
- 微型 USB 麦克风
- 迷你音箱
- 16G TF 卡
- 川宇读卡器
- 杜邦线, 外壳, 散热片 …
树莓派零碎装置及根底配置
新的树莓派不像你买的 Macbook Pro 一样开机就能用 ????, 想要顺利体验树莓派, 还得一步一步来~
烧录操作系统
树莓派没有硬盘构造, 仅有一个 micro SD 卡插槽用于存储, 因而要把操作系统装到 micro SD 卡中。
树莓派反对许多操作系统, 这里抉择的是官网举荐的 Raspbian,这是一款基于 Debian Linux 的树莓派专用零碎,实用于树莓派所有的型号。
装置零碎我用的是 Raspberry Pi Imager 工具为树莓派烧录零碎镜像。
根底配置
要对树莓派进行配置, 首先要启动零碎 (咱们装置的是零碎镜像, 可免装置间接进入), 而后将树莓派连贯显示器即可看到零碎桌面, 我这里应用的是另一种办法:
- 应用 IP Scanner 工具 扫描出 Raspberry Pi 的 IP
- 扫描出 IP 后应用 VNC Viewer 工具 连贯进零碎
- 也能够间接 ssh 连贯, 而后通过 raspi-config 命令进行配置
- 配置网络 / 分辨率 / 语言 / 输入输出音频等参数
volute 实现思路
任务调度服务
const fs = require("fs"); | |
const path = require("path"); | |
const Speaker = require("speaker"); | |
const {record} = require("node-record-lpcm16"); | |
const XunFeiIAT = require("./services/xunfeiiat.service"); | |
const XunFeiTTS = require("./services/xunfeitts.service"); | |
const initSnowboy = require("./services/snowboy.service"); | |
const TulingBotService = require("./services/tulingbot.service"); | |
// 任务调度服务 | |
const taskScheduling = { | |
// 麦克风 | |
mic: null, | |
speaker: null, | |
detector: null, | |
// 音频输出流 | |
inputStream: null, | |
// 音頻輸出流 | |
outputStream: null, | |
init() { | |
// 初始化 snowboy | |
this.detector = initSnowboy({record: this.recordSound.bind(this), | |
stopRecord: this.stopRecord.bind(this), | |
}); | |
// 管道流, 将麦克风接管到的流传递给 snowboy | |
this.mic.pipe(this.detector); | |
}, | |
start() { | |
// 监听麦克风输出流 | |
this.mic = record({ | |
sampleRate: 16000, // 采样率 | |
threshold: 0.5, | |
verbose: true, | |
recordProgram: "arecord", | |
}).stream(); | |
this.init();}, | |
// 记录音频输出 | |
recordSound() { | |
// 每次记录前, 先进行上次未播放实现的输入流 | |
this.stopSpeak(); | |
console.log("start record"); | |
// 创立可写流 | |
this.inputStream = fs.createWriteStream(path.resolve(__dirname, "./assets/input.wav"), | |
{encoding: "binary",} | |
); | |
// 管道流, 将麦克风承受到的输出流 传递给 创立的可写流 | |
this.mic.pipe(this.inputStream); | |
}, | |
// 进行音频输出 | |
stopRecord() {if (this.inputStream) {console.log("stop record"); | |
// 解绑 this.mac 绑定的管道流 | |
this.mic.unpipe(this.inputStream); | |
this.mic.unpipe(this.detector); | |
process.nextTick(() => { | |
// 销毁输出流 | |
this.inputStream.destroy(); | |
this.inputStream = null; | |
// 从新初始化 | |
this.init(); | |
// 调用语音听写服务 | |
this.speech2Text();}); | |
} | |
}, | |
// speech to text | |
speech2Text() { | |
// 实例化 语音听写服务 | |
const iatService = new XunFeiIAT({onReply: (msg) => {console.log("msg", msg); | |
// 回调, 调用聊天性能 | |
this.onChat(msg); | |
}, | |
}); | |
iatService.init();}, | |
// 聊天 -> 图灵机器人 | |
onChat(text) { | |
// 实例化聊天机器人 | |
TulingBotService.start(text).then((res) => {console.log(res); | |
// 接管到聊天音讯, 调用语音合成服务 | |
this.text2Speech(res); | |
}); | |
}, | |
// text to speech | |
text2Speech(text) { | |
// 实例化 语音合成服务 | |
const ttsService = new XunFeiTTS({ | |
text, | |
onDone: () => {console.log("onDone"); | |
this.onSpeak();}, | |
}); | |
ttsService.init();}, | |
// 播放, 音频输入 | |
onSpeak() { | |
// 实例化 speaker, 用于播放语音 | |
this.speaker = new Speaker({ | |
channels: 1, | |
bitDepth: 16, | |
sampleRate: 16000, | |
}); | |
// 创立可读流 | |
this.outputStream = fs.createReadStream(path.resolve(__dirname, "./assets/output.wav") | |
); | |
// this is just to activate the speaker, 2s delay | |
this.speaker.write(Buffer.alloc(32000, 10)); | |
// 管道流, 将输入流传递给 speaker 进行播放 | |
this.outputStream.pipe(this.speaker); | |
this.outputStream.on("end", () => { | |
this.outputStream = null; | |
this.speaker = null; | |
}); | |
}, | |
// 进行播放 | |
stopSpeak() {this.outputStream && this.outputStream.unpipe(this.speaker); | |
}, | |
}; | |
taskScheduling.start(); |
热词唤醒 Snowboy
语音助手须要像市面上的设施一样,须要唤醒。如果没有唤醒步骤,始终做监听的话,对存储资源和网络连接的需要是十分大的。
Snowboy 是一款高度可定制的唤醒词检测引擎 (Hotwords Detection Library),能够用于实时嵌入式零碎,通过训练热词之后,能够离线运行,并且 功耗很低。以后,它能够运行在 Raspberry Pi、(Ubuntu)Linux 和 Mac OS X 零碎上。
const path = require("path"); | |
const snowboy = require("snowboy"); | |
const models = new snowboy.Models(); | |
// 增加训练模型 | |
models.add({file: path.resolve(__dirname, "../configs/volute.pmdl"), | |
sensitivity: "0.5", | |
hotwords: "volute", | |
}); | |
// 初始化 Detector 对象 | |
const detector = new snowboy.Detector({resource: path.resolve(__dirname, "../configs/common.res"), | |
models: models, | |
audioGain: 1.0, | |
applyFrontend: false, | |
}); | |
/** | |
* 初始化 initSnowboy | |
* 实现思路: | |
* 1. 监听到热词, 进行唤醒, 开始录音 | |
* 2. 录音期间, 有声音时, 重置 silenceCount 参数 | |
* 3. 录音期间, 未承受到声音时, 对 silenceCount 进行累加, 当累加值大于 3 时, 进行录音 | |
*/ | |
function initSnowboy({record, stopRecord}) { | |
const MAX_SILENCE_COUNT = 3; | |
let silenceCount = 0, | |
speaking = false; | |
/** | |
* silence 事件回调, 没声音时触发 | |
*/ | |
const onSilence = () => {console.log("silence"); | |
if (speaking && ++silenceCount > MAX_SILENCE_COUNT) { | |
speaking = false; | |
stopRecord && stopRecord(); | |
detector.off("silence", onSilence); | |
detector.off("sound", onSound); | |
detector.off("hotword", onHotword); | |
} | |
}; | |
/** | |
* sound 事件回调, 有声音时触发 | |
*/ | |
const onSound = () => {console.log("sound"); | |
if (speaking) {silenceCount = 0;} | |
}; | |
/** | |
* hotword 事件回调, 监听到热词时触发 | |
*/ | |
const onHotword = (index, hotword, buffer) => {if (!speaking) { | |
silenceCount = 0; | |
speaking = true; | |
record && record();} | |
}; | |
detector.on("silence", onSilence); | |
detector.on("sound", onSound); | |
detector.on("hotword", onHotword); | |
return detector; | |
} | |
module.exports = initSnowboy; |
语音听写 科大讯飞 API
语音转文字应用的是讯飞开放平台的语音听写服务. 它能够将短音频(≤60 秒)精准辨认成文字,除中文普通话和英文外,反对 25 种方言和 12 个语种,实时返回后果,达到边说边返回的成果。
require("dotenv").config(); | |
const fs = require("fs"); | |
const WebSocket = require("ws"); | |
const {resolve} = require("path"); | |
const {createAuthParams} = require("../utils/auth"); | |
class XunFeiIAT {constructor({ onReply}) {super(); | |
// websocket 连贯 | |
this.ws = null; | |
// 返回后果, 解析后的音讯文字 | |
this.message = ""; | |
this.onReply = onReply; | |
// 须要进行转换的输出流 语音文件 | |
this.inputFile = resolve(__dirname, "../assets/input.wav"); | |
// 接口 入参 | |
this.params = { | |
host: "iat-api.xfyun.cn", | |
path: "/v2/iat", | |
apiKey: process.env.XUNFEI_API_KEY, | |
secret: process.env.XUNFEI_SECRET, | |
}; | |
} | |
// 生成 websocket 连贯 | |
generateWsUrl() {const { host, path} = this.params; | |
// 接口鉴权, 参数加密 | |
const params = createAuthParams(this.params); | |
return `ws://${host}${path}?${params}`; | |
} | |
// 初始化 | |
init() {const reqUrl = this.generateWsUrl(); | |
this.ws = new WebSocket(reqUrl); | |
this.initWsEvent();} | |
// 初始化 websocket 事件 | |
initWsEvent() {this.ws.on("open", this.onOpen.bind(this)); | |
this.ws.on("error", this.onError); | |
this.ws.on("close", this.onClose); | |
this.ws.on("message", this.onMessage.bind(this)); | |
} | |
/** | |
* websocket open 事件, 触发示意已胜利建设连贯 | |
*/ | |
onOpen() {console.log("open"); | |
this.onPush(this.inputFile); | |
} | |
onPush(file) {this.pushAudioFile(file); | |
} | |
// websocket 音讯接管 回调 | |
onMessage(data) {const payload = JSON.parse(data); | |
if (payload.data && payload.data.result) { | |
// 拼接音讯后果 | |
this.message += payload.data.result.ws.reduce((acc, item) => acc + item.cw.map((cw) => cw.w), | |
"" | |
); | |
// status 2 示意完结 | |
if (payload.data.status === 2) {this.onReply(this.message); | |
} | |
} | |
} | |
// websocket 敞开事件 | |
onClose() {console.log("close"); | |
} | |
// websocket 谬误事件 | |
onError(error) {console.log(error); | |
} | |
/** | |
* 解析语音文件, 将语音以二进制流的模式传送给后端 | |
*/ | |
pushAudioFile(audioFile) { | |
this.message = ""; | |
// 发送须要的载体参数 | |
const audioPayload = (statusCode, audioBase64) => ({ | |
common: | |
statusCode === 0 | |
? {app_id: "5f6cab72",} | |
: undefined, | |
business: | |
statusCode === 0 | |
? { | |
language: "zh_cn", | |
domain: "iat", | |
ptt: 0, | |
} | |
: undefined, | |
data: { | |
status: statusCode, | |
format: "audio/L16;rate=16000", | |
encoding: "raw", | |
audio: audioBase64, | |
}, | |
}); | |
const chunkSize = 9000; | |
// 创立 buffer, 用于存储二进制数据 | |
const buffer = Buffer.alloc(chunkSize); | |
// 关上语音文件 | |
fs.open(audioFile, "r", (err, fd) => {if (err) {throw err;} | |
let i = 0; | |
// 以二进制流的模式递归发送 | |
function readNextChunk() {fs.read(fd, buffer, 0, chunkSize, null, (errr, nread) => {if (errr) {throw errr;} | |
// nread 示意文件流已读完, 发送传输完结标识 (status=2) | |
if (nread === 0) { | |
this.ws.send( | |
JSON.stringify({data: { status: 2}, | |
}) | |
); | |
return fs.close(fd, (err) => {if (err) {throw err;} | |
}); | |
} | |
let data; | |
if (nread < chunkSize) {data = buffer.slice(0, nread); | |
} else {data = buffer;} | |
const audioBase64 = data.toString("base64"); | |
const payload = audioPayload(i >= 1 ? 1 : 0, audioBase64); | |
this.ws.send(JSON.stringify(payload)); | |
i++; | |
readNextChunk.call(this); | |
}); | |
} | |
readNextChunk.call(this); | |
}); | |
} | |
} | |
module.exports = XunFeiIAT; |
聊天机器人 图灵机器人 API
图灵机器人 API V2.0 是基于图灵机器人平台语义了解、深度学习等核心技术,为宽广开发者和企业提供的在线服务和开发接口。
目前 API 接口可调用聊天对话、语料库、技能三大模块的语料:
聊天对话是指平台收费提供的近 10 亿条私有对话语料,满足用户对话娱乐需要;
语料库是指用户在平台上传的公有语料,仅供集体查看应用,帮忙用户最便捷的搭建业余畛域次的语料。
技能服务是指平台打包的 26 种实用服务技能。涵盖生存、出行、购物等多个畛域,一站式满足用户需要。
require("dotenv").config(); | |
const axios = require("axios"); | |
// 太简略了.. 懒得解释 ???? | |
const TulingBotService = { | |
requestUrl: "http://openapi.tuling123.com/openapi/api/v2", | |
start(text) {return new Promise((resolve) => { | |
axios | |
.post(this.requestUrl, { | |
reqType: 0, | |
perception: { | |
inputText: {text,}, | |
}, | |
userInfo: { | |
apiKey: process.env.TULING_BOT_API_KEY, | |
userId: process.env.TULING_BOT_USER_ID, | |
}, | |
}) | |
.then((res) => {// console.log(JSON.stringify(res.data, null, 2)); | |
resolve(res.data.results[0].values.text); | |
}); | |
}); | |
}, | |
}; | |
module.exports = TulingBotService; |
语音合成 科大讯飞 API
语音合成流式接口将文字信息转化为声音信息,同时提供了泛滥极具特色的发音人(音库)供您抉择。
该语音能力是通过 Websocket API 的形式给开发者提供一个通用的接口。Websocket API 具备流式传输能力,实用于须要流式数据传输的 AI 服务场景。相较于 SDK,API 具备轻量、跨语言的特点;相较于 HTTP API,Websocket API 协定有原生反对跨域的劣势。
require("dotenv").config(); | |
const fs = require("fs"); | |
const WebSocket = require("ws"); | |
const {resolve} = require("path"); | |
const {createAuthParams} = require("../utils/auth"); | |
class XunFeiTTS {constructor({ text, onDone}) {super(); | |
this.ws = null; | |
// 要转换的文字 | |
this.text = text; | |
this.onDone = onDone; | |
// 转换后的语音文件 | |
this.outputFile = resolve(__dirname, "../assets/output.wav"); | |
// 接口入参 | |
this.params = { | |
host: "tts-api.xfyun.cn", | |
path: "/v2/tts", | |
appid: process.env.XUNFEI_APP_ID, | |
apiKey: process.env.XUNFEI_API_KEY, | |
secret: process.env.XUNFEI_SECRET, | |
}; | |
} | |
// 生成 websocket 连贯 | |
generateWsUrl() {const { host, path} = this.params; | |
const params = createAuthParams(this.params); | |
return `ws://${host}${path}?${params}`; | |
} | |
// 初始化 | |
init() {const reqUrl = this.generateWsUrl(); | |
console.log(reqUrl); | |
this.ws = new WebSocket(reqUrl); | |
this.initWsEvent();} | |
// 初始化 websocket 事件 | |
initWsEvent() {this.ws.on("open", this.onOpen.bind(this)); | |
this.ws.on("error", this.onError); | |
this.ws.on("close", this.onClose); | |
this.ws.on("message", this.onMessage.bind(this)); | |
} | |
/** | |
* websocket open 事件, 触发示意已胜利建设连贯 | |
*/ | |
onOpen() {console.log("open"); | |
this.onSend(); | |
if (fs.existsSync(this.outputFile)) {fs.unlinkSync(this.outputFile); | |
} | |
} | |
// 发送要转换的参数信息 | |
onSend() { | |
const frame = { | |
// 填充 common | |
common: {app_id: this.params.appid,}, | |
// 填充 business | |
business: { | |
aue: "raw", | |
auf: "audio/L16;rate=16000", | |
vcn: "xiaoyan", | |
tte: "UTF8", | |
}, | |
// 填充 data | |
data: {text: Buffer.from(this.text).toString("base64"), | |
status: 2, | |
}, | |
}; | |
this.ws.send(JSON.stringify(frame)); | |
} | |
// 保留转换后的语音后果 | |
onSave(data) {fs.writeFileSync(this.outputFile, data, { flag: "a"}); | |
} | |
// websocket 音讯接管 回调 | |
onMessage(data, err) {if (err) return; | |
const res = JSON.parse(data); | |
if (res.code !== 0) {this.ws.close(); | |
return; | |
} | |
// 接管音讯后果并进行保留 | |
const audio = res.data.audio; | |
const audioBuf = Buffer.from(audio, "base64"); | |
this.onSave(audioBuf); | |
if (res.code == 0 && res.data.status == 2) {this.ws.close(); | |
this.onDone();} | |
} | |
onClose() {console.log("close"); | |
} | |
onError(error) {console.log(error); | |
} | |
} | |
module.exports = XunFeiTTS; |
成果演示
语雀 - 文章最底部可看成果
源码地址
Github 源码地址 如果有帮忙到你, 留个 star 呗~