dever 6 лет назад
Родитель
Сommit
2a0adcffe9

+ 49 - 3
conf/dev.conf

@@ -29,17 +29,63 @@ port				= 6379
 name				= office_file
 prefix				= convert_
 
+;机器人配置 应该加到数据库中
+
+;snowboy配置
+[snowboy]
+hotword				= wukong.pmdl
+hotword_switch		= False
+on_hotword			= 悟空别吵.pmdl
+off_hotword			= 悟空醒醒.pmdl
+sensitivity			= 0.5
+silent_threshold	= 15
+recording_timeout	= 5
+;勿扰模式 开始时间-结束时间
+bother_enable		= True
+bother_since		= 1
+bother_till			= 100
+
 [robot]
 asr					= tencent
-;ai 				= tuling
-;tts 				= baidu
-;nlu 				= unit
+ai 					= tuling
+tts 				= baidu
+nlu 				= baidu
 
 [tencent]
 appid				= 1
 secret_id			= 1232
 secret_key			= 22
 region				= ap-guangzhou
+voiceType 			= 0
+language			= 1
+
+[baidu]
+appid				= 1
+api_key				= 1232
+secret_key			= 22
+dev_pid				= 1936
+per 				= 1
+lan 				= zh
+
+[xunfei]
+appid				= 1
+api_key				= 1232
+voice_name 			= vecan
+
+[ali]
+appKey				= 1
+token				= 1232
+voice 				= vecan
+
+[tuling]
+key 				= 1
+
+[emotibot]
+appid 				= 1
+location			= 深圳
+more				= False
+
+;以上为机器人配置
 
 ;定义一些tornado的配置,可为空
 [tornado]

+ 4 - 0
install.py

@@ -1,6 +1,10 @@
 # -*- coding: utf-8 -*-
 from demeter.core import *
 
+Shell.popen('pip install pydub')
+Shell.popen('pip install baidu-aip')
+Shell.popen('pip install python-dateutil')
+
 #CREATE DATABASE IF NOT EXISTS yourdbname DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
 def manage():
 	model = Demeter.model('manage_admin')

+ 61 - 4
service/__load__.py

@@ -1,12 +1,69 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 """
-    demeter service
-    name:__load__.py
-    author:rabin
+	demeter service
+	name:__load__.py
+	author:rabin
 """
 from demeter.core import *
 from datetime import *
 import uuid
 import os
-import os.path
+import os.path
+import wave
+import tempfile
+from pydub import AudioSegment
+from uuid import getnode as get_mac
+from abc import ABCMeta, abstractmethod
+
+""" 
+将 mp3 文件转成 wav
+
+:param file: mp3 文件路径
+:return: wav 文件路径
+"""
+def convertMp3ToWav(file):
+	target = file.replace(".mp3", ".wav")
+	if not os.path.exists(file):
+		Demeter.logger.critical("文件错误 {}".format(file))
+		return None
+	AudioSegment.from_mp3(file).export(target, format="wav")
+	return target
+
+""" 
+将 wav 文件转成 mp3
+
+:param file: wav 文件路径
+:return: mp3 文件路径
+"""
+def convertWavToMp3(file):
+	target = file.replace('.wav', '.mp3')
+	if not os.path.exists(file):
+		Demeter.logger.critical("文件错误 {}".format(file))
+		return None
+	AudioSegment.from_wav(file).export(target, format="mp3")
+	return target
+
+""" 
+从 wav 文件中读取 pcm
+
+:param file: wav 文件路径
+:return: pcm 数据
+"""
+def getPcmFromWav(file):
+	wav = wave.open(file, 'rb')
+	return wav.readframes(wav.getnframes())
+
+""" 
+二进制形式写入临时文件
+
+:param data: 二进制数据
+:param suffix: 后缀名
+:return: 文件保存后的路径
+"""
+def writeTempFile(data, suffix):
+	
+	with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
+		f.write(data)
+		tmpfile = f.name
+	return tmpfile

+ 0 - 0
service/asr/__init__.py → service/api/__init__.py


+ 30 - 0
service/api/ali.py

@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+"""
+阿里的语音识别API
+"""
+from __future__ import division
+from ..__load__ import *
+from base import Base
+
+class Ali(Base):
+
+	def __init__(self):
+		super(self.__class__, self).__init__(True)
+
+	def setting(self, appKey, token, voice='vecan', **args):
+		self.appKey = appKey
+		self.token = token
+
+	def asr(self, fp):
+		result = self.sdk.asr(self.appKey, self.token, fp)
+		if result is not None:
+			return self.arrLog(1, result)
+		else:
+			return self.arrLog(2, '')
+
+	def tts(self, phrase):
+		tmpfile = self.sdk.tts(self.appKey, self.token, self.voice, phrase)
+		if tmpfile is not None:
+			return self.ttsLog(1, tmpfile)
+		else:
+			return self.ttsLog(2, '')

+ 109 - 0
service/api/baidu.py

@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+"""
+百度的语音识别API.
+dev_pid:
+	- 1936: 普通话远场
+	- 1536:普通话(支持简单的英文识别)
+	- 1537:普通话(纯中文识别)
+	- 1737:英语
+	- 1637:粤语
+	- 1837:四川话
+要使用本模块, 首先到 yuyin.baidu.com 注册一个开发者账号,
+之后创建一个新应用, 然后在应用管理的"查看key"中获得 API Key 和 Secret Key
+填入 config.xml 中.
+...
+	baidu_yuyin: 
+		appid: '9670645'
+		api_key: 'qg4haN8b2bGvFtCbBGqhrmZy'
+		secret_key: '585d4eccb50d306c401d7df138bb02e7'
+	...
+"""
+from __future__ import division
+from ..__load__ import *
+from base import Base
+# 需要百度SDK
+from aip import AipSpeech
+
+class Baidu(Base):
+
+	def __init__(self):
+		super(self.__class__, self).__init__(True)
+
+	def setting(self, appid, api_key, secret_key, dev_pid=1936, per=1, lan='zh', **args):
+		self.engine = AipSpeech(appid, api_key, secret_key)
+		self.dev_pid = dev_pid
+		self.per = per
+		self.lan = lan
+
+	def asr(self, fp):
+		pcm = getPcmFromWav(fp)
+		res = self.engine.asr(pcm, 'pcm', 16000, {
+			'dev_pid': self.dev_pid,
+		})
+		if res['err_no'] == 0:
+			res['result'] = ''.join(res['result'])
+			return self.arrLog(1, res['result'])
+		else:
+			res['err_msg'] = ''.join(res['err_msg'])
+			return self.arrLog(2, res['err_msg'])
+
+	def tts(self, phrase):
+		result  = self.engine.synthesis(phrase, self.lan, 1, {'per': self.per});
+		# 识别正确返回语音二进制 错误则返回dict 参照下面错误码
+		if not isinstance(result, dict):
+			tmpfile = writeTempFile(result, '.mp3')
+			return self.ttsLog(1, tmpfile)
+		else:
+			return self.ttsLog(2, '')
+
+	#使用百度 UNIT 进行 NLU 解析
+	def nlu(self, query, service_id):
+		return self.sdk.getUnit(query, service_id, self.api_key, self.secret_key)
+
+	""" 
+	提取意图
+	:param parsed: 解析结果
+	:return: 意图数组
+	"""
+	def getIntent(self, parsed):
+		return self.sdk.getIntent(parsed)
+
+	""" 
+	判断是否包含某个意图
+	:param parsed: UNIT 解析结果
+	:param intent: 意图的名称
+	:return: True: 包含; False: 不包含
+	"""
+	def hasIntent(self, parsed, intent):
+		return self.sdk.hasIntent(parsed, intent)
+
+	""" 
+	提取某个意图的所有词槽
+
+	:param parsed: UNIT 解析结果
+	:param intent: 意图的名称
+	:return: 词槽列表。你可以通过 name 属性筛选词槽,
+	再通过 normalized_word 属性取出相应的值
+	"""
+	def getSlots(self, parsed, intent):
+		return self.sdk.getSlots(parsed, intent)
+
+	""" 
+	找出命中某个词槽的内容
+
+	:param parsed: UNIT 解析结果
+	:param intent: 意图的名称
+	:param name: 词槽名
+	:return: 命中该词槽的值的列表。
+	"""
+	def getSlotWords(self, parsed, intent, name):
+		return self.sdk.getSlotWords(parsed, intent, name)
+
+	"""
+	提取 UNIT 的回复文本
+	:param parsed: UNIT 解析结果
+	:param intent: 意图的名称
+	:return: UNIT 的回复文本
+	"""
+	def getSay(self, parsed, intent):
+		return self.sdk.getSay(parsed, intent)

