241205_给自己的应用加上语音助手功能
前面我们自己做了一个网易云音乐,但每次都要去点点点显得有点麻烦,所以我就考虑添加一些语音助手的功能。
其实当前在日常windows使用中,我觉得也就音乐播放需要一个语音助手交互,其他的功能,要么太复杂,简单的声控无法实现,要么语音控制的效率太低,不如直接上手操作。
最简单的语音助手仅需要实现三个功能,本地语音转文本,根据文本内容提取关键词完成对应指令,播放“收到”“已完成”等语音,我们此处就实现这样的最简单的语音助手。
语音转文字
本地语音转文字我们使用vosk语音识别库实现。model使用本地的一个60m的小模型.
这里附模型链接: https://pan.baidu.com/s/1mY2VoRqjSwS6n4qj1pPZMA?pwd=8888
解压放在项目根目录就可以了
为了实现一个较好的唤醒效果,我们采用后台实时录音的方式。检测到声音的时候就进行转文字,但是因为环境噪音是一直存在的,此时我们就要给他设定一个阈值,当超过这个阈值时,我们才认为他是有效输入,才进行转文字输入。
def SaveWave(model):# 设置音频参数FORMAT = pyaudio.paInt16 # 音频流的格式RATE = 44100 # 采样率,单位HzCHUNK = 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 = 0while 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 += 1elif count > 0:count -= 1frames.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_runresult=stt_run()if "打开" in res:app_name=res.replace("打开",'').strip()if app_name in dir_path:app_path=dir_path[app_name]import subprocesssubprocess.Popen(app_path)answer = "好的,已经为您打开了"answermethod(answer)if "歌" in res:from testyuncloud import yuncloudMWmw.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 urlopenfrom urllib.request import Requestfrom urllib.error import URLErrorfrom urllib.parse import urlencodefrom urllib.parse import quote_plus
else:import urllib2from urllib import quote_plusfrom urllib2 import urlopenfrom urllib2 import Requestfrom urllib2 import URLErrorfrom urllib import urlencodeAPI_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 = 3FORMATS = {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需要两次urlencodeprint(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 = Falsetry: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 = Truesave_file = "error.txt" if has_error else 'result.' + FORMATwith 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:# 初始化pygamepygame.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_pinyindef 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 = 0temp = 0for pinyin in pinyin_list:if pinyin in hnd:digit = hnd[pinyin]if digit >= 10:if temp == 0:temp = 1result += temp * digittemp = 0else:temp = temp * 10 + digitresult += tempreturn result
然后在关键词匹配的方法中实现调整音量
if "音量" in res:volumn=res.split('百分之')[1]volumn=hanzi_to_number_pinyin(volumn)print(volumn)mw.volumeSet(volumn)
然后在我们的主方法里面,开启一个新线程,进行调用即可
以下是全部代码:
# weekupm.py
import json
import timeimport pyaudio
import numpy as np
from vosk import Model, KaldiRecognizer, SetLogLevel
from baidu_tts import baidu_tts_testdir_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 # 采样率,单位HzCHUNK = 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 = 0while 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 += 1elif count > 0:count -= 1frames.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_retdef 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_runresult=stt_run()if "打开" in res:app_name=res.replace("打开",'').strip()if app_name in dir_path:app_path=dir_path[app_name]import subprocesssubprocess.Popen(app_path)answer = "好的,已经为您打开了"answermethod(answer)if "歌" in res:from testyuncloud import yuncloudMWmw.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_pinyindef 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 = 0temp = 0for pinyin in pinyin_list:if pinyin in hnd:digit = hnd[pinyin]if digit >= 10:if temp == 0:temp = 1result += temp * digittemp = 0else:temp = temp * 10 + digitresult += tempreturn 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 urlopenfrom urllib.request import Requestfrom urllib.error import URLErrorfrom urllib.parse import urlencodefrom urllib.parse import quote_plus
else:import urllib2from urllib import quote_plusfrom urllib2 import urlopenfrom urllib2 import Requestfrom urllib2 import URLErrorfrom urllib import urlencodeAPI_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 = 3FORMATS = {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需要两次urlencodeprint(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 = Falsetry: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 = Truesave_file = "error.txt" if has_error else 'result.' + FORMATwith 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:# 初始化pygamepygame.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)
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:# 初始化pygamepygame.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)