像谷歌助手这样的平台简化了自定义语音助手的构建。但是,如果你想要构建一个在本地运行并能确保数据隐私的助手,该怎么办呢?你可以使用Rasa、Mozilla DeepSpeech和Mozilla TTS等开源工具来实现。通过本教程,你可以了解实现过程。
随着谷歌助手和Alexa等平台越来越受欢迎,语音助手必将成为各行各业客户互动的下一个重要领域。然而,除非你使用托管的现成解决方案,否则语音助手的开发将带来一系列超越NLU和对话管理的全新挑战——除此之外,你还需要照顾到语音转文本、文本转语音组件以及前端。不久前,当我们尝试构建一个基于Rasa的谷歌助手时,我们谈到了语音主题。像谷歌助手这样的平台消除了实现语音处理和前端组件的障碍,但是它迫使你在数据的安全性和所用工具的灵活性上做出妥协。
那么,如果你想要构建一个本地运行并能确保数据安全的语音助手,你有哪些选项呢?我们来看看。在本文中,你将了解仅使用开源工具如何构建语音助手——从后端到前端。
本文的目的是向你展示仅使用开源工具如何构建自己的语音助手。一般来说,构建语音助手需要五个主要组件:
虽然对于NLU和对话管理来说,开源的Rasa是一个显而易见的选择,但是STT和TTS的选择就比较困难了,因为没有那么多开源框架可供选择。在研究了当前可用的选项CMUSphinx、Mozilla DeepSpeech、Mozilla TTS、Kaldi之后,我们决定使用Mozilla的工具——Mozilla DeepSpeech和Mozilla TTS,原因如下:
Mozilla DeepSpeech和Mozilla TTS是什么?Mozilla DeepSpeech是一个语音转文本的框架,它接收用户的音频输入,并使用机器学习将其转换为文本格式,稍后由NLU和对话系统进行处理。Mozilla TTS则负责相反的工作——它接收文本输入(在我们的例子中是对话系统生成的助手响应),并使用机器学习创建文本的音频表示。
NLU、对话管理和语音处理组件处理语音助手的后端,那么前端呢?好吧,这就是最大的问题所在——如果你搜索开源语音界面小部件,最终很可能没有结果。至少我们遇到了这样的情况,这也是为什么我们开发了自己的语音界面Rasa,我们将在这个项目中使用它,并且很高兴能与社区分享!
综上所述,开源语音助手包含以下组成部分:
对于这个项目,我们将使用一个已有的Rasa助手——Sara。它是一个基于Rasa的开源助手,可以回答关于Rasa框架的各种问题来帮助你入门。下面是和Sara对话的一个例子:
下面是在本地机器上安装Sara的步骤:
git clone https://github.com/RasaHQ/rasa-demo.git
cd rasa-demo
pip install -e .
rasa train --augmentation 0
docker run -p 8000:8000 rasa/duckling
rasa run actions --actions demo.actions
make run-cmdline
为了把Sara变成一个语音助手,我们必须在实现的后期编辑一些项目文件。在此之前,让我们先实现TTS和STT组件。
我们接下来要实现语音转文本组件——Mozilla DeepSpeech模型。阅读Rouben Morais的这篇博文,进一步了解Mozilla DeepSpeech的工作原理。Mozilla DeepSpeech提供了一些预训练的模型,也允许你训练自己的模型。简单起见,我们在这个项目中使用了一个预先训练好的模型。以下是在本地机器上安装STT的步骤:
pip3 install deepspeech
wget https://github.com/mozilla/DeepSpeech/releases/download/v0.5.1/deepspeech-0.5.1-models.tar.gz
tar xvfz deepspeech-0.5.1-models.tar.gz
运行上述命令后,项目目录下会新增一个目录deepspeech-0.5.1-models,其中包含模型文件。
检查组件设置是否正确的最佳方法是使用一些音频输入示例测试模型。下面是测试脚本:
import pyaudio
from deepspeech import Model
import scipy.io.wavfile as wav
WAVE_OUTPUT_FILENAME = "test_audio.wav"
def record_audio(WAVE_OUTPUT_FILENAME):
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
RECORD_SECONDS = 5
p = pyaudio.PyAudio()
stream = p.open(format=FORMAT,
channels=CHANNELS,
rate=RATE,
input=True,
frames_per_buffer=CHUNK)
print("* recording")
frames = [stream.read(CHUNK) for i in range(0, int(RATE / CHUNK * RECORD_SECONDS))]
print("* done recording")
stream.stop_stream()
stream.close()
p.terminate()
wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb')
wf.setnchannels(CHANNELS)
wf.setsampwidth(p.get_sample_size(FORMAT))
wf.setframerate(RATE)
wf.writeframes(b''.join(frames))
wf.close()
def deepspeech_predict(WAVE_OUTPUT_FILENAME):
N_FEATURES = 25
N_CONTEXT = 9
BEAM_WIDTH = 500
LM_ALPHA = 0.75
LM_BETA = 1.85
ds = Model('deepspeech-0.5.1-models/output_graph.pbmm', N_FEATURES, N_CONTEXT, 'deepspeech-0.5.1-models/alphabet.txt', BEAM_WIDTH)
fs, audio = wav.read(WAVE_OUTPUT_FILENAME)
return ds.stt(audio, fs)
if __name__ == '__main__':
record_audio(WAVE_OUTPUT_FILENAME)
predicted_text = deepspeech_predict(WAVE_OUTPUT_FILENAME)
print(predicted_text)
使用下面的命令运行脚本,一旦你看到消息“录制……”就说一个你希望用于测试这个模型的句子:
python deepspeech_test_prediction.py
在本文的下一部分中,你将学习如何设置项目的第三部分——文本转语音组件。
为了使助手能够用语音而不是文本进行响应,我们必须设置文本转语音组件,该组件将接收Rasa生成的文本响应并将其转换为声音。为此,我们使用Mozilla TTS。就像Mozilla DeepSpeech一样,它提供了预训练模型,但是你也可以使用自定义数据训练自己的模型。这次,我们还是使用一个预先训练好的TTS模型。下面是在本地机器上设置TTS组件的步骤:
git clone https://github.com/mozilla/TTS.git
cd TTS
git checkout db7f3d3
python setup.py develop
在Sara目录下,创建一个文件夹tts_mode*l ,并从这里下载模型文件:
https://drive.google.com/drive/folders/1GU8WGix98WrR3ayjoiirmmbLUZzwg4n0
使用以下脚本测试文本转语音组件。下面是脚本完成的工作:
如果你希望使用自定义的输入来测试模型,则可以修改sentence变量。脚本运行结束后,结果将保存在test_tt .wav文件中,你可以听下该文件来查看模型的效果。
import os
import sys
import io
import torch
from collections import OrderedDict
from TTS.models.tacotron import Tacotron
from TTS.layers import *
from TTS.utils.data import *
from TTS.utils.audio import AudioProcessor
from TTS.utils.generic_utils import load_config
from TTS.utils.text import text_to_sequence
from TTS.utils.synthesis import synthesis
from utils.text.symbols import symbols, phonemes
from TTS.utils.visual import visualize
# 设置常量
MODEL_PATH = './tts_model/best_model.pth.tar'
CONFIG_PATH = './tts_model/config.json'
OUT_FILE = 'tts_out.wav'
CONFIG = load_config(CONFIG_PATH)
use_cuda = False
def tts(model, text, CONFIG, use_cuda, ap, OUT_FILE):
waveform, alignment, spectrogram, mel_spectrogram, stop_tokens = synthesis(model, text, CONFIG, use_cuda, ap)
ap.save_wav(waveform, OUT_FILE)
return alignment, spectrogram, stop_tokens
def load_model(MODEL_PATH, sentence, CONFIG, use_cuda, OUT_FILE):
# 加载模型
num_chars = len(phonemes) if CONFIG.use_phonemes else len(symbols)
model = Tacotron(num_chars, CONFIG.embedding_size, CONFIG.audio['num_freq'], CONFIG.audio['num_mels'], CONFIG.r, attn_windowing=False)
# 加载音频处理器
# CONFIG.audio["power"] = 1.3
CONFIG.audio["preemphasis"] = 0.97
ap = AudioProcessor(**CONFIG.audio)
# 加载模型状态
if use_cuda:
cp = torch.load(MODEL_PATH)
else:
cp = torch.load(MODEL_PATH, map_location=lambda storage, loc: storage)
# 加载模型
model.load_state_dict(cp['model'])
if use_cuda:
model.cuda()
model.eval()
model.eval()
model.decoder.max_decoder_steps = 1000
align, spec, stop_tokens = tts(model, sentence, CONFIG, use_cuda, ap, OUT_FILE)
if __name__ == '__main__':
sentence = "Hello, how are you doing? My name is Sara"
load_model(MODEL_PATH, sentence, CONFIG, use_cuda, OUT_FILE)
到这里,你本地的机器上应该已经运行着所有最重要的组件——Rasa助手、语音转文本和文本转语音组件。接下来要做的就是将所有这些组件组合到一起,并将助手连接到Rasa语音界面。要了解如何实现,请阅读本文的下一部分。
要把这些组件组合到一起对语音助手进行实际的测试,我们还需要两个东西:
(1)语音界面;
(2)一个可以在UI和后端(Mozilla和Rasa组件)之间建立通信的连接器。
我们先设置下Rasa语音界面,步骤如下:
git clone https://github.com/RasaHQ/rasa-voice-interface.git
cd rasa-voice-interface
npm install
npm run serve
运行上述命令后,打开浏览器并导航到https://localhost:8080,检查语音界面是否正在加载。弹跳球表示它已成功加载并在等待连接。
为了将助手连接到界面,你需要一个连接器。该连接器还将确定用户说话时引发什么动作,以及音频响应如何传回前端组件。要创建一个连接器,我们可以使用现有的socketio连接器,并使用几个新组件升级它:
下面是升级后连接器的全部代码:
import logging
import uuid
from sanic import Blueprint, response
from sanic.request import Request
from socketio import AsyncServer
from typing import Optional, Text, Any, List, Dict, Iterable
from rasa.core.channels.channel import InputChannel
from rasa.core.channels.channel import UserMessage, OutputChannel
import deepspeech
from deepspeech import Model
import scipy.io.wavfile as wav
import os
import sys
import io
import torch
import time
import numpy as np
from collections import OrderedDict
import urllib
import librosa
from TTS.models.tacotron import Tacotron
from TTS.layers import *
from TTS.utils.data import *
from TTS.utils.audio import AudioProcessor
from TTS.utils.generic_utils import load_config
from TTS.utils.text import text_to_sequence
from TTS.utils.synthesis import synthesis
from utils.text.symbols import symbols, phonemes
from TTS.utils.visual import visualize
logger = logging.getLogger(__name__)
def load_deepspeech_model():
N_FEATURES = 25
N_CONTEXT = 9
BEAM_WIDTH = 500
LM_ALPHA = 0.75
LM_BETA = 1.85
ds = Model('deepspeech-0.5.1-models/output_graph.pbmm', N_FEATURES, N_CONTEXT, 'deepspeech-0.5.1-models/alphabet.txt', BEAM_WIDTH)
return ds
def load_tts_model():
MODEL_PATH = './tts_model/best_model.pth.tar'
CONFIG_PATH = './tts_model/config.json'
CONFIG = load_config(CONFIG_PATH)
use_cuda = False
num_chars = len(phonemes) if CONFIG.use_phonemes else len(symbols)
model = Tacotron(num_chars, CONFIG.embedding_size, CONFIG.audio['num_freq'], CONFIG.audio['num_mels'], CONFIG.r, attn_windowing=False)
num_chars = len(phonemes) if CONFIG.use_phonemes else len(symbols)
model = Tacotron(num_chars, CONFIG.embedding_size, CONFIG.audio['num_freq'], CONFIG.audio['num_mels'], CONFIG.r, attn_windowing=False)
# 加载音频处理器
# CONFIG.audio["power"] = 1.3
CONFIG.audio["preemphasis"] = 0.97
ap = AudioProcessor(**CONFIG.audio)
# 加载模型状态
if use_cuda:
cp = torch.load(MODEL_PATH)
else:
cp = torch.load(MODEL_PATH, map_location=lambda storage, loc: storage)
# 加载模型
model.load_state_dict(cp['model'])
if use_cuda:
model.cuda()
#model.eval()
model.decoder.max_decoder_steps = 1000
return model, ap, MODEL_PATH, CONFIG, use_cuda
ds = load_deepspeech_model()
model, ap, MODEL_PATH, CONFIG, use_cuda = load_tts_model()
class SocketBlueprint(Blueprint):
def __init__(self, sio: AsyncServer, socketio_path, *args, **kwargs):
self.sio = sio
self.socketio_path = socketio_path
super(SocketBlueprint, self).__init__(*args, **kwargs)
def register(self, app, options):
self.sio.attach(app, self.socketio_path)
super(SocketBlueprint, self).register(app, options)
class SocketIOOutput(OutputChannel):
@classmethod
def name(cls):
return "socketio"
def __init__(self, sio, sid, bot_message_evt, message):
self.sio = sio
self.sid = sid
self.bot_message_evt = bot_message_evt
self.message = message
def tts(self, model, text, CONFIG, use_cuda, ap, OUT_FILE):
import numpy as np
waveform, alignment, spectrogram, mel_spectrogram, stop_tokens = synthesis(model, text, CONFIG, use_cuda, ap)
ap.save_wav(waveform, OUT_FILE)
wav_norm = waveform * (32767 / max(0.01, np.max(np.abs(waveform))))
return alignment, spectrogram, stop_tokens, wav_norm
def tts_predict(self, MODEL_PATH, sentence, CONFIG, use_cuda, OUT_FILE):
align, spec, stop_tokens, wav_norm = self.tts(model, sentence, CONFIG, use_cuda, ap, OUT_FILE)
return wav_norm
async def _send_audio_message(self, socket_id, response, **kwargs: Any):
# type: (Text, Any) -> None
"""Sends a message to the recipient using the bot event."""
ts = time.time()
OUT_FILE = str(ts)+'.wav'
link = "http://localhost:8888/"+OUT_FILE
wav_norm = self.tts_predict(MODEL_PATH, response['text'], CONFIG, use_cuda, OUT_FILE)
await self.sio.emit(self.bot_message_evt, {'text':response['text'], "link":link}, room=socket_id)
async def send_text_message(self, recipient_id: Text, message: Text, **kwargs: Any) -> None:
"""Send a message through this channel."""
await self._send_audio_message(self.sid, {"text": message})
class SocketIOInput(InputChannel):
"""A socket.io input channel."""
@classmethod
def name(cls):
return "socketio"
@classmethod
def from_credentials(cls, credentials):
credentials = credentials or {}
return cls(credentials.get("user_message_evt", "user_uttered"),
credentials.get("bot_message_evt", "bot_uttered"),
credentials.get("namespace"),
credentials.get("session_persistence", False),
credentials.get("socketio_path", "/socket.io"),
)
def __init__(self,
user_message_evt: Text = "user_uttered",
bot_message_evt: Text = "bot_uttered",
namespace: Optional[Text] = None,
session_persistence: bool = False,
socketio_path: Optional[Text] = '/socket.io'
):
self.bot_message_evt = bot_message_evt
self.session_persistence = session_persistence
self.user_message_evt = user_message_evt
self.namespace = namespace
self.socketio_path = socketio_path
def blueprint(self, on_new_message):
sio = AsyncServer(async_mode="sanic")
socketio_webhook = SocketBlueprint(
sio, self.socketio_path, "socketio_webhook", __name__
)
@socketio_webhook.route("/", methods=['GET'])
async def health(request):
return response.json({"status": "ok"})
@sio.on('connect', namespace=self.namespace)
async def connect(sid, environ):
logger.debug("User {} connected to socketIO endpoint.".format(sid))
print('Connected!')
@sio.on('disconnect', namespace=self.namespace)
async def disconnect(sid):
logger.debug("User {} disconnected from socketIO endpoint."
"".format(sid))
@sio.on('session_request', namespace=self.namespace)
async def session_request(sid, data):
print('This is sessioin request')
if data is None:
data = {}
if 'session_id' not in data or data['session_id'] is None:
data['session_id'] = uuid.uuid4().hex
await sio.emit("session_confirm", data['session_id'], room=sid)
logger.debug("User {} connected to socketIO endpoint."
"".format(sid))
@sio.on('user_uttered', namespace=self.namespace)
async def handle_message(sid, data):
output_channel = SocketIOOutput(sio, sid, self.bot_message_evt, data['message'])
if data['message'] == "/get_started":
message = data['message']
else:
##receive audio
received_file = 'output_'+sid+'.wav'
urllib.request.urlretrieve(data['message'], received_file)
path = os.path.dirname(__file__)
fs, audio = wav.read("output_{0}.wav".format(sid))
message = ds.stt(audio, fs)
await sio.emit(self.user_message_evt, {"text":message}, room=sid)
message_rasa = UserMessage(message, output_channel, sid,
input_channel=self.name())
await on_new_message(message_rasa)
return socketio_webhook
将上述代码保存为项目目录下的一个文件socketio_connector.py。
在开始试用之前,还有最后一件事需要完成,就是连接器配置——既然我们已经构建了一个自定义的连接器,所以我们必须告诉Rasa使用这个自定义连接器接收用户输入并发回响应。为此,在Sara的项目目录中创建一个credentials.yml文件,并提供以下详细信息(这里的socketio_connector是实现自定义连接器的模块的名称,而SocketIOInput是自定义连接器的输入类的名称):
socketio_connector.SocketIOInput:
bot_message_evt: bot_uttered
session_persistence: true
user_message_evt: user_uttered
至此,所有工作就已经完成了!剩下的就是启动助手并与它进行对话,步骤如下:
rasa run --enable-api -p 5005
rasa run actions --actions demo.actions
docker run -p 8000:8000 rasa/duckling
python3 -m http.server 8888
如果你在浏览器中刷新Rasa语音界面,就会看到我们构建的语音助手已经准备好对话:
点击开始,与仅使用开源工具构建的语音助手进行对话!
语音助手开发带来了一系列全新的挑战——不仅仅是有效的NLU和对话,你还需要有效的STT和TTS组件,而且,你的NLU必须足够灵活,可以弥补STT所犯的错误。如果你跟随本教程做了这个项目,就会注意到这个助手并不完美,它还有很大的改进空间,尤其是在STT和NLU阶段。那么如何改进呢?以下是一些建议:
在Rasa,为了使开发人员能够构建出了不起的东西,我们一直在寻找方法突破工具和软件的极限。我们希望通过构建这个项目向你展示,使用Rasa不仅可以构建文本助手,还可以构建语音助手。同时,我们也希望可以为你带来灵感,让你构建出了不起的应用程序,而又不损害你所使用的工具的安全性和灵活性。你用Rasa构建语音助手了吗?你用了什么工具?请在Rasa社区论坛上分享你的经验。
查看英文原文:
https://blog.rasa.com/how-to-build-a-voice-assistant-with-open-source-rasa-and-mozilla-tools/
领取专属 10元无门槛券
私享最新 技术干货