+ 69 - 0
service/api/base.py

@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+from __future__ import division
+from ..__load__ import *
+
+class Base(object):
+
+	def __init__(self, sdk = False):
+		self.name = self.__class__.__name__
+		self.name = self.name.lower()
+		if sdk == True:
+			self.sdk = Demeter.service(self.name, 'sdk')
+		pass
+
+	# Automatic Speech Recognition asr 自动语音识别
+	@abstractmethod
+	def asr(self, fp):
+		pass
+
+	# AI 聊天机器人
+	@abstractmethod
+	def chat(self, texts):
+		pass
+
+	# Text To Speech 人机对话 文本转语音
+	@abstractmethod
+	def tts(self, phrase):
+		pass
+
+	# Natural Language Understanding 自然语言理解
+	@abstractmethod
+	def nlu(self, query):
+		pass
+
+	# 设置工具
+	@abstractmethod
+	def setting(self, **args):
+		pass
+
+	def asrLog(self, state, msg = ''):
+		if state == 1:
+			method = 'info'
+			text = '语音识别成功'
+		else:
+			method = 'critical'
+			text = '语音识别失败'
+		return self.log(method, text, msg)
+
+	def chatLog(self, state, msg = ''):
+		if state == 1:
+			method = 'info'
+			text = '回答'
+		else:
+			method = 'critical'
+			text = '未做回答'
+		return self.log(method, text, msg)
+
+	def ttsLog(self, state, msg = ''):
+		if state == 1:
+			method = 'info'
+			text = '语音合成成功,合成路径'
+		else:
+			method = 'critical'
+			text = '语音合成失败'
+		return self.log(method, text, msg)
+
+	def log(self, method, text, msg = ''):
+		logger = getattr(Demeter.logger, method)
+		logger('{}-{}:{}'.format(self.name, text, msg))
+		return msg;

+ 46 - 0
service/api/emotibot.py

@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+"""
+Emotibot机器人API:http://www.emotibot.com
+"""
+from __future__ import division
+from ..__load__ import *
+from base import Base
+
+class Emotibot(Base):
+
+	def __init__(self):
+		super(self.__class__, self).__init__()
+
+	def setting(self, appid, location, more, **args):
+		self.appid, self.location, self.more = appid, location, more
+
+	def chat(self, fp):
+		msg = ''.join(texts)
+		try:
+			url = "http://idc.emotibot.com/api/ApiKey/openapi.php"
+			userid = str(get_mac())[:32]
+			register_data = {
+				"cmd": "chat",
+				"appid": self.appid,
+				"userid": userid,
+				"text": msg,
+				"location": self.location
+			}
+			r = requests.post(url, params=register_data)
+			jsondata = json.loads(r.text)
+			result = ''
+			responds = []
+			if jsondata['return'] == 0:
+				if self.more:
+					datas = jsondata.get('data')
+					for data in datas:
+						if data.get('type') == 'text':
+							responds.append(data.get('value'))
+				else:
+					responds.append(jsondata.get('data')[0].get('value'))
+				result = '\n'.join(responds)
+			else:
+				result = "抱歉, 我的大脑短路了,请稍后再试试."
+			return self.chatLog(1, result)
+		except Exception:
+			return self.chatLog(2, msg)

+ 50 - 0
service/api/tencent.py

@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+"""
+腾讯API
+"""
+from __future__ import division
+from ..__load__ import *
+from base import Base
+
+class Tencent(Base):
+
+	def __init__(self):
+		super(self.__class__, self).__init__(True)
+
+	def setting(self, appid, secret_id, secret_key, region='ap-guangzhou', voiceType=0, language=1, **args):
+		self.engine = self.sdk.init(secret_id, secret_key)
+		self.region = region
+		self.voiceType = voiceType
+		self.language = language
+
+	def asr(self, fp):
+		file = convertMp3ToWav(fp)
+		r = self.engine.ASR(file, 'mp3', '1', self.region)
+		File.remove(file)
+		res = json.loads(r)
+		if 'Response' in res and 'Result' in res['Response']:
+			return self.asrLog(1, res['Response']['Result'])
+		else:
+			return self.asrLog(2, '')
+
+	"""
+	腾讯的语音合成
+	region: 服务地域,挑个离自己最近的区域有助于提升速度。
+		有效值:https://cloud.tencent.com/document/api/441/17365#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
+	voiceType:
+		- 0:女声1,亲和风格(默认)
+		- 1:男声1,成熟风格
+		- 2:男声2,成熟风格
+	language:
+		- 1: 中文,最大100个汉字(标点符号算一个汉子)
+		- 2: 英文,最大支持400个字母(标点符号算一个字母)
+	"""
+	def tts(self, phrase):
+		result = self.engine.TTS(phrase, self.voiceType, self.language, self.region)
+		if 'Response' in result and 'Audio' in result['Response']:
+			audio = result['Response']['Audio']
+			data = base64.b64decode(audio)
+			tmpfile = writeTempFile(data, '.wav')
+			return self.ttsLog(1, tmpfile)
+		else:
+			return self.ttsLog(2, '')

+ 40 - 0
service/api/tuling.py

