241205_给自己的应用加上语音助手功能
前面我们自己做了一个网易云音乐,但每次都要去点点点显得有点麻烦,所以我就考虑添加一些语音助手的功能。
其实当前在日常windows使用中,我觉得也就音乐播放需要一个语音助手交互,其他的功能,要么太复杂,简单的声控无法实现,要么语音控制的效率太低,不如直接上手操作。
最简单的语音助手仅需要实现三个功能,本地语音转文本,根据文本内容提取关键词完成对应指令,播放“收到”“已完成”等语音,我们此处就实现这样的最简单的语音助手。
语音转文字
本地语音转文字我们使用vosk语音识别库实现。model使用本地的一个60m的小模型.
这里附模型链接: https://pan.baidu.com/s/1mY2VoRqjSwS6n4qj1pPZMA?pwd=8888
解压放在项目根目录就可以了
为了实现一个较好的唤醒效果,我们采用后台实时录音的方式。检测到声音的时候就进行转文字,但是因为环境噪音是一直存在的,此时我们就要给他设定一个阈值,当超过这个阈值时,我们才认为他是有效输入,才进行转文字输入。
def SaveWave(model):
# 设置音频参数
FORMAT = pyaudio.paInt16 # 音频流的格式
RATE = 44100 # 采样率,单位Hz
CHUNK = 4000 # 单位帧
THRESHOLDNUM = 10 # 静默时间,超过这个个数就保存文件
THRESHOLD = 100 # 设定停止采集阈值
audio = pyaudio.PyAudio()
stream = audio.open(format=FORMAT,
channels=1,
rate=RATE,
input=True,
frames_per_buffer=CHUNK)
frames = []
print("开始录音...")
count = 0
while count < THRESHOLDNUM:
data = stream.read(CHUNK, exception_on_overflow=False)
np_data = np.frombuffer(data, dtype=np.int16)
frame_energy = np.mean(np.abs(np_data))
# print(frame_energy)
# 如果能量低于阈值持续时间过长,则停止录音
if frame_energy < THRESHOLD:
count += 1
elif count > 0:
count -= 1
frames.append(data)
print("停止录音!")
stream.stop_stream()
stream.close()
audio.terminate()
rec = KaldiRecognizer(model, RATE)
rec.SetWords(True)
str_ret = ""
for data in frames:
if rec.AcceptWaveform(data):
result = json.loads(rec.Result())
if 'text' in result:
str_ret += result['text']
result = json.loads(rec.FinalResult())
if 'text' in result:
str_ret += result['text']
str_ret = "".join(str_ret.split())
return str_ret
执行命令
得到语音转文字的结果之后,我们就可指定对应执行的指令了,因为我们不需要联网去搜索结果,只需要定义我们自己能用到的指令就可以了,此处简单定义指令。
def weekup(mw):
model = Model("vosk-model-cn-0.15")
SetLogLevel(-1)
while 1:
res = SaveWave(model)
if res != "" and res != None:
print(res)
if "小爱同学" in res:
answer = "我在"
answermethod(answer)
if "聊天" in res:
answer = "好的,请开始"
answermethod(answer)
from stt import run as stt_run
result=stt_run()
if "打开" in res:
app_name=res.replace("打开",'').strip()
if app_name in dir_path:
app_path=dir_path[app_name]
import subprocess
subprocess.Popen(app_path)
answer = "好的,已经为您打开了"
answermethod(answer)
if "歌" in res:
from testyuncloud import yuncloudMW
mw.playMusic()
# print("播放歌曲")
if "暂停" in res:
mw.playMusic()
if "上一首" in res:
mw.prevMusic()
if "下一首" in res:
mw.nextMusic()
在这个方法中,我调用了一个answermethod方法,在这个方法中,我调用了百度的语音合成api,当然这里合成的都是一些简单的语音,你也可以直接自己录音,然后直接播放录音文件。就避免了调用api。
下面是answermethod以及百度语音合成api的调用。
def answermethod(answer):
baidu_tts_test(answer)
time.sleep(1)
# coding=utf-8
import sys
import json
import pygame
import time
import io
IS_PY3 = sys.version_info.major == 3
if IS_PY3:
from urllib.request import urlopen
from urllib.request import Request
from urllib.error import URLError
from urllib.parse import urlencode
from urllib.parse import quote_plus
else:
import urllib2
from urllib import quote_plus
from urllib2 import urlopen
from urllib2 import Request
from urllib2 import URLError
from urllib import urlencode
API_KEY = ' ' # 这里和下面替换成你的百度语音合成服务api的key
SECRET_KEY = ' '
# TEXT = "欢迎使用百度语音合成。"
# 发音人选择, 基础音库:0为度小美,1为度小宇,3为度逍遥,4为度丫丫,
# 精品音库:5为度小娇,103为度米朵,106为度博文,110为度小童,111为度小萌,默认为度小美
PER = 4
# 语速,取值0-15,默认为5中语速
SPD = 5
# 音调,取值0-15,默认为5中语调
PIT = 5
# 音量,取值0-9,默认为5中音量
VOL = 5
# 下载的文件格式, 3:mp3(default) 4: pcm-16k 5: pcm-8k 6. wav
AUE = 3
FORMATS = {3: "mp3", 4: "pcm", 5: "pcm", 6: "wav"}
FORMAT = FORMATS[AUE]
CUID = "123456PYTHON"
TTS_URL = 'http://tsn.baidu.com/text2audio'
class DemoError(Exception):
pass
""" TOKEN start """
TOKEN_URL = 'http://aip.baidubce.com/oauth/2.0/token'
SCOPE = 'audio_tts_post' # 有此scope表示有tts能力,没有请在网页里勾选
def fetch_token():
print("fetch token begin")
params = {'grant_type': 'client_credentials',
'client_id': API_KEY,
'client_secret': SECRET_KEY}
post_data = urlencode(params)
if (IS_PY3):
post_data = post_data.encode('utf-8')
req = Request(TOKEN_URL, post_data)
try:
f = urlopen(req, timeout=5)
result_str = f.read()
except URLError as err:
print('token http response http code : ' + str(err.code))
result_str = err.read()
if (IS_PY3):
result_str = result_str.decode()
print(result_str)
result = json.loads(result_str)
print(result)
if ('access_token' in result.keys() and 'scope' in result.keys()):
if not SCOPE in result['scope'].split(' '):
raise DemoError('scope is not correct')
print('SUCCESS WITH TOKEN: %s ; EXPIRES IN SECONDS: %s' % (result['access_token'], result['expires_in']))
return result['access_token']
else:
raise DemoError('MAYBE API_KEY or SECRET_KEY not correct: access_token or scope not found in token response')
""" TOKEN end """
def baidu_tts_test(text,PER=0,SPD=10,PIT=PIT,VOL=VOL):
token = fetch_token()
tex = quote_plus(text) # 此处TEXT需要两次urlencode
print(tex)
params = {'tok': token, 'tex': tex, 'per': PER, 'spd': SPD, 'pit': PIT, 'vol': VOL, 'aue': AUE, 'cuid': CUID,
'lan': 'zh', 'ctp': 1} # lan ctp 固定参数
data = urlencode(params)
print('test on Web Browser' + TTS_URL + '?' + data)
req = Request(TTS_URL, data.encode('utf-8'))
has_error = False
try:
f = urlopen(req)
result_str = f.read()
headers = dict((name.lower(), value) for name, value in f.headers.items())
has_error = ('content-type' not in headers.keys() or headers['content-type'].find('audio/') < 0)
except URLError as err:
print('asr http response http code : ' + str(err.code))
result_str = err.read()
has_error = True
save_file = "error.txt" if has_error else 'result.' + FORMAT
with open(save_file, 'wb') as of:
of.write(result_str)
if has_error:
if (IS_PY3):
result_str = str(result_str, 'utf-8')
print("tts api error:" + result_str)
else:
# 初始化pygame
pygame.mixer.init()
# 将音频数据加载到内存
pygame.mixer.music.load(io.BytesIO(result_str))
# 播放音频
pygame.mixer.music.play()
# # 防止程序立即退出
# while pygame.mixer.music.get_busy():
# time.sleep(1)
print("result saved as :" + save_file)
随后我要实现调整音量的功能,但是识别出来调整音量的百分比是汉字,无法直接去调整,此处就要建立汉字的数字到阿拉伯数字的映射。
from pypinyin import lazy_pinyin
def hanzi_to_number_pinyin(hanzi_str):
hnd = {
'ling': 0, 'yi': 1, 'er': 2, 'san': 3, 'si': 4, 'wu': 5, 'liu': 6, 'qi': 7, 'ba': 8, 'jiu': 9,
'shi': 10, 'bai': 100, 'qian': 1000, 'wan': 10000, 'yi': 100000000
}
pinyin_list = lazy_pinyin(hanzi_str)
result = 0
temp = 0
for pinyin in pinyin_list:
if pinyin in hnd:
digit = hnd[pinyin]
if digit >= 10:
if temp == 0:
temp = 1
result += temp * digit
temp = 0
else:
temp = temp * 10 + digit
result += temp
return result
然后在关键词匹配的方法中实现调整音量
if "音量" in res:
volumn=res.split('百分之')[1]
volumn=hanzi_to_number_pinyin(volumn)
print(volumn)
mw.volumeSet(volumn)
然后在我们的主方法里面,开启一个新线程,进行调用即可
以下是全部代码:
# weekupm.py
import json
import time
import pyaudio
import numpy as np
from vosk import Model, KaldiRecognizer, SetLogLevel
from baidu_tts import baidu_tts_test
dir_path={
'浏览器': "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
'记事本': "C:\\Windows\\System32\\notepad.exe",
'计算器': "C:\\Windows\\System32\\calc.exe"
}
def SaveWave(model):
# 设置音频参数
FORMAT = pyaudio.paInt16 # 音频流的格式
RATE = 44100 # 采样率,单位Hz
CHUNK = 4000 # 单位帧
THRESHOLDNUM = 10 # 静默时间,超过这个个数就保存文件
THRESHOLD = 100 # 设定停止采集阈值
audio = pyaudio.PyAudio()
stream = audio.open(format=FORMAT,
channels=1,
rate=RATE,
input=True,
frames_per_buffer=CHUNK)
frames = []
print("开始录音...")
count = 0
while count < THRESHOLDNUM:
data = stream.read(CHUNK, exception_on_overflow=False)
np_data = np.frombuffer(data, dtype=np.int16)
frame_energy = np.mean(np.abs(np_data))
# print(frame_energy)
# 如果能量低于阈值持续时间过长,则停止录音
if frame_energy < THRESHOLD:
count += 1
elif count > 0:
count -= 1
frames.append(data)
print("停止录音!")
stream.stop_stream()
stream.close()
audio.terminate()
rec = KaldiRecognizer(model, RATE)
rec.SetWords(True)
str_ret = ""
for data in frames:
if rec.AcceptWaveform(data):
result = json.loads(rec.Result())
if 'text' in result:
str_ret += result['text']
result = json.loads(rec.FinalResult())
if 'text' in result:
str_ret += result['text']
str_ret = "".join(str_ret.split())
return str_ret
def answermethod(answer):
baidu_tts_test(answer)
time.sleep(1)
def weekup(mw):
model = Model("vosk-model-cn-0.15")
SetLogLevel(-1)
while 1:
res = SaveWave(model)
if res != "" and res != None:
print(res)
if "小爱同学" in res:
answer = "我在"
answermethod(answer)
if "聊天" in res:
answer = "好的,请开始"
answermethod(answer)
from stt import run as stt_run
result=stt_run()
if "打开" in res:
app_name=res.replace("打开",'').strip()
if app_name in dir_path:
app_path=dir_path[app_name]
import subprocess
subprocess.Popen(app_path)
answer = "好的,已经为您打开了"
answermethod(answer)
if "歌" in res:
from testyuncloud import yuncloudMW
mw.playMusic()
# print("播放歌曲")
if "暂停" in res:
mw.playMusic()
if "上一首" in res:
mw.prevMusic()
if "下一首" in res:
mw.nextMusic()
if "音量" in res:
volumn=res.split('百分之')[1]
volumn=hanzi_to_number_pinyin(volumn)
print(volumn)
mw.volumeSet(volumn)
from pypinyin import lazy_pinyin
def hanzi_to_number_pinyin(hanzi_str):
hnd = {
'ling': 0, 'yi': 1, 'er': 2, 'san': 3, 'si': 4, 'wu': 5, 'liu': 6, 'qi': 7, 'ba': 8, 'jiu': 9,
'shi': 10, 'bai': 100, 'qian': 1000, 'wan': 10000, 'yi': 100000000
}
pinyin_list = lazy_pinyin(hanzi_str)
result = 0
temp = 0
for pinyin in pinyin_list:
if pinyin in hnd:
digit = hnd[pinyin]
if digit >= 10:
if temp == 0:
temp = 1
result += temp * digit
temp = 0
else:
temp = temp * 10 + digit
result += temp
return result
# baidu_tts.py
# coding=utf-8
import sys
import json
import pygame
import time
import io
IS_PY3 = sys.version_info.major == 3
if IS_PY3:
from urllib.request import urlopen
from urllib.request import Request
from urllib.error import URLError
from urllib.parse import urlencode
from urllib.parse import quote_plus
else:
import urllib2
from urllib import quote_plus
from urllib2 import urlopen
from urllib2 import Request
from urllib2 import URLError
from urllib import urlencode
API_KEY = '2I46jXf5CmlNycyOVAETigud'
SECRET_KEY = 'IJu44Y0K09y93ZQAOsObkJEhOEd1NBQa'
# TEXT = "欢迎使用百度语音合成。"
# 发音人选择, 基础音库:0为度小美,1为度小宇,3为度逍遥,4为度丫丫,
# 精品音库:5为度小娇,103为度米朵,106为度博文,110为度小童,111为度小萌,默认为度小美
PER = 4
# 语速,取值0-15,默认为5中语速
SPD = 5
# 音调,取值0-15,默认为5中语调
PIT = 5
# 音量,取值0-9,默认为5中音量
VOL = 5
# 下载的文件格式, 3:mp3(default) 4: pcm-16k 5: pcm-8k 6. wav
AUE = 3
FORMATS = {3: "mp3", 4: "pcm", 5: "pcm", 6: "wav"}
FORMAT = FORMATS[AUE]
CUID = "123456PYTHON"
TTS_URL = 'http://tsn.baidu.com/text2audio'
class DemoError(Exception):
pass
""" TOKEN start """
TOKEN_URL = 'http://aip.baidubce.com/oauth/2.0/token'
SCOPE = 'audio_tts_post' # 有此scope表示有tts能力,没有请在网页里勾选
def fetch_token():
print("fetch token begin")
params = {'grant_type': 'client_credentials',
'client_id': API_KEY,
'client_secret': SECRET_KEY}
post_data = urlencode(params)
if (IS_PY3):
post_data = post_data.encode('utf-8')
req = Request(TOKEN_URL, post_data)
try:
f = urlopen(req, timeout=5)
result_str = f.read()
except URLError as err:
print('token http response http code : ' + str(err.code))
result_str = err.read()
if (IS_PY3):
result_str = result_str.decode()
print(result_str)
result = json.loads(result_str)
print(result)
if ('access_token' in result.keys() and 'scope' in result.keys()):
if not SCOPE in result['scope'].split(' '):
raise DemoError('scope is not correct')
print('SUCCESS WITH TOKEN: %s ; EXPIRES IN SECONDS: %s' % (result['access_token'], result['expires_in']))
return result['access_token']
else:
raise DemoError('MAYBE API_KEY or SECRET_KEY not correct: access_token or scope not found in token response')
""" TOKEN end """
def baidu_tts_test(text,PER=0,SPD=10,PIT=PIT,VOL=VOL):
token = fetch_token()
tex = quote_plus(text) # 此处TEXT需要两次urlencode
print(tex)
params = {'tok': token, 'tex': tex, 'per': PER, 'spd': SPD, 'pit': PIT, 'vol': VOL, 'aue': AUE, 'cuid': CUID,
'lan': 'zh', 'ctp': 1} # lan ctp 固定参数
data = urlencode(params)
print('test on Web Browser' + TTS_URL + '?' + data)
req = Request(TTS_URL, data.encode('utf-8'))
has_error = False
try:
f = urlopen(req)
result_str = f.read()
headers = dict((name.lower(), value) for name, value in f.headers.items())
has_error = ('content-type' not in headers.keys() or headers['content-type'].find('audio/') < 0)
except URLError as err:
print('asr http response http code : ' + str(err.code))
result_str = err.read()
has_error = True
save_file = "error.txt" if has_error else 'result.' + FORMAT
with open(save_file, 'wb') as of:
of.write(result_str)
if has_error:
if (IS_PY3):
result_str = str(result_str, 'utf-8')
print("tts api error:" + result_str)
else:
# 初始化pygame
pygame.mixer.init()
# 将音频数据加载到内存
pygame.mixer.music.load(io.BytesIO(result_str))
# 播放音频
pygame.mixer.music.play()
# # 防止程序立即退出
# while pygame.mixer.music.get_busy():
# time.sleep(1)
print("result saved as :" + save_file)