@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+"""
+图灵机器人API
+"""
+from __future__ import division
+from ..__load__ import *
+from base import Base
+
+class Tuling(Base):
+
+	def __init__(self):
+		super(self.__class__, self).__init__()
+
+	def setting(self, key, **args):
+		self.key = key
+
+	def chat(self, fp):
+		msg = ''.join(texts)
+		try:
+			url = "http://www.tuling123.com/openapi/api"
+			userid = str(get_mac())[:32]
+			body = {'key': self.key, 'info': msg, 'userid': userid}
+			r = requests.post(url, data=body)
+			respond = json.loads(r.text)
+			result = ''
+			if respond['code'] == 100000:
+				result = respond['text'].replace('<br>', '  ')
+				result = result.replace(u'\xa0', u' ')
+			elif respond['code'] == 200000:
+				result = respond['url']
+			elif respond['code'] == 302000:
+				for k in respond['list']:
+					result = result + u"【" + k['source'] + u"】 " +\
+						k['article'] + "\t" + k['detailurl'] + "\n"
+			else:
+				result = respond['text'].replace('<br>', '  ')
+				result = result.replace(u'\xa0', u' ')
+			return self.chatLog(1, result)
+		except Exception:
+			return self.chatLog(2, msg)

+ 68 - 0
service/api/xunfei.py

@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+"""
+科大讯飞的语音识别API.
+外网ip查询:https://ip.51240.com/
+voice_name: https://www.xfyun.cn/services/online_tts
+"""
+from __future__ import division
+from ..__load__ import *
+from base import Base
+
+class Baidu(Base):
+
+	url = 'http://api.xfyun.cn/v1/service/v1/'
+	def __init__(self):
+		super(self.__class__, self).__init__()
+
+	def setting(self, appid, api_key, voice_name='vecan', **args):
+		self.appid = appid
+		self.api_key = api_key
+		self.voice_name = voice_name
+
+	def getHeader(self, aue, engineType):
+		curTime = str(int(time.time()))
+		# curTime = '1526542623'
+		if aue = 'raw':
+			param = "{\"aue\":\"" + aue + "\"" + ",\"engine_type\":\"" + engineType + "\"}"
+		else:
+			param = "{\"aue\":\""+aue+"\",\"auf\":\"audio/L16;rate=16000\",\"voice_name\":\"" + self.voice_name + "\",\"engine_type\":\"intp65\"}"
+		Demeter.logger.debug("param:{}".format(param))
+		paramBase64 = str(base64.b64encode(param.encode('utf-8')), 'utf-8')
+		Demeter.logger.debug("x_param:{}".format(paramBase64))
+
+		m2 = hashlib.md5()
+		m2.update((self.api_key + curTime + paramBase64).encode('utf-8'))
+		checkSum = m2.hexdigest()
+		header = {
+			'X-CurTime': curTime,
+			'X-Param': paramBase64,
+			'X-Appid': self.appid,
+			'X-CheckSum': checkSum,
+			'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
+		}
+		return header
+
+	def getBody(self, filepath):
+		binfile = open(filepath, 'rb')
+		data = {'audio': base64.b64encode(binfile.read())}
+		return data
+
+	def asr(self, fp):
+		URL = self.url + "iat"
+		r = requests.post(URL, headers=self.getHeader('raw', 'sms16k'), data=self.getBody(fp))
+		res = json.loads(r.content.decode('utf-8'))
+		Demeter.logger.debug(res)
+		if 'code' in res and res['code'] == '0':
+			return self.log(1, res['data'])
+		else:
+			return self.log(2, '')
+
+	def tts(self, phrase):
+		URL = self.url + "tts"
+		r = requests.post(URL, headers=self.getHeader('lame'), data=self.getBody(phrase))
+		contentType = r.headers['Content-Type']
+		if contentType == "audio/mpeg":
+			tmpfile = writeTempFile(r.content, '.mp3')
+			return self.ttsLog(1, tmpfile)
+		else :
+			return self.ttsLog(2, r.text)

+ 0 - 11
service/asr/base.py

@@ -1,11 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import division
-from ..__load__ import *
-
-class Base(object):
-
-	def __init__(self):
-		pass
-
-	def setting(self, **args):
-		pass

+ 0 - 26
service/asr/tencent.py

@@ -1,26 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import division
-from ..__load__ import *
-from base import Base
-
-class Tencent(Base):
-
-	def __init__(self):
-		super(self.__class__, self).__init__()
-		self.sdk = Demeter.service('tencent', 'sdk')
-
-	def setting(self, appid, secret_id, secret_key, region, **args):
-		self.engine = self.sdk.init(secret_id, secret_key)
-		self.region = region
-
-	def transcribe(self, fp):
-		mp3_path = utils.convert_wav_to_mp3(fp)
-		r = self.engine.ASR(mp3_path, 'mp3', '1', self.region)
-		utils.check_and_delete(mp3_path)
-		res = json.loads(r)
-		if 'Response' in res and 'Result' in res['Response']:
-			logger.info('{} 语音识别到了:{}'.format(self.SLUG, res['Response']['Result']))
-			return res['Response']['Result']
-		else:
-			logger.critical('{} 语音识别出错了'.format(self.SLUG), exc_info=True)
-			return ''

+ 69 - 6
service/robot.py

@@ -3,14 +3,31 @@ from __future__ import division
 from .__load__ import *
 
 class Robot(object):
-
+	tool = {}
 	def __init__(self, profiling=False):
 		self.start()
+		self.isRecording = False
+
+	def say(self, msg, cache=False, plugin='', onCompleted=None):
+		print msg
+		self.player = Player.SoxPlayer()
+		voice = self.tool['tts'].tts(msg)
+		if onCompleted is None:
+			onCompleted = lambda: self.onCompleted(msg)
+		self.player.play(voice, not cache, onCompleted)
 
-	def say(self, content):
-		print content
+	def onCompleted(self, msg):
+		if config.get('active_mode', False) and \
+		   (
+			   msg.endswith('?') or 
+			   msg.endswith(u'?') or 
+			   u'告诉我' in msg or u'请回答' in msg
+		   ):
+			query = self.activeListen()
+			self.doResponse(query)
 
 	def start(self):
+		# 机器人配置,后续改成从数据库读取
 		self.config = Demeter.config['robot']
 		self.reload()
 
@@ -18,12 +35,58 @@ class Robot(object):
 		try:
 			for item in self.config:
 				setting = self.setting(self.config[item])
-				self.config[item] = Demeter.service(self.config[item], item)
-				self.config[item].setting(**setting)
+				if item not in self.tool:
+					self.tool[item] = Demeter.service(self.config[item], 'api')
+					self.tool[item].setting(**setting)
+			self.player = None
 		except Exception as e:
 			Demeter.logger.critical("机器人初始化失败:{}".format(e))
 
 	def setting(self, name):
 		# 后续要改成数据库读取
 		config = Demeter.config[name]
-		return config
+		return config
+
+	def interrupt(self):
+		if self.player is not None and self.player.is_playing():
+			self.player.stop()
+			self.player = None
+		if self.immersiveMode:
+			self.brain.pause()
+
+	def converse(self, fp, callback=None):
+		""" 核心对话逻辑 """
+		Player.play(constants.getData('beep_lo.wav'))
+		logger.info('结束录音')
+		self.isRecording = False
+		if self.profiling:
+			logger.info('性能调试已打开')
+			pr = cProfile.Profile()
+			pr.enable()
+			self.doConverse(fp, callback)
+			pr.disable()
+			s = io.StringIO()
+			sortby = 'cumulative'
+			ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
+			ps.print_stats()
+			print(s.getvalue())
+		else:
+			self.doConverse(fp, callback)
+
+	def doConverse(self, fp, callback=None, onSay=None):
+		try:
+			self.interrupt()
+			query = self.asr.transcribe(fp)
+			utils.check_and_delete(fp)
+			self.doResponse(query, callback, onSay)
+		except Exception as e:
+			logger.critical(e)
+			utils.clean()
+
+	def pardon(self):
+		if not self.hasPardon:
+			self.say("抱歉,刚刚没听清,能再说一遍吗?", onCompleted=lambda: self.doResponse(self.activeListen()))
+			self.hasPardon = True
+		else:
+			self.say("没听清呢")
+			self.hasPardon = False

+ 0 - 92
service/sdk/AbstractPlugin.py

@@ -1,92 +0,0 @@
-from abc import ABCMeta, abstractmethod
-from robot import constants
-from robot import logging
-import sys
-
-logger = logging.getLogger(__name__)
-
-try:
-    sys.path.append(constants.CONTRIB_PATH)
-except Exception as e:
-    logger.debug("未检测到插件目录,Error:{}".format(e))
-    
-class AbstractPlugin(metaclass=ABCMeta):
-    """ 技能插件基类 """
-
-    SLUG = 'AbstractPlugin'
-    IS_IMMERSIVE = False
-
-    def __init__(self, con):
-        if self.IS_IMMERSIVE is not None:
-            self.isImmersive = self.IS_IMMERSIVE
-        else:
-            self.isImmersive = False
-        self.priority = 0
-        self.con = con
-        self.nlu = self.con.nlu
-
-    def play(self, src, delete=False, onCompleted=None, volume=1):
-        self.con.play(src, delete, onCompleted, volume)
-
-    def say(self, text, cache=False, onCompleted=None):
-        self.con.say(text, cache=cache, plugin=self.SLUG, onCompleted=onCompleted)
-
-    def activeListen(self, silent=False):
-        return self.con.activeListen(silent)
-
-    def clearImmersive(self):
-        self.con.setImmersiveMode(None)
-
-    @abstractmethod
-    def isValid(self, query, parsed):
-        """
-        是否适合由该插件处理
-
-        参数:
-        query -- 用户的指令字符串
-        parsed -- 用户指令经过 NLU 解析后的结果
-
-        返回:
-        True: 适合由该插件处理
-        False: 不适合由该插件处理
-        """
-        return False
-
-    @abstractmethod
-    def handle(self, query, parsed):
-        """
-        处理逻辑
-
-        参数:
-        query -- 用户的指令字符串
-        parsed -- 用户指令经过 NLU 解析后的结果
-        """
-        pass
-
-    def isValidImmersive(self, query, parsed):
-        """
-        是否适合在沉浸模式下处理,
-        仅适用于有沉浸模式的插件(如音乐等)
-        当用户唤醒时,可以响应更多指令集。
-        例如:“"上一首"、"下一首" 等
-        """
-        return False
-
-    def pause(self):
-        """
-        暂停当前正在处理的任务,
-        当处于该沉浸模式下且被唤醒时,
-        将自动触发这个方法,
-        可以用于强制暂停一个耗时的操作        
-        """
-        return
-
-    def restore(self):
-        """
-        恢复当前插件,
-        仅适用于有沉浸模式的插件(如音乐等)
-        当用户误唤醒或者唤醒进行闲聊后,
-        可以自动恢复当前插件的处理逻辑
-        """
-        return
-    

+ 0 - 141
service/sdk/AliSpeech.py

@@ -1,141 +0,0 @@
-# -*- coding: UTF-8 -*-
-
-import http.client
-import urllib.parse
-import json
-from robot import utils
-from robot import logging
-
-logger = logging.getLogger(__name__)
-
-def processGETRequest(appKey, token, voice, text, format, sampleRate) :
-    host = 'nls-gateway.cn-shanghai.aliyuncs.com'
-    url = 'https://' + host + '/stream/v1/tts'
-    # 设置URL请求参数
-    url = url + '?appkey=' + appKey
-    url = url + '&token=' + token
-    url = url + '&text=' + text
-    url = url + '&format=' + format
-    url = url + '&sample_rate=' + str(sampleRate)
-    url = url + '&voice=' + voice
-    logger.debug(url)
-    conn = http.client.HTTPSConnection(host)
-    conn.request(method='GET', url=url)
-    # 处理服务端返回的响应
-    response = conn.getresponse()
-    logger.debug('Response status and response reason:')
-    logger.debug(response.status ,response.reason)
-    contentType = response.getheader('Content-Type')
-    logger.debug(contentType)
-    body = response.read()
-    if 'audio/mpeg' == contentType :
-        logger.debug('The GET request succeed!')
-        tmpfile = utils.write_temp_file(body, '.mp3')
-        conn.close()
-        return tmpfile
-    else :
-        logger.debug('The GET request failed: ' + str(body))
-        conn.close()
-        return None
-
-def processPOSTRequest(appKey, token, voice, text, format, sampleRate) :
-    host = 'nls-gateway.cn-shanghai.aliyuncs.com'
-    url = 'https://' + host + '/stream/v1/tts'
-    # 设置HTTPS Headers
-    httpHeaders = {
-        'Content-Type': 'application/json'
-        }
-    # 设置HTTPS Body
-    body = {'appkey': appKey, 'token': token, 'text': text, 'format': format, 'sample_rate': sampleRate, 'voice': voice}
-    body = json.dumps(body)
-    logger.debug('The POST request body content: ' + body)
-    # Python 2.x 请使用httplib
-    # conn = httplib.HTTPSConnection(host)
-    # Python 3.x 请使用http.client
-    conn = http.client.HTTPSConnection(host)
-    conn.request(method='POST', url=url, body=body, headers=httpHeaders)
-    # 处理服务端返回的响应
-    response = conn.getresponse()
-    logger.debug('Response status and response reason:')
-    logger.debug(response.status ,response.reason)
-    contentType = response.getheader('Content-Type')
-    logger.debug(contentType)
-    body = response.read()
-    if 'audio/mpeg' == contentType :
-        logger.debug('The POST request succeed!')
-        tmpfile = utils.write_temp_file(body, '.mp3')
-        conn.close()
-        return tmpfile
-    else :
-        logger.critical('The POST request failed: ' + str(body))
-        conn.close()
-        return None
-
-def process(request, token, audioContent) :
-    # 读取音频文件
-    host = 'nls-gateway.cn-shanghai.aliyuncs.com'
-    # 设置HTTP请求头部
-    httpHeaders = {
-        'X-NLS-Token': token,
-        'Content-type': 'application/octet-stream',
-        'Content-Length': len(audioContent)
-        }
-    conn = http.client.HTTPConnection(host)
-    conn.request(method='POST', url=request, body=audioContent, headers=httpHeaders)
-    response = conn.getresponse()
-    logger.debug('Response status and response reason:')
-    logger.debug(response.status ,response.reason)
-    body = response.read()
-    try:
-        logger.debug('Recognize response is:')
-        body = json.loads(body)
-        logger.debug(body)
-        status = body['status']
-        if status == 20000000 :
-            result = body['result']
-            logger.debug('Recognize result: ' + result)
-            conn.close()
-            return result
-        else :
-            logger.critical('Recognizer failed!')
-            conn.close()
-            return None
-    except ValueError:
-        logger.debug('The response is not json format string')
-        conn.close()
-        return None
-
-def tts(appKey, token, voice, text):
-    # 采用RFC 3986规范进行urlencode编码
-    textUrlencode = text
-    textUrlencode = urllib.parse.quote_plus(textUrlencode)
-    textUrlencode = textUrlencode.replace("+", "%20")
-    textUrlencode = textUrlencode.replace("*", "%2A")
-    textUrlencode = textUrlencode.replace("%7E", "~")
-    format = 'mp3'
-    sampleRate = 16000
-    return processPOSTRequest(appKey, token, voice, text, format, sampleRate)
-
-def asr(appKey, token, wave_file):
-    # 服务请求地址
-    url = 'http://nls-gateway.cn-shanghai.aliyuncs.com/stream/v1/asr'
-    pcm = utils.get_pcm_from_wav(wave_file)
-    # 音频文件
-    format = 'pcm'
-    sampleRate = 16000
-    enablePunctuationPrediction  = True
-    enableInverseTextNormalization = True
-    enableVoiceDetection  = False
-    # 设置RESTful请求参数
-    request = url + '?appkey=' + appKey
-    request = request + '&format=' + format
-    request = request + '&sample_rate=' + str(sampleRate)
-    if enablePunctuationPrediction :
-        request = request + '&enable_punctuation_prediction=' + 'true'
-    if enableInverseTextNormalization :
-        request = request + '&enable_inverse_text_normalization=' + 'true'
-    if enableVoiceDetection :
-        request = request + '&enable_voice_detection=' + 'true'
-    logger.debug('Request: ' + request)
-    return process(request, token, pcm)
-    

+ 0 - 137
service/sdk/RASRsdk.py

@@ -1,137 +0,0 @@
-# -*- coding:utf-8 -*-
-import urllib.request
-import hmac
-import hashlib
-import base64
-import time
-import random
-import os
-import json
-
-
-def formatSignString(param):
-    signstr = "POSTaai.qcloud.com/asr/v1/"
-    for t in param:
-        if 'appid' in t:
-            signstr += str(t[1])
-            break
-    signstr += "?"
-    for x in param:
-        tmp = x
-        if 'appid' in x:
-            continue
-        for t in tmp:
-            signstr += str(t)
-            signstr += "="
-        signstr = signstr[:-1]
-        signstr += "&"
-    signstr = signstr[:-1]
-    # print 'signstr',signstr
-    return signstr
-
-
-def sign(signstr, secret_key):
-    sign_bytes= bytes(signstr , 'utf-8')
-    secret_bytes = bytes(secret_key, 'utf-8')
-    hmacstr = hmac.new(secret_bytes, sign_bytes, hashlib.sha1).digest()
-    s = base64.b64encode(hmacstr).decode('utf-8')
-    return s
-
-
-def randstr(n):
-    seed = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
-    sa = []
-    for i in range(n):
-        sa.append(random.choice(seed))
-    salt = ''.join(sa)
-    # print salt
-    return salt
-
-
-def sendVoice(secret_key, secretid, appid, engine_model_type, res_type, result_text_format, voice_format, filepath, cutlength, template_name=""):
-    if len(str(secret_key)) == 0:
-        print('secretKey can not empty')
-        return
-    if len(str(secretid)) == 0:
-        print('secretid can not empty')
-        return
-    if len(str(appid)) == 0:
-        print('appid can not empty')
-        return
-    if len(str(engine_model_type)) == 0 or (
-            str(engine_model_type) != '8k_0' and str(engine_model_type) != '16k_0' and str(
-        engine_model_type) != '16k_en'):
-        print('engine_model_type is not right')
-        return
-    if len(str(res_type)) == 0 or (str(res_type) != '0' and str(res_type) != '1'):
-        print('res_type is not right')
-        return
-    if len(str(result_text_format)) == 0 or (str(result_text_format) != '0' and str(result_text_format) != '1' and str(
-            result_text_format) != '2' and str(result_text_format) != '3'):
-        print('result_text_format is not right')
-        return
-    if len(str(voice_format)) == 0 or (
-            str(voice_format) != '1' and str(voice_format) != '4' and str(voice_format) != '6'):
-        print('voice_format is not right')
-        return
-    if len(str(filepath)) == 0:
-        print('filepath can not empty')
-        return
-    if len(str(cutlength)) == 0 or str(cutlength).isdigit() == False or cutlength > 200000:
-        print('cutlength can not empty')
-        return
-    # secret_key = "oaYWFO70LGDmcpfwo8uF1IInayysGtgZ"
-    query_arr = dict()
-    query_arr['appid'] = appid
-    query_arr['projectid'] = 1013976
-    if len(template_name) > 0:
-        query_arr['template_name'] = template_name
-    query_arr['sub_service_type'] = 1
-    query_arr['engine_model_type'] = engine_model_type
-    query_arr['res_type'] = res_type
-    query_arr['result_text_format'] = result_text_format
-    query_arr['voice_id'] = randstr(16)
-    query_arr['timeout'] = 100
-    query_arr['source'] = 0
-    query_arr['secretid'] = secretid
-    query_arr['timestamp'] = str(int(time.time()))
-    query_arr['expired'] = int(time.time()) + 24 * 60 * 60
-    query_arr['nonce'] = query_arr['timestamp'][0:4]
-    query_arr['voice_format'] = voice_format
-    file_object = open(filepath, 'rb')
-    file_object.seek(0, os.SEEK_END)
-    datalen = file_object.tell()
-    file_object.seek(0, os.SEEK_SET)
-    seq = 0
-    response = []
-    while (datalen > 0):
-        end = 0
-        if (datalen < cutlength):
-            end = 1
-        query_arr['end'] = end
-        query_arr['seq'] = seq
-        query = sorted(query_arr.items(), key=lambda d: d[0])
-        signstr = formatSignString(query)
-        autho = sign(signstr, secret_key)
-
-        if (datalen < cutlength):
-            content = file_object.read(datalen)
-        else:
-            content = file_object.read(cutlength)
-        seq = seq + 1
-        datalen = datalen - cutlength
-        headers = dict()
-        headers['Authorization'] = autho
-        headers['Content-Length'] = len(content)
-        requrl = "http://"
-        requrl += signstr[4::]
-        req = urllib.request.Request(requrl, data=content, headers=headers)        
-        res_data = urllib.request.urlopen(req)
-        r = res_data.read().decode('utf-8')
-        res = json.loads(r)
-        if res['code'] == 0:
-            response.append(res['text'])    
-    file_object.close()
-    return response[len(response)-1]
-
-

+ 139 - 0
service/sdk/ali.py

@@ -0,0 +1,139 @@
+# -*- coding: UTF-8 -*-
+from __future__ import division
+from ..__load__ import *
+
+import http.client
+import urllib.parse
+
+class Ali(object):
+	def processGETRequest(self, appKey, token, voice, text, format, sampleRate) :
+		host = 'nls-gateway.cn-shanghai.aliyuncs.com'
+		url = 'https://' + host + '/stream/v1/tts'
+		# 设置URL请求参数
+		url = url + '?appkey=' + appKey
+		url = url + '&token=' + token
+		url = url + '&text=' + text
+		url = url + '&format=' + format
+		url = url + '&sample_rate=' + str(sampleRate)
+		url = url + '&voice=' + voice
+		Demeter.logger.debug(url)
+		conn = http.client.HTTPSConnection(host)
+		conn.request(method='GET', url=url)
+		# 处理服务端返回的响应
+		response = conn.getresponse()
+		Demeter.logger.debug('Response status and response reason:')
+		Demeter.logger.debug(response.status ,response.reason)
+		contentType = response.getheader('Content-Type')
+		Demeter.logger.debug(contentType)
+		body = response.read()
+		if 'audio/mpeg' == contentType :
+			Demeter.logger.debug('The GET request succeed!')
+			tmpfile = writeTempFile(body, '.mp3')
+			conn.close()
+			return tmpfile
+		else :
+			Demeter.logger.debug('The GET request failed: ' + str(body))
+			conn.close()
+			return None
+
+	def processPOSTRequest(self, appKey, token, voice, text, format, sampleRate) :
+		host = 'nls-gateway.cn-shanghai.aliyuncs.com'
+		url = 'https://' + host + '/stream/v1/tts'
+		# 设置HTTPS Headers
+		httpHeaders = {
+			'Content-Type': 'application/json'
+			}
+		# 设置HTTPS Body
+		body = {'appkey': appKey, 'token': token, 'text': text, 'format': format, 'sample_rate': sampleRate, 'voice': voice}
+		body = json.dumps(body)
+		Demeter.logger.debug('The POST request body content: ' + body)
+		# Python 2.x 请使用httplib
+		# conn = httplib.HTTPSConnection(host)
+		# Python 3.x 请使用http.client
+		conn = http.client.HTTPSConnection(host)
+		conn.request(method='POST', url=url, body=body, headers=httpHeaders)
+		# 处理服务端返回的响应
+		response = conn.getresponse()
+		Demeter.logger.debug('Response status and response reason:')
+		Demeter.logger.debug(response.status ,response.reason)
+		contentType = response.getheader('Content-Type')
+		Demeter.logger.debug(contentType)
+		body = response.read()
+		if 'audio/mpeg' == contentType :
+			Demeter.logger.debug('The POST request succeed!')
+			tmpfile = writeTempFile(body, '.mp3')
+			conn.close()
+			return tmpfile
+		else :
+			Demeter.logger.critical('The POST request failed: ' + str(body))
+			conn.close()
+			return None
+
+	def process(self, request, token, audioContent) :
+		# 读取音频文件
+		host = 'nls-gateway.cn-shanghai.aliyuncs.com'
+		# 设置HTTP请求头部
+		httpHeaders = {
+			'X-NLS-Token': token,
+			'Content-type': 'application/octet-stream',
+			'Content-Length': len(audioContent)
+			}
+		conn = http.client.HTTPConnection(host)
+		conn.request(method='POST', url=request, body=audioContent, headers=httpHeaders)
+		response = conn.getresponse()
+		Demeter.logger.debug('Response status and response reason:')
+		Demeter.logger.debug(response.status ,response.reason)
+		body = response.read()
+		try:
+			Demeter.logger.debug('Recognize response is:')
+			body = json.loads(body)
+			Demeter.logger.debug(body)
+			status = body['status']
+			if status == 20000000 :
+				result = body['result']
+				Demeter.logger.debug('Recognize result: ' + result)
+				conn.close()
+				return result
+			else :
+				Demeter.logger.critical('Recognizer failed!')
+				conn.close()
+				return None
+		except ValueError:
+			Demeter.logger.debug('The response is not json format string')
+			conn.close()
+			return None
+
+	def tts(self, appKey, token, voice, text):
+		# 采用RFC 3986规范进行urlencode编码
+		textUrlencode = text
+		textUrlencode = urllib.parse.quote_plus(textUrlencode)
+		textUrlencode = textUrlencode.replace("+", "%20")
+		textUrlencode = textUrlencode.replace("*", "%2A")
+		textUrlencode = textUrlencode.replace("%7E", "~")
+		format = 'mp3'
+		sampleRate = 16000
+		return self.processPOSTRequest(appKey, token, voice, text, format, sampleRate)
+
+	def asr(self, appKey, token, wave_file):
+		# 服务请求地址
+		url = 'http://nls-gateway.cn-shanghai.aliyuncs.com/stream/v1/asr'
+		pcm = getPcmFromWav(wave_file)
+		# 音频文件
+		format = 'pcm'
+		sampleRate = 16000
+		enablePunctuationPrediction  = True
+		enableInverseTextNormalization = True
+		enableVoiceDetection  = False
+		# 设置RESTful请求参数
+		request = url + '?appkey=' + appKey
+		request = request + '&format=' + format
+		request = request + '&sample_rate=' + str(sampleRate)
+		if enablePunctuationPrediction :
+			request = request + '&enable_punctuation_prediction=' + 'true'
+		if enableInverseTextNormalization :
+			request = request + '&enable_inverse_text_normalization=' + 'true'
+		if enableVoiceDetection :
+			request = request + '&enable_voice_detection=' + 'true'
+		Demeter.logger.debug('Request: ' + request)
+		return self.process(request, token, pcm)
+		

+ 155 - 0
service/sdk/baidu.py

@@ -0,0 +1,155 @@
+# encoding:utf-8
+from __future__ import division
+from ..__load__ import *
+import requests
+import datetime
+import uuid
+from dateutil import parser as dparser
+
+class Baidu(object):
+	def get_token(self, api_key, secret_key):
+        # 临时目录存放token
+		cache = open(os.path.join(File.path() + 'runtime/', 'baiduToken.ini'), 'a+')
+		try:
+			pms = cache.readlines()
+			if len(pms) > 0:
+				time = pms[0]
+				tk = pms[1]
+				# 计算token是否过期 官方说明一个月,这里保守29天
+				time = dparser.parse(time)
+				endtime = datetime.datetime.now()
+				if (endtime - time).days <= 29:
+					return tk
+		finally:
+			cache.close()
+		URL = 'http://openapi.baidu.com/oauth/2.0/token'
+		params = {'grant_type': 'client_credentials',
+				  'client_id': api_key,
+				  'client_secret': secret_key}
+		r = requests.get(URL, params=params)
+		try:
+			r.raise_for_status()
+			token = r.json()['access_token']
+			return token
+		except requests.exceptions.HTTPError:
+			return ''
+
+	def getUnit(self, query, service_id, api_key, secret_key):
+		""" 
+		NLU 解析
+
+		:param query: 用户的指令字符串
+		:param service_id: UNIT 的 service_id
+		:param api_key: UNIT apk_key
+		:param secret_key: UNIT secret_key
+		:returns: UNIT 解析结果。如果解析失败,返回 None
+		"""
+		access_token = self.get_token(api_key, secret_key)
+		url = 'https://aip.baidubce.com/rpc/2.0/unit/service/chat?access_token=' + access_token
+		request={
+			"query":query,
+			"user_id":"888888",
+		}
+		body={
+			"log_id": str(uuid.uuid1()),
+			"version":"2.0",
+			"service_id": service_id,
+			"session_id": str(uuid.uuid1()),
+			"request":request
+		}
+		try:
+			headers = {'Content-Type': 'application/json'}
+			request = requests.post(url, json=body, headers=headers)
+			return json.loads(request.text)
+		except Exception:
+			return None
+
+
+	def getIntent(self, parsed):
+		""" 
+		提取意图
+
+		:param parsed: UNIT 解析结果
+		:returns: 意图数组
+		"""
+		if parsed is not None and 'result' in parsed and \
+		   'response_list' in parsed['result']:
+			return parsed['result']['response_list'][0]['schema']['intent']
+		else:
+			return ''
+
+
+	def hasIntent(self, parsed, intent):
+		""" 
+		判断是否包含某个意图
+
+		:param parsed: UNIT 解析结果
+		:param intent: 意图的名称
+		:returns: True: 包含; False: 不包含
+		"""
+		if parsed is not None and 'result' in parsed and \
+		   'response_list' in parsed['result']:
+			response_list = parsed['result']['response_list']
+			for response in response_list:
+				if response['schema']['intent'] == intent:
+					return True
+			return False
+		else:
+			return False
+
+
+	def getSlots(self, parsed, intent=''):
+		""" 
+		提取某个意图的所有词槽
+		
+		:param parsed: UNIT 解析结果
+		:param intent: 意图的名称
+		:returns: 词槽列表。你可以通过 name 属性筛选词槽,
+	再通过 normalized_word 属性取出相应的值
+		"""
+		if parsed is not None and 'result' in parsed and \
+		   'response_list' in parsed['result']:
+			response_list = parsed['result']['response_list']
+			if intent == '':
+				return parsed['result']['response_list'][0]['schema']['slots']
+			for response in response_list:
+				if response['schema']['intent'] == intent:
+					return response['schema']['slots']
+		else:
+			return []
+
+	def getSlotWords(self, parsed, intent, name):
+		""" 
+		找出命中某个词槽的内容
+		
+		:param parsed: UNIT 解析结果
+		:param intent: 意图的名称
+		:param name: 词槽名
+		:returns: 命中该词槽的值的列表。
+		"""
+		slots = getSlots(parsed, intent)
+		words = []
+		for slot in slots:
+			if slot['name'] == name:
+				words.append(slot['normalized_word'])
+		return words
+
+	def getSay(self, parsed, intent=''):
+		"""
+		提取 UNIT 的回复文本
+
+		:param parsed: UNIT 解析结果
+		:param intent: 意图的名称
+		:returns: UNIT 的回复文本
+		"""
+		if parsed is not None and 'result' in parsed and \
+		   'response_list' in parsed['result']:
+			response_list = parsed['result']['response_list']
+			if intent == '':
+				return response_list[0]['action_list'][0]['say']
+			for response in response_list:
+				if response['schema']['intent'] == intent:
+					return response['action_list'][0]['say']
+			return ''
+		else:
+			return ''

+ 0 - 161
service/sdk/unit.py

@@ -1,161 +0,0 @@
-# encoding:utf-8
-import requests
-import datetime
-import uuid
-import json
-import os
-from dateutil import parser as dparser
-from robot import constants, logging
-
-logger = logging.getLogger(__name__)
-
-def get_token(api_key, secret_key):
-    cache = open(os.path.join(constants.TEMP_PATH, 'baidustt.ini'), 'a+')
-    try:
-        pms = cache.readlines()
-        if len(pms) > 0:
-            time = pms[0]
-            tk = pms[1]
-            # 计算token是否过期 官方说明一个月,这里保守29天
-            time = dparser.parse(time)
-            endtime = datetime.datetime.now()
-            if (endtime - time).days <= 29:
-                return tk
-    finally:
-        cache.close()
-    URL = 'http://openapi.baidu.com/oauth/2.0/token'
-    params = {'grant_type': 'client_credentials',
-              'client_id': api_key,
-              'client_secret': secret_key}
-    r = requests.get(URL, params=params)
-    try:
-        r.raise_for_status()
-        token = r.json()['access_token']
-        return token
-    except requests.exceptions.HTTPError:
-        return ''
-
-def getUnit(query, service_id, api_key, secret_key):
-    """ 
-    NLU 解析
-
-    :param query: 用户的指令字符串
-    :param service_id: UNIT 的 service_id
-    :param api_key: UNIT apk_key
-    :param secret_key: UNIT secret_key
-    :returns: UNIT 解析结果。如果解析失败,返回 None
-    """
-    access_token = get_token(api_key, secret_key)
-    url = 'https://aip.baidubce.com/rpc/2.0/unit/service/chat?access_token=' + access_token
-    request={
-        "query":query,
-        "user_id":"888888",
-    }
-    body={
-        "log_id": str(uuid.uuid1()),
-        "version":"2.0",
-        "service_id": service_id,
-        "session_id": str(uuid.uuid1()),
-        "request":request
-    }
-    try:
-        headers = {'Content-Type': 'application/json'}
-        request = requests.post(url, json=body, headers=headers)
-        return json.loads(request.text)
-    except Exception:
-        return None
-
-
-def getIntent(parsed):
-    """ 
-    提取意图
-
-    :param parsed: UNIT 解析结果
-    :returns: 意图数组
-    """
-    if parsed is not None and 'result' in parsed and \
-       'response_list' in parsed['result']:
-        return parsed['result']['response_list'][0]['schema']['intent']
-    else:
-        return ''
-
-
-def hasIntent(parsed, intent):
-    """ 
-    判断是否包含某个意图
-
-    :param parsed: UNIT 解析结果
-    :param intent: 意图的名称
-    :returns: True: 包含; False: 不包含
-    """
-    if parsed is not None and 'result' in parsed and \
-       'response_list' in parsed['result']:
-        response_list = parsed['result']['response_list']
-        for response in response_list:
-            if response['schema']['intent'] == intent:
-                return True
-        return False
-    else:
-        return False
-
-
-def getSlots(parsed, intent=''):
-    """ 
-    提取某个意图的所有词槽
-    
-    :param parsed: UNIT 解析结果
-    :param intent: 意图的名称
-    :returns: 词槽列表。你可以通过 name 属性筛选词槽,
-再通过 normalized_word 属性取出相应的值
-    """
-    if parsed is not None and 'result' in parsed and \
-       'response_list' in parsed['result']:
-        response_list = parsed['result']['response_list']
-        if intent == '':
-            return parsed['result']['response_list'][0]['schema']['slots']
-        for response in response_list:
-            if response['schema']['intent'] == intent:
-                return response['schema']['slots']
-    else:
-        return []
-
-def getSlotWords(parsed, intent, name):
-    """ 
-    找出命中某个词槽的内容
-    
-    :param parsed: UNIT 解析结果
-    :param intent: 意图的名称
-    :param name: 词槽名
-    :returns: 命中该词槽的值的列表。
-    """
-    slots = getSlots(parsed, intent)
-    words = []
-    for slot in slots:
-        if slot['name'] == name:
-            words.append(slot['normalized_word'])
-    return words
-
-def getSay(parsed, intent=''):
-    """
-    提取 UNIT 的回复文本
-
-    :param parsed: UNIT 解析结果
-    :param intent: 意图的名称
-    :returns: UNIT 的回复文本
-    """
-    if parsed is not None and 'result' in parsed and \
-       'response_list' in parsed['result']:
-        response_list = parsed['result']['response_list']
-        if intent == '':
-            return response_list[0]['action_list'][0]['say']
-        for response in response_list:
-            if response['schema']['intent'] == intent:
-                return response['action_list'][0]['say']
-        return ''
-    else:
-        return ''
-
-
-if __name__ == '__main__':
-    parsed = getUnit('今天的天气', "S13442", 'w5v7gUV3iPGsGntcM84PtOOM', 'KffXwW6E1alcGplcabcNs63Li6GvvnfL')
-    print(parsed)

+ 91 - 0
service/snowboy.py

@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+from __future__ import division
+from .__load__ import *
+from snowboy import snowboydecoder
+class Snowboy(object):
+
+	def init(self, robot):
+		self.bother = False
+		self.robot = robot
+		self.detector = None
+		self.interrupted = False
+		self.data = File.runtime('data')
+
+		if self.detector is not None:
+			self.detector.terminate()
+		# snowboy配置,后续改成从数据库读取
+		snowboyConfig = Demeter.config['snowboy']
+		if snowboyConfig['hotword_switch'] == True:
+			models = [
+				self.data + snowboyConfig['hotword'],
+				self.data + snowboyConfig['on_hotword'],
+				self.data + snowboyConfig['off_hotword']
+			]
+		else:
+			models = self.data + snowboyConfig['hotword']
+		self.detector = snowboydecoder.HotwordDetector(models, sensitivity=snowboyConfig['sensitivity'])
+		# main loop
+		try:
+			if snowboyConfig['hotword_switch'] == True:
+				callbacks = [self.detectedCallback,
+							 self.onHotwordCallback,
+							 self.offHotwordCallback]
+			else:
+				callbacks = self.detectedCallback
+			self.detector.start(detected_callback=callbacks,
+								audio_recorder_callback=self.robot.converse,
+								interrupt_check=self.interruptCallback,
+								silent_count_threshold=int(snowboyConfig['silent_threshold']),
+								recording_timeout=int(snowboyConfig['recording_timeout']) * 4,
+								sleep_time=0.03)
+			self.detector.terminate()
+		except Exception as e:
+			Demeter.logger.critical('离线唤醒机制初始化失败:{}'.format(e))
+
+	def stop(self):
+		self.interrupted = True
+
+	def detectedCallback(self):
+		if not self.isProperTime():
+			Demeter.logger.warning('勿扰模式开启中')
+			return
+		if self.robot.isRecording:
+			Demeter.logger.warning('正在录音中,跳过')
+			return
+		Player.play(self.data + 'beep_hi.wav')
+		Demeter.logger.info('开始录音')
+		self.robot.interrupt()
+		self.robot.isRecording = True;
+
+	def onHotwordCallback(self):
+		if snowboyConfig['hotword_switch'] == True:
+			self.bother = True
+			Player.play(self.data + 'off.wav')
+			Demeter.logger.info('勿扰模式打开')
+
+	def offHotwordCallback(self):
+		if snowboyConfig['hotword_switch'] == True:
+			self.bother = False
+			Player.play(self.data + 'on.wav')
+			Demeter.logger.info('勿扰模式关闭')
+
+	def interruptCallback(self):
+		return self.interrupted
+
+	def isProperTime():
+		if self.bother == True:
+			return False
+		if 'bother_enable' not in Demeter.config['snowboy']:
+			return True
+		if Demeter.config['snowboy']['bother_enable'] == False:
+			return True
+		if 'bother_since' not in Demeter.config['snowboy'] or 'bother_till' not in Demeter.config['snowboy']:
+			return True
+		since = Demeter.config['snowboy']['bother_since']
+		till = Demeter.config['snowboy']['bother_till']
+		current = time.localtime(time.time()).tm_hour
+		if till > since:
+			return current not in range(since, till)
+		else:
+			return not (current in range(since, 25) or
+						current in range(-1, till))

+ 0 - 1
service/tts/__init__.py

@@ -1 +0,0 @@
-# -*- coding: utf-8 -*-

+ 0 - 14
service/tts/load.py

@@ -1,14 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import division
-from .__load__ import *
-
-class Robot(object):
-
-	def __init__(self, profiling=False):
-        self.start()
-
-	def say(self, content):
-		print content
-
-	def start(self):
-		self.asr = Demeter.service('arc.load')

+ 20 - 9
vecan.py

@@ -3,18 +3,15 @@ from demeter.core import *
 
 class Vecan(object):
 
-	@classmethod
 	def init(self):
 		print('''
 		********************************************************
-		*          vecan-robot - 神算终端机器人                *
-		*          (c) 2019 rabin <rabin@dever.cc>             *
-		*     https://github.com/shemic/vecan.git              *
+		*		  vecan-robot - 神算终端机器人				*
+		*		  (c) 2019 rabin <rabin@dever.cc>			 *
+		*	 https://github.com/shemic/vecan.git			  *
 		********************************************************
-		            如需退出,可以按 Ctrl-4 组合键。
+					如需退出,可以按 Ctrl-4 组合键。
 		''')
-
-
 		Log.init(__name__)
 		Demeter.logger.info('测试测试')
 
@@ -23,14 +20,28 @@ class Vecan(object):
 
 		WatchDog.init()
 
-	@classmethod
 	def stop(self, signal, frame):
+		self.clean()
+		self.snowboy.stop()
 		Demeter.dog.stop()
 
-	@classmethod
 	def run(self):
 		self.init()
 		signal.signal(signal.SIGINT, self.stop)
+		#Demeter.webInit('front')
+		try:
+			self.snowboy = Demeter.service('snowboy')
+			self.snowboy.init(self.robot)
+		except AttributeError:
+			Demeter.logger.error('初始化离线唤醒功能失败')
+			pass
+
+	def clean():
+		temp = File.runtime('temp')
+		temp_files = os.listdir(temp)
+		for f in temp_files:
+			if os.path.isfile(os.path.join(temp, f)) and re.match(r'output[\d]*\.wav', os.path.basename(f)):
+				os.remove(os.path.join(temp, f))
 
 if __name__ == '__main__':
 	vecan = Vecan()