├── .gitignore ├── LICENSE ├── README.md ├── app ├── __init__.py ├── app_config.py ├── auth.py ├── enter_trigger_main.py ├── framework │ ├── __init__.py │ ├── mic.py │ └── player.py ├── resources │ └── du.mp3 ├── snowboy │ ├── __init__.py │ ├── _snowboydetect.so │ ├── demo.py │ ├── demo2.py │ ├── demo3.py │ ├── demo_arecord.py │ ├── demo_threaded.py │ ├── requirements.txt │ ├── resources │ │ ├── alexa.umdl │ │ ├── alexa │ │ │ ├── SnowboyAlexaDemo.apk │ │ │ ├── alexa-avs-sample-app │ │ │ │ └── alexa.umdl │ │ │ └── alexa_02092017.umdl │ │ ├── common.res │ │ ├── ding.wav │ │ ├── dong.wav │ │ ├── snowboy.umdl │ │ └── snowboy.wav │ ├── snowboydecoder.py │ ├── snowboydecoder_arecord.py │ ├── snowboydetect.py │ ├── snowboythreaded.py │ └── xiaoduxiaodu_all_10022017.umdl ├── utils │ ├── __init__.py │ ├── mic_data_saver.py │ └── prompt_tone.py └── wakeup_trigger_main.py ├── auth.sh ├── enter_trigger_start.sh ├── readme_resources └── 代码结构.png ├── sdk ├── __init__.py ├── auth.py ├── configurate.py ├── dueros_core.py ├── interface │ ├── __init__.py │ ├── alerts.py │ ├── audio_player.py │ ├── notifications.py │ ├── playback_controller.py │ ├── screen_display.py │ ├── speaker.py │ ├── speech_recognizer.py │ ├── speech_synthesizer.py │ └── system.py ├── resources │ ├── README.md │ └── alarm.wav └── sdk_config.py └── wakeup_trigger_start.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # pyenv python configuration file 61 | .python-version 62 | .idea/ 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | GNU GENERAL PUBLIC LICENSE 3 | Version 3, 29 June 2007 4 | 5 | Python implementation of Alexa Voice Service 6 | Copyright (C) 2017 Yihui Xiong 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | Also add information on how to contact you by electronic and paper mail. 22 | 23 | You should also get your employer (if you work as a programmer) or school, 24 | if any, to sign a "copyright disclaimer" for the program, if necessary. 25 | For more information on this, and how to apply and follow the GNU GPL, see 26 | . 27 | 28 | The GNU General Public License does not permit incorporating your program 29 | into proprietary programs. If your program is a subroutine library, you 30 | may consider it more useful to permit linking proprietary applications with 31 | the library. If this is what you want to do, use the GNU Lesser General 32 | Public License instead of this License. But first, please read 33 | . 34 | 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DuerOS-Python-Client使用说明 2 | 文档参考 3 | * [《Step by Step带你玩转DuerOS - Python DuerOS SDK[Ubuntu平台] (2)》](http://open.duer.baidu.com/forum/topic/show?topicId=244631) 4 | * [《Step by Step带你玩转DuerOS - 内容目录》](https://dueros.baidu.com/didp/forum/topic/show?topicId=244800) 5 | ## 运行依赖 6 | * gstreamer1.0 7 | * gstreamer1.0-plugins-good 8 | * gstreamer1.0-plugins-ugly 9 | * python-gi 10 | * python-gst 11 | * gir1.2-gstreamer-1.0 12 | ## 测试环境 13 | * Ubuntu 16.04 14 | * Python 2.7.12 15 | ## 使用说明 16 | ### 项目获取 17 | 通过git下载代码到本地 18 | 19 | # git clone https://github.com/MyDuerOS/DuerOS-Python-Client.git 20 | 21 | ### 认证授权 22 | 在DuerOS-Python-Client目录下执行 23 | 24 | # ./auth.sh 25 | 26 | ### 通过[Enter]键触发唤醒状态 27 | 在DuerOS-Python-Client目录下执行 28 | 29 | # ./enter_trigger_start.sh 30 | 31 | 然后,每次单击[Enter]键后进行语音输入 32 | ### 通过[小度小度]触发唤醒状态 33 | 在DuerOS-Python-Client目录下执行 34 | 35 | # ./wakeup_trigger_start.sh 36 | 然后,每次通过[小度小度]进行唤醒,然后,进行语音输入 37 | 38 | 39 | ## 代码结构 40 | DuerOS-Python-Client代码结构如下图所示, 41 | 42 | ![图片](./readme_resources/代码结构.png) 43 | 44 | 45 | 其中, 46 | 47 | *DuerOS-Python-Client:项目根目录* 48 | 49 | * DuerOS-Python-Client/auth.sh:认证授权脚本 50 | * DuerOS-Python-Client/enter_trigger_start.sh:[Enter]按键触发唤醒脚本 51 | * DuerOS-Python-Client/wakeup_tirgger_start.sh:[小度小度]触发唤醒脚本 52 | 53 | *DuerOS-Python-Client/app:应用目录* 54 | 55 | * DuerOS-Python-Client/app/auth.py:认证授权实现模块 56 | * DuerOS-Python-Client/app/enter_trigger_main.py:[Enter]按键触发唤醒实现模块 57 | * DuerOS-Python-Client/app/wakeup_tirgger_main.py:[小度小度]触发唤醒实现模块 58 | * DuerOS-Python-Client/app/framework:平台相关目录 59 | * DuerOS-Python-Client/app/framework/mic.py:录音模块(基于pyaudio) 60 | * DuerOS-Python-Client/app/framework/player.py:播放模块(基于GStreamer) 61 | * DuerOS-Python-Client/app/snowboy:snowboy唤醒引擎 62 | 63 | *DuerOS-Python-Client/sdk:dueros sdk目录* 64 | 65 | * DuerOS-Python-Client/sdk/auth.py:授权相关实现 66 | * DuerOS-Python-Client/sdk/dueros_core.py:dueros交互实现 67 | * DuerOS-Python-Client/sdk/interface:端能力接口实现 68 | 69 | ## SDK接口说明 70 | ### 授权模块(sdk/auth) 71 | #### 授权接口 72 | 用户通过授权接口完成基于OAuth2.0的认证授权流程 73 | 74 | def auth_request(client_id=CLIENT_ID, client_secret=CLIENT_SECRET): 75 | ''' 76 | 发起认证 77 | :param client_id:开发者注册信息 78 | :param client_secret: 开发者注册信息 79 | :return: 80 | ''' 81 | 82 | ### DuerOS核心模块(sdk/dueros_core) 83 | #### 启动DuerOS核心模块 84 | DuerOS核心处理模块启动 85 | 86 | def start(self): 87 | ''' 88 | DuerOS模块启动 89 | :return: 90 | ''' 91 | #### 停止DuerOS核心模块 92 | DuerOS核心处理模块停止 93 | 94 | def stop(self): 95 | ''' 96 | DuerOS模块停止 97 | :return: 98 | ''' 99 | #### 触发语音识别状态 100 | DuerOS核心处理模块进入语音识别状态(唤醒后触发) 101 | 102 | def listen(self): 103 | ''' 104 | DuerOS进入语音识别状态 105 | :return: 106 | ''' 107 | #### directive监听注册 108 | 通过监听注册接口,用户可以获得云端下发的directive内容 109 | 110 | def set_directive_listener(self, listener): 111 | ''' 112 | directive监听器设置 113 | :param listener: directive监听器 114 | :return: 115 | ''' 116 | 117 | ## App接口说明 118 | ### 录音模块(app/framework/mic) 119 | #### 开始录音 120 | 录音开始控制 121 | 122 | def start(self): 123 | ''' 124 | 开始录音 125 | :return: 126 | ''' 127 | #### 结束录音 128 | 录音结束控制 129 | 130 | def stop(self): 131 | ''' 132 | 结束录音 133 | :return: 134 | ''' 135 | #### 录音接收实体绑定 136 | 将录音组件同,duersdk进行绑定 137 | 138 | def link(self, sink): 139 | ''' 140 | 绑定录音接收实体 141 | :param sink: 录音接收实体 142 | :return: 143 | ''' 144 | #### 录音实体解除绑定 145 | 解除录音组件同duersdk间的绑定 146 | 147 | def unlink(self, sink): 148 | ''' 149 | 录音实体解除绑定 150 | :param sink: 录音接收实体 151 | :return: 152 | ''' 153 | ### 播放模块(app/framework/player) 154 | #### 开始播放 155 | 开始播放控制 156 | 157 | def play(self, uri): 158 | ''' 159 | 播放 160 | :param uri:播放资源地址 161 | :return: 162 | ''' 163 | #### 停止播放 164 | 停止播放控制 165 | 166 | def stop(self): 167 | ''' 168 | 停止 169 | :return: 170 | ''' 171 | #### 暂停播放 172 | 暂停播放控制 173 | 174 | def pause(self): 175 | ''' 176 | 暂停 177 | :return: 178 | ''' 179 | #### 恢复播放 180 | 恢复播放控制 181 | 182 | def resume(self): 183 | ''' 184 | 回复播放 185 | :return: 186 | ''' 187 | #### 播放状态监听注册 188 | 注册播放状态的监听器 189 | 190 | def add_callback(self, name, callback): 191 | ''' 192 | 播放状态回调 193 | :param name: {eos, ...} 194 | :param callback: 回调函数 195 | :return: 196 | ''' 197 | ##### 播放时长 198 | 当前播放音频的播放时长(模块属性) 199 | 200 | @property 201 | def duration(self): 202 | ''' 203 | 播放时长 204 | :return: 205 | ''' 206 | ##### 播放位置 207 | 当前播放音频的播放位置(模块属性) 208 | 209 | @property 210 | def position(self): 211 | ''' 212 | 播放位置 213 | :return: 214 | ''' 215 | #### 播放状态 216 | 当前播放音频的播放状态(模块属性) 217 | 218 | @property 219 | def state(self): 220 | ''' 221 | 播放状态 222 | :return: 223 | ''' 224 | ### 工具模块(app/utils/prompt_tone) 225 | #### 提示音播放 226 | 短暂提示音("du")播放 227 | 228 | def play(self): 229 | ''' 230 | 提示音播放 231 | :return: 232 | ''' 233 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/__init__.py -------------------------------------------------------------------------------- /app/app_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 应用配置 4 | ''' 5 | import logging 6 | 7 | # 应用打印log等级 8 | LOGGER_LEVEL = logging.INFO 9 | -------------------------------------------------------------------------------- /app/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | OAuth2.0认证 4 | ''' 5 | import sdk.auth as auth 6 | 7 | # 开发者注册信息 8 | CLIENT_ID = 'XXXXX' 9 | CLIENT_SECRET = 'XXXXXX' 10 | 11 | 12 | 13 | 14 | def main(): 15 | # 使用开发者注册信息 16 | #auth.auth_request(CLIENT_ID, CLIENT_SECRET) 17 | 18 | # 使用默认的CLIENT_ID和CLIENT_SECRET 19 | auth.auth_request() 20 | 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /app/enter_trigger_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 通过输入[Enter]触发唤醒状态 4 | ''' 5 | import logging 6 | from sdk.dueros_core import DuerOS 7 | 8 | from app.framework.mic import Audio 9 | from app.framework.player import Player 10 | from app.utils.prompt_tone import PromptTone 11 | 12 | logging.basicConfig(level=logging.INFO) 13 | 14 | 15 | def directive_listener(directive_content): 16 | ''' 17 | 云端下发directive监听器 18 | :param directive_content:云端下发directive内容 19 | :return: 20 | ''' 21 | content = u'云端下发directive:%s' % (directive_content) 22 | logging.info(content) 23 | 24 | 25 | def main(): 26 | # 创建录音设备(平台相关) 27 | audio = Audio() 28 | # 创建播放器(平台相关) 29 | player = Player() 30 | 31 | dueros = DuerOS(player) 32 | dueros.set_directive_listener(directive_listener) 33 | 34 | audio.link(dueros) 35 | 36 | dueros.start() 37 | audio.start() 38 | 39 | prompt_tone_player = PromptTone() 40 | 41 | while True: 42 | try: 43 | try: 44 | print '\n' 45 | input('单击[Enter]建,然后发起对话\n') 46 | except SyntaxError: 47 | pass 48 | # 唤醒态提示音 49 | prompt_tone_player.play() 50 | dueros.listen() 51 | except KeyboardInterrupt: 52 | break 53 | 54 | dueros.stop() 55 | audio.stop() 56 | 57 | 58 | if __name__ == '__main__': 59 | main() 60 | -------------------------------------------------------------------------------- /app/framework/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/framework/__init__.py -------------------------------------------------------------------------------- /app/framework/mic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pyaudio 3 | import logging 4 | 5 | import app.app_config as app_config 6 | 7 | logging.basicConfig(level=app_config.LOGGER_LEVEL) 8 | logger = logging.getLogger(__file__) 9 | 10 | 11 | class Audio(object): 12 | ''' 13 | 录音类(基于pyaudio) 14 | ''' 15 | 16 | def __init__(self, rate=16000, frames_size=None, channels=None, device_index=None): 17 | ''' 18 | 录音类初始化 19 | :param rate:采样率 20 | :param frames_size:数据帧大小 21 | :param channels:通道数 22 | :param device_index:录音设备id 23 | ''' 24 | self.sample_rate = rate 25 | self.frames_size = frames_size if frames_size else rate / 100 26 | self.channels = channels if channels else 1 27 | 28 | self.pyaudio_instance = pyaudio.PyAudio() 29 | 30 | if device_index is None: 31 | if channels: 32 | for i in range(self.pyaudio_instance.get_device_count()): 33 | dev = self.pyaudio_instance.get_device_info_by_index(i) 34 | name = dev['name'].encode('utf-8') 35 | logger.info('{}:{} with {} input channels'.format(i, name, dev['maxInputChannels'])) 36 | if dev['maxInputChannels'] == channels: 37 | logger.info('Use {}'.format(name)) 38 | device_index = i 39 | break 40 | else: 41 | device_index = self.pyaudio_instance.get_default_input_device_info()['index'] 42 | 43 | if device_index is None: 44 | raise Exception('Can not find an input device with {} channel(s)'.format(channels)) 45 | 46 | self.stream = self.pyaudio_instance.open( 47 | start=False, 48 | format=pyaudio.paInt16, 49 | input_device_index=device_index, 50 | channels=self.channels, 51 | rate=int(self.sample_rate), 52 | frames_per_buffer=int(self.frames_size), 53 | stream_callback=self.__callback, 54 | input=True 55 | ) 56 | 57 | self.sinks = [] 58 | 59 | def start(self): 60 | ''' 61 | 开始录音 62 | :return: 63 | ''' 64 | self.stream.start_stream() 65 | 66 | def stop(self): 67 | ''' 68 | 结束录音 69 | :return: 70 | ''' 71 | self.stream.stop_stream() 72 | 73 | def link(self, sink): 74 | ''' 75 | 绑定录音接收实体 76 | :param sink: 录音接收实体 77 | :return: 78 | ''' 79 | if hasattr(sink, 'put') and callable(sink.put): 80 | self.sinks.append(sink) 81 | else: 82 | raise ValueError('Not implement put() method') 83 | 84 | def unlink(self, sink): 85 | ''' 86 | 录音实体解除绑定 87 | :param sink: 录音接收实体 88 | :return: 89 | ''' 90 | self.sinks.remove(sink) 91 | 92 | def __callback(self, in_data, frame_count, time_info, status): 93 | ''' 94 | 录音数据(pmc)回调 95 | :param in_data:录音数据 96 | :param frame_count: 97 | :param time_info: 98 | :param status: 99 | :return: 100 | ''' 101 | for sink in self.sinks: 102 | sink.put(in_data) 103 | return None, pyaudio.paContinue 104 | -------------------------------------------------------------------------------- /app/framework/player.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 基于GStreamer的播放模块 5 | """ 6 | 7 | import gi 8 | 9 | gi.require_version('Gst', '1.0') 10 | from gi.repository import Gst 11 | 12 | Gst.init(None) 13 | 14 | 15 | class Player(object): 16 | ''' 17 | 播放器实现类 18 | ''' 19 | 20 | def __init__(self): 21 | self.player = Gst.ElementFactory.make("playbin", "player") 22 | 23 | self.bus = self.player.get_bus() 24 | self.bus.add_signal_watch() 25 | self.bus.enable_sync_message_emission() 26 | # self.bus.connect('sync-message::eos', self.on_eos) 27 | 28 | def play(self, uri): 29 | ''' 30 | 播放 31 | :param uri:播放资源地址 32 | :return: 33 | ''' 34 | self.player.set_state(Gst.State.NULL) 35 | self.player.set_property('uri', uri) 36 | self.player.set_state(Gst.State.PLAYING) 37 | 38 | def stop(self): 39 | ''' 40 | 停止 41 | :return: 42 | ''' 43 | self.player.set_state(Gst.State.NULL) 44 | 45 | def pause(self): 46 | ''' 47 | 暂停 48 | :return: 49 | ''' 50 | self.player.set_state(Gst.State.PAUSED) 51 | 52 | def resume(self): 53 | ''' 54 | 回复播放 55 | :return: 56 | ''' 57 | self.player.set_state(Gst.State.PLAYING) 58 | 59 | def add_callback(self, name, callback): 60 | ''' 61 | 播放状态回调 62 | :param name: {eos, ...} 63 | :param callback: 回调函数 64 | :return: 65 | ''' 66 | if not callable(callback): 67 | return 68 | 69 | def on_message(bus, message): 70 | callback() 71 | 72 | self.bus.connect('sync-message::{}'.format(name), on_message) 73 | 74 | @property 75 | def duration(self): 76 | ''' 77 | 播放时长 78 | :return: 79 | ''' 80 | success, duration = self.player.query_duration(Gst.Format.TIME) 81 | if success: 82 | return int(duration / Gst.MSECOND) 83 | 84 | @property 85 | def position(self): 86 | ''' 87 | 播放位置 88 | :return: 89 | ''' 90 | success, position = self.player.query_position(Gst.Format.TIME) 91 | if not success: 92 | position = 0 93 | 94 | return int(position / Gst.MSECOND) 95 | 96 | @property 97 | def state(self): 98 | ''' 99 | 播放状态 100 | :return: 101 | ''' 102 | # GST_STATE_VOID_PENDING no pending state. 103 | # GST_STATE_NULL the NULL state or initial state of an element. 104 | # GST_STATE_READY the element is ready to go to PAUSED. 105 | # GST_STATE_PAUSED the element is PAUSED, it is ready to accept and process data. 106 | # Sink elements however only accept one buffer and then block. 107 | # GST_STATE_PLAYING the element is PLAYING, the GstClock is running and the data is flowing. 108 | _, state, _ = self.player.get_state(Gst.SECOND) 109 | return 'FINISHED' if state != Gst.State.PLAYING else 'PLAYING' 110 | -------------------------------------------------------------------------------- /app/resources/du.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/resources/du.mp3 -------------------------------------------------------------------------------- /app/snowboy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/__init__.py -------------------------------------------------------------------------------- /app/snowboy/_snowboydetect.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/_snowboydetect.so -------------------------------------------------------------------------------- /app/snowboy/demo.py: -------------------------------------------------------------------------------- 1 | import snowboydecoder 2 | import sys 3 | import signal 4 | 5 | interrupted = False 6 | 7 | 8 | def signal_handler(signal, frame): 9 | global interrupted 10 | interrupted = True 11 | 12 | 13 | def interrupt_callback(): 14 | global interrupted 15 | return interrupted 16 | 17 | if len(sys.argv) == 1: 18 | print("Error: need to specify model name") 19 | print("Usage: python demo.py your.model") 20 | sys.exit(-1) 21 | 22 | model = sys.argv[1] 23 | 24 | # capture SIGINT signal, e.g., Ctrl+C 25 | signal.signal(signal.SIGINT, signal_handler) 26 | 27 | detector = snowboydecoder.HotwordDetector(model, sensitivity=0.5) 28 | print('Listening... Press Ctrl+C to exit') 29 | 30 | # main loop 31 | detector.start(detected_callback=snowboydecoder.play_audio_file, 32 | interrupt_check=interrupt_callback, 33 | sleep_time=0.03) 34 | 35 | detector.terminate() 36 | -------------------------------------------------------------------------------- /app/snowboy/demo2.py: -------------------------------------------------------------------------------- 1 | import snowboydecoder 2 | import sys 3 | import signal 4 | 5 | # Demo code for listening to two hotwords at the same time 6 | 7 | interrupted = False 8 | 9 | 10 | def signal_handler(signal, frame): 11 | global interrupted 12 | interrupted = True 13 | 14 | 15 | def interrupt_callback(): 16 | global interrupted 17 | return interrupted 18 | 19 | if len(sys.argv) != 3: 20 | print("Error: need to specify 2 model names") 21 | print("Usage: python demo.py 1st.model 2nd.model") 22 | sys.exit(-1) 23 | 24 | models = sys.argv[1:] 25 | 26 | # capture SIGINT signal, e.g., Ctrl+C 27 | signal.signal(signal.SIGINT, signal_handler) 28 | 29 | sensitivity = [0.5]*len(models) 30 | detector = snowboydecoder.HotwordDetector(models, sensitivity=sensitivity) 31 | callbacks = [lambda: snowboydecoder.play_audio_file(snowboydecoder.DETECT_DING), 32 | lambda: snowboydecoder.play_audio_file(snowboydecoder.DETECT_DONG)] 33 | print('Listening... Press Ctrl+C to exit') 34 | 35 | # main loop 36 | # make sure you have the same numbers of callbacks and models 37 | detector.start(detected_callback=callbacks, 38 | interrupt_check=interrupt_callback, 39 | sleep_time=0.03) 40 | 41 | detector.terminate() 42 | -------------------------------------------------------------------------------- /app/snowboy/demo3.py: -------------------------------------------------------------------------------- 1 | import snowboydecoder 2 | import sys 3 | import wave 4 | 5 | # Demo code for detecting hotword in a .wav file 6 | # Example Usage: 7 | # $ python demo3.py resources/snowboy.wav resources/snowboy.umdl 8 | # Should print: 9 | # Hotword Detected! 10 | # 11 | # $ python demo3.py resources/ding.wav resources/snowboy.umdl 12 | # Should print: 13 | # Hotword Not Detected! 14 | 15 | 16 | if len(sys.argv) != 3: 17 | print("Error: need to specify wave file name and model name") 18 | print("Usage: python demo3.py wave_file model_file") 19 | sys.exit(-1) 20 | 21 | wave_file = sys.argv[1] 22 | model_file = sys.argv[2] 23 | 24 | f = wave.open(wave_file) 25 | assert f.getnchannels() == 1, "Error: Snowboy only supports 1 channel of audio (mono, not stereo)" 26 | assert f.getframerate() == 16000, "Error: Snowboy only supports 16K sampling rate" 27 | assert f.getsampwidth() == 2, "Error: Snowboy only supports 16bit per sample" 28 | data = f.readframes(f.getnframes()) 29 | f.close() 30 | 31 | sensitivity = 0.5 32 | detection = snowboydecoder.HotwordDetector(model_file, sensitivity=sensitivity) 33 | 34 | ans = detection.detector.RunDetection(data) 35 | 36 | if ans == 1: 37 | print('Hotword Detected!') 38 | else: 39 | print('Hotword Not Detected!') 40 | 41 | -------------------------------------------------------------------------------- /app/snowboy/demo_arecord.py: -------------------------------------------------------------------------------- 1 | import snowboydecoder_arecord 2 | import sys 3 | import signal 4 | 5 | interrupted = False 6 | 7 | 8 | def signal_handler(signal, frame): 9 | global interrupted 10 | interrupted = True 11 | 12 | 13 | def interrupt_callback(): 14 | global interrupted 15 | return interrupted 16 | 17 | if len(sys.argv) == 1: 18 | print("Error: need to specify model name") 19 | print("Usage: python demo.py your.model") 20 | sys.exit(-1) 21 | 22 | model = sys.argv[1] 23 | 24 | # capture SIGINT signal, e.g., Ctrl+C 25 | signal.signal(signal.SIGINT, signal_handler) 26 | 27 | detector = snowboydecoder_arecord.HotwordDetector(model, sensitivity=0.5) 28 | print('Listening... Press Ctrl+C to exit') 29 | 30 | # main loop 31 | detector.start(detected_callback=snowboydecoder_arecord.play_audio_file, 32 | interrupt_check=interrupt_callback, 33 | sleep_time=0.03) 34 | 35 | detector.terminate() 36 | -------------------------------------------------------------------------------- /app/snowboy/demo_threaded.py: -------------------------------------------------------------------------------- 1 | import snowboythreaded 2 | import sys 3 | import signal 4 | import time 5 | 6 | stop_program = False 7 | 8 | # This a demo that shows running Snowboy in another thread 9 | 10 | 11 | def signal_handler(signal, frame): 12 | global stop_program 13 | stop_program = True 14 | 15 | 16 | if len(sys.argv) == 1: 17 | print("Error: need to specify model name") 18 | print("Usage: python demo4.py your.model") 19 | sys.exit(-1) 20 | 21 | model = sys.argv[1] 22 | 23 | # capture SIGINT signal, e.g., Ctrl+C 24 | signal.signal(signal.SIGINT, signal_handler) 25 | 26 | # Initialize ThreadedDetector object and start the detection thread 27 | threaded_detector = snowboythreaded.ThreadedDetector(model, sensitivity=0.5) 28 | threaded_detector.start() 29 | 30 | print('Listening... Press Ctrl+C to exit') 31 | 32 | # main loop 33 | threaded_detector.start_recog(sleep_time=0.03) 34 | 35 | # Let audio initialization happen before requesting input 36 | time.sleep(1) 37 | 38 | # Do a simple task separate from the detection - addition of numbers 39 | while not stop_program: 40 | try: 41 | num1 = int(raw_input("Enter the first number to add: ")) 42 | num2 = int(raw_input("Enter the second number to add: ")) 43 | print "Sum of number: {}".format(num1 + num2) 44 | except ValueError: 45 | print "You did not enter a number." 46 | 47 | threaded_detector.terminate() 48 | -------------------------------------------------------------------------------- /app/snowboy/requirements.txt: -------------------------------------------------------------------------------- 1 | PyAudio==0.2.9 2 | -------------------------------------------------------------------------------- /app/snowboy/resources/alexa.umdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/resources/alexa.umdl -------------------------------------------------------------------------------- /app/snowboy/resources/alexa/SnowboyAlexaDemo.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/resources/alexa/SnowboyAlexaDemo.apk -------------------------------------------------------------------------------- /app/snowboy/resources/alexa/alexa-avs-sample-app/alexa.umdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/resources/alexa/alexa-avs-sample-app/alexa.umdl -------------------------------------------------------------------------------- /app/snowboy/resources/alexa/alexa_02092017.umdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/resources/alexa/alexa_02092017.umdl -------------------------------------------------------------------------------- /app/snowboy/resources/common.res: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/resources/common.res -------------------------------------------------------------------------------- /app/snowboy/resources/ding.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/resources/ding.wav -------------------------------------------------------------------------------- /app/snowboy/resources/dong.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/resources/dong.wav -------------------------------------------------------------------------------- /app/snowboy/resources/snowboy.umdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/resources/snowboy.umdl -------------------------------------------------------------------------------- /app/snowboy/resources/snowboy.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/resources/snowboy.wav -------------------------------------------------------------------------------- /app/snowboy/snowboydecoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import collections 4 | import pyaudio 5 | import snowboydetect 6 | import time 7 | import wave 8 | import os 9 | import logging 10 | 11 | import sdk.sdk_config as sdk_config 12 | 13 | logging.basicConfig() 14 | logger = logging.getLogger("snowboy") 15 | logger.setLevel(logging.INFO) 16 | TOP_DIR = os.path.dirname(os.path.abspath(__file__)) 17 | 18 | RESOURCE_FILE = os.path.join(TOP_DIR, "resources/common.res") 19 | DETECT_DING = os.path.join(TOP_DIR, "resources/ding.wav") 20 | DETECT_DONG = os.path.join(TOP_DIR, "resources/dong.wav") 21 | 22 | 23 | class RingBuffer(object): 24 | """Ring buffer to hold audio from PortAudio""" 25 | 26 | def __init__(self, size=4096): 27 | self._buf = collections.deque(maxlen=size) 28 | 29 | def extend(self, data): 30 | """Adds data to the end of buffer""" 31 | self._buf.extend(data) 32 | 33 | def get(self): 34 | """Retrieves data from the beginning of buffer and clears it""" 35 | tmp = bytes(bytearray(self._buf)) 36 | self._buf.clear() 37 | return tmp 38 | 39 | 40 | def play_audio_file(fname=DETECT_DING): 41 | """Simple callback function to play a wave file. By default it plays 42 | a Ding sound. 43 | 44 | :param str fname: wave file name 45 | :return: None 46 | """ 47 | ding_wav = wave.open(fname, 'rb') 48 | ding_data = ding_wav.readframes(ding_wav.getnframes()) 49 | audio = pyaudio.PyAudio() 50 | stream_out = audio.open( 51 | format=audio.get_format_from_width(ding_wav.getsampwidth()), 52 | channels=ding_wav.getnchannels(), 53 | rate=ding_wav.getframerate(), input=False, output=True) 54 | stream_out.start_stream() 55 | stream_out.write(ding_data) 56 | time.sleep(0.2) 57 | stream_out.stop_stream() 58 | stream_out.close() 59 | audio.terminate() 60 | 61 | 62 | class HotwordDetector(object): 63 | """ 64 | Snowboy decoder to detect whether a keyword specified by `decoder_model` 65 | exists in a microphone input stream. 66 | 67 | :param decoder_model: decoder model file path, a string or a list of strings 68 | :param resource: resource file path. 69 | :param sensitivity: decoder sensitivity, a float of a list of floats. 70 | The bigger the value, the more senstive the 71 | decoder. If an empty list is provided, then the 72 | default sensitivity in the model will be used. 73 | :param audio_gain: multiply input volume by this factor. 74 | """ 75 | 76 | def __init__(self, decoder_model, 77 | resource=RESOURCE_FILE, 78 | sensitivity=[], 79 | audio_gain=1): 80 | 81 | # def audio_callback(in_data, frame_count, time_info, status): 82 | # self.ring_buffer.extend(in_data) 83 | # play_data = chr(0) * len(in_data) 84 | # return play_data, pyaudio.paContinue 85 | 86 | tm = type(decoder_model) 87 | ts = type(sensitivity) 88 | if tm is not list: 89 | decoder_model = [decoder_model] 90 | if ts is not list: 91 | sensitivity = [sensitivity] 92 | model_str = ",".join(decoder_model) 93 | 94 | self.detector = snowboydetect.SnowboyDetect( 95 | resource_filename=resource.encode(), model_str=model_str.encode()) 96 | self.detector.SetAudioGain(audio_gain) 97 | self.num_hotwords = self.detector.NumHotwords() 98 | 99 | if len(decoder_model) > 1 and len(sensitivity) == 1: 100 | sensitivity = sensitivity * self.num_hotwords 101 | # if len(sensitivity) != 0: 102 | # assert self.num_hotwords == len(sensitivity), \ 103 | # "number of hotwords in decoder_model (%d) and sensitivity " \ 104 | # "(%d) does not match" % (self.num_hotwords, len(sensitivity)) 105 | # sensitivity_str = ",".join([str(t) for t in sensitivity]) 106 | if len(sensitivity) != 0: 107 | self.detector.SetSensitivity(sdk_config.SNOWBOAY_SENSITIVITY) 108 | 109 | self.ring_buffer = RingBuffer( 110 | self.detector.NumChannels() * self.detector.SampleRate() * 5) 111 | 112 | # self.audio = pyaudio.PyAudio() 113 | # self.stream_in = self.audio.open( 114 | # input=True, output=False, 115 | # format=self.audio.get_format_from_width( 116 | # self.detector.BitsPerSample() / 8), 117 | # channels=self.detector.NumChannels(), 118 | # rate=self.detector.SampleRate(), 119 | # frames_per_buffer=2048, 120 | # stream_callback=audio_callback) 121 | 122 | def feed_data(self, data): 123 | self.ring_buffer.extend(data) 124 | 125 | def start(self, detected_callback=play_audio_file, 126 | interrupt_check=lambda: False, 127 | sleep_time=0.03): 128 | """ 129 | Start the voice detector. For every `sleep_time` second it checks the 130 | audio buffer for triggering keywords. If detected, then call 131 | corresponding function in `detected_callback`, which can be a single 132 | function (single model) or a list of callback functions (multiple 133 | models). Every loop it also calls `interrupt_check` -- if it returns 134 | True, then breaks from the loop and return. 135 | 136 | :param detected_callback: a function or list of functions. The number of 137 | items must match the number of models in 138 | `decoder_model`. 139 | :param interrupt_check: a function that returns True if the main loop 140 | needs to stop. 141 | :param float sleep_time: how much time in second every loop waits. 142 | :return: None 143 | """ 144 | if interrupt_check(): 145 | logger.debug("detect voice return") 146 | return 147 | 148 | tc = type(detected_callback) 149 | if tc is not list: 150 | detected_callback = [detected_callback] 151 | if len(detected_callback) == 1 and self.num_hotwords > 1: 152 | detected_callback *= self.num_hotwords 153 | 154 | assert self.num_hotwords == len(detected_callback), \ 155 | "Error: hotwords in your models (%d) do not match the number of " \ 156 | "callbacks (%d)" % (self.num_hotwords, len(detected_callback)) 157 | 158 | logger.debug("detecting...") 159 | 160 | while True: 161 | if interrupt_check(): 162 | logger.debug("detect voice break") 163 | break 164 | data = self.ring_buffer.get() 165 | if len(data) == 0: 166 | time.sleep(sleep_time) 167 | continue 168 | 169 | ans = self.detector.RunDetection(data) 170 | if ans == -1: 171 | logger.warning("Error initializing streams or reading audio data") 172 | elif ans > 0: 173 | message = "Keyword " + str(ans) + " detected at time: " 174 | message += time.strftime("%Y-%m-%d %H:%M:%S", 175 | time.localtime(time.time())) 176 | logger.info(message) 177 | callback = detected_callback[ans - 1] 178 | if callback is not None: 179 | callback() 180 | 181 | logger.debug("finished.") 182 | 183 | def terminate(self): 184 | """ 185 | Terminate audio stream. Users cannot call start() again to detect. 186 | :return: None 187 | """ 188 | self.stream_in.stop_stream() 189 | self.stream_in.close() 190 | self.audio.terminate() 191 | -------------------------------------------------------------------------------- /app/snowboy/snowboydecoder_arecord.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import collections 4 | import snowboydetect 5 | import time 6 | import wave 7 | import os 8 | import logging 9 | import subprocess 10 | import threading 11 | 12 | logging.basicConfig() 13 | logger = logging.getLogger("snowboy") 14 | logger.setLevel(logging.INFO) 15 | TOP_DIR = os.path.dirname(os.path.abspath(__file__)) 16 | 17 | RESOURCE_FILE = os.path.join(TOP_DIR, "resources/common.res") 18 | DETECT_DING = os.path.join(TOP_DIR, "resources/ding.wav") 19 | DETECT_DONG = os.path.join(TOP_DIR, "resources/dong.wav") 20 | 21 | 22 | class RingBuffer(object): 23 | """Ring buffer to hold audio from audio capturing tool""" 24 | def __init__(self, size = 4096): 25 | self._buf = collections.deque(maxlen=size) 26 | 27 | def extend(self, data): 28 | """Adds data to the end of buffer""" 29 | self._buf.extend(data) 30 | 31 | def get(self): 32 | """Retrieves data from the beginning of buffer and clears it""" 33 | tmp = bytes(bytearray(self._buf)) 34 | self._buf.clear() 35 | return tmp 36 | 37 | 38 | def play_audio_file(fname=DETECT_DING): 39 | """Simple callback function to play a wave file. By default it plays 40 | a Ding sound. 41 | 42 | :param str fname: wave file name 43 | :return: None 44 | """ 45 | os.system("aplay " + fname + " > /dev/null 2>&1") 46 | 47 | 48 | class HotwordDetector(object): 49 | """ 50 | Snowboy decoder to detect whether a keyword specified by `decoder_model` 51 | exists in a microphone input stream. 52 | 53 | :param decoder_model: decoder model file path, a string or a list of strings 54 | :param resource: resource file path. 55 | :param sensitivity: decoder sensitivity, a float of a list of floats. 56 | The bigger the value, the more senstive the 57 | decoder. If an empty list is provided, then the 58 | default sensitivity in the model will be used. 59 | :param audio_gain: multiply input volume by this factor. 60 | """ 61 | def __init__(self, decoder_model, 62 | resource=RESOURCE_FILE, 63 | sensitivity=[], 64 | audio_gain=1): 65 | 66 | tm = type(decoder_model) 67 | ts = type(sensitivity) 68 | if tm is not list: 69 | decoder_model = [decoder_model] 70 | if ts is not list: 71 | sensitivity = [sensitivity] 72 | model_str = ",".join(decoder_model) 73 | 74 | self.detector = snowboydetect.SnowboyDetect( 75 | resource_filename=resource.encode(), model_str=model_str.encode()) 76 | self.detector.SetAudioGain(audio_gain) 77 | self.num_hotwords = self.detector.NumHotwords() 78 | 79 | if len(decoder_model) > 1 and len(sensitivity) == 1: 80 | sensitivity = sensitivity*self.num_hotwords 81 | if len(sensitivity) != 0: 82 | assert self.num_hotwords == len(sensitivity), \ 83 | "number of hotwords in decoder_model (%d) and sensitivity " \ 84 | "(%d) does not match" % (self.num_hotwords, len(sensitivity)) 85 | sensitivity_str = ",".join([str(t) for t in sensitivity]) 86 | if len(sensitivity) != 0: 87 | self.detector.SetSensitivity(sensitivity_str.encode()) 88 | 89 | self.ring_buffer = RingBuffer( 90 | self.detector.NumChannels() * self.detector.SampleRate() * 5) 91 | 92 | def record_proc(self): 93 | CHUNK = 2048 94 | RECORD_RATE = 16000 95 | cmd = 'arecord -q -r %d -f S16_LE' % RECORD_RATE 96 | process = subprocess.Popen(cmd.split(' '), 97 | stdout = subprocess.PIPE, 98 | stderr = subprocess.PIPE) 99 | wav = wave.open(process.stdout, 'rb') 100 | while self.recording: 101 | data = wav.readframes(CHUNK) 102 | self.ring_buffer.extend(data) 103 | process.terminate() 104 | 105 | def init_recording(self): 106 | """ 107 | Start a thread for spawning arecord process and reading its stdout 108 | """ 109 | self.recording = True 110 | self.record_thread = threading.Thread(target = self.record_proc) 111 | self.record_thread.start() 112 | 113 | def start(self, detected_callback=play_audio_file, 114 | interrupt_check=lambda: False, 115 | sleep_time=0.03): 116 | """ 117 | Start the voice detector. For every `sleep_time` second it checks the 118 | audio buffer for triggering keywords. If detected, then call 119 | corresponding function in `detected_callback`, which can be a single 120 | function (single model) or a list of callback functions (multiple 121 | models). Every loop it also calls `interrupt_check` -- if it returns 122 | True, then breaks from the loop and return. 123 | 124 | :param detected_callback: a function or list of functions. The number of 125 | items must match the number of models in 126 | `decoder_model`. 127 | :param interrupt_check: a function that returns True if the main loop 128 | needs to stop. 129 | :param float sleep_time: how much time in second every loop waits. 130 | :return: None 131 | """ 132 | 133 | self.init_recording() 134 | 135 | if interrupt_check(): 136 | logger.debug("detect voice return") 137 | return 138 | 139 | tc = type(detected_callback) 140 | if tc is not list: 141 | detected_callback = [detected_callback] 142 | if len(detected_callback) == 1 and self.num_hotwords > 1: 143 | detected_callback *= self.num_hotwords 144 | 145 | assert self.num_hotwords == len(detected_callback), \ 146 | "Error: hotwords in your models (%d) do not match the number of " \ 147 | "callbacks (%d)" % (self.num_hotwords, len(detected_callback)) 148 | 149 | logger.debug("detecting...") 150 | 151 | while True: 152 | if interrupt_check(): 153 | logger.debug("detect voice break") 154 | break 155 | data = self.ring_buffer.get() 156 | if len(data) == 0: 157 | time.sleep(sleep_time) 158 | continue 159 | 160 | ans = self.detector.RunDetection(data) 161 | if ans == -1: 162 | logger.warning("Error initializing streams or reading audio data") 163 | elif ans > 0: 164 | message = "Keyword " + str(ans) + " detected at time: " 165 | message += time.strftime("%Y-%m-%d %H:%M:%S", 166 | time.localtime(time.time())) 167 | logger.info(message) 168 | callback = detected_callback[ans-1] 169 | if callback is not None: 170 | callback() 171 | 172 | logger.debug("finished.") 173 | 174 | def terminate(self): 175 | """ 176 | Terminate audio stream. Users cannot call start() again to detect. 177 | :return: None 178 | """ 179 | self.recording = False 180 | self.record_thread.join() 181 | 182 | -------------------------------------------------------------------------------- /app/snowboy/snowboydetect.py: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by SWIG (http://www.swig.org). 2 | # Version 3.0.12 3 | # 4 | # Do not make changes to this file unless you know what you are doing--modify 5 | # the SWIG interface file instead. 6 | 7 | from sys import version_info as _swig_python_version_info 8 | if _swig_python_version_info >= (2, 7, 0): 9 | def swig_import_helper(): 10 | import importlib 11 | pkg = __name__.rpartition('.')[0] 12 | mname = '.'.join((pkg, '_snowboydetect')).lstrip('.') 13 | try: 14 | return importlib.import_module(mname) 15 | except ImportError: 16 | return importlib.import_module('_snowboydetect') 17 | _snowboydetect = swig_import_helper() 18 | del swig_import_helper 19 | elif _swig_python_version_info >= (2, 6, 0): 20 | def swig_import_helper(): 21 | from os.path import dirname 22 | import imp 23 | fp = None 24 | try: 25 | fp, pathname, description = imp.find_module('_snowboydetect', [dirname(__file__)]) 26 | except ImportError: 27 | import _snowboydetect 28 | return _snowboydetect 29 | try: 30 | _mod = imp.load_module('_snowboydetect', fp, pathname, description) 31 | finally: 32 | if fp is not None: 33 | fp.close() 34 | return _mod 35 | _snowboydetect = swig_import_helper() 36 | del swig_import_helper 37 | else: 38 | import _snowboydetect 39 | del _swig_python_version_info 40 | 41 | try: 42 | _swig_property = property 43 | except NameError: 44 | pass # Python < 2.2 doesn't have 'property'. 45 | 46 | try: 47 | import builtins as __builtin__ 48 | except ImportError: 49 | import __builtin__ 50 | 51 | def _swig_setattr_nondynamic(self, class_type, name, value, static=1): 52 | if (name == "thisown"): 53 | return self.this.own(value) 54 | if (name == "this"): 55 | if type(value).__name__ == 'SwigPyObject': 56 | self.__dict__[name] = value 57 | return 58 | method = class_type.__swig_setmethods__.get(name, None) 59 | if method: 60 | return method(self, value) 61 | if (not static): 62 | if _newclass: 63 | object.__setattr__(self, name, value) 64 | else: 65 | self.__dict__[name] = value 66 | else: 67 | raise AttributeError("You cannot add attributes to %s" % self) 68 | 69 | 70 | def _swig_setattr(self, class_type, name, value): 71 | return _swig_setattr_nondynamic(self, class_type, name, value, 0) 72 | 73 | 74 | def _swig_getattr(self, class_type, name): 75 | if (name == "thisown"): 76 | return self.this.own() 77 | method = class_type.__swig_getmethods__.get(name, None) 78 | if method: 79 | return method(self) 80 | raise AttributeError("'%s' object has no attribute '%s'" % (class_type.__name__, name)) 81 | 82 | 83 | def _swig_repr(self): 84 | try: 85 | strthis = "proxy of " + self.this.__repr__() 86 | except __builtin__.Exception: 87 | strthis = "" 88 | return "<%s.%s; %s >" % (self.__class__.__module__, self.__class__.__name__, strthis,) 89 | 90 | try: 91 | _object = object 92 | _newclass = 1 93 | except __builtin__.Exception: 94 | class _object: 95 | pass 96 | _newclass = 0 97 | 98 | class SnowboyDetect(_object): 99 | __swig_setmethods__ = {} 100 | __setattr__ = lambda self, name, value: _swig_setattr(self, SnowboyDetect, name, value) 101 | __swig_getmethods__ = {} 102 | __getattr__ = lambda self, name: _swig_getattr(self, SnowboyDetect, name) 103 | __repr__ = _swig_repr 104 | 105 | def __init__(self, resource_filename, model_str): 106 | this = _snowboydetect.new_SnowboyDetect(resource_filename, model_str) 107 | try: 108 | self.this.append(this) 109 | except __builtin__.Exception: 110 | self.this = this 111 | 112 | def Reset(self): 113 | return _snowboydetect.SnowboyDetect_Reset(self) 114 | 115 | def RunDetection(self, *args): 116 | return _snowboydetect.SnowboyDetect_RunDetection(self, *args) 117 | 118 | def SetSensitivity(self, sensitivity_str): 119 | return _snowboydetect.SnowboyDetect_SetSensitivity(self, sensitivity_str) 120 | 121 | def GetSensitivity(self): 122 | return _snowboydetect.SnowboyDetect_GetSensitivity(self) 123 | 124 | def SetAudioGain(self, audio_gain): 125 | return _snowboydetect.SnowboyDetect_SetAudioGain(self, audio_gain) 126 | 127 | def UpdateModel(self): 128 | return _snowboydetect.SnowboyDetect_UpdateModel(self) 129 | 130 | def NumHotwords(self): 131 | return _snowboydetect.SnowboyDetect_NumHotwords(self) 132 | 133 | def ApplyFrontend(self, apply_frontend): 134 | return _snowboydetect.SnowboyDetect_ApplyFrontend(self, apply_frontend) 135 | 136 | def SampleRate(self): 137 | return _snowboydetect.SnowboyDetect_SampleRate(self) 138 | 139 | def NumChannels(self): 140 | return _snowboydetect.SnowboyDetect_NumChannels(self) 141 | 142 | def BitsPerSample(self): 143 | return _snowboydetect.SnowboyDetect_BitsPerSample(self) 144 | __swig_destroy__ = _snowboydetect.delete_SnowboyDetect 145 | __del__ = lambda self: None 146 | SnowboyDetect_swigregister = _snowboydetect.SnowboyDetect_swigregister 147 | SnowboyDetect_swigregister(SnowboyDetect) 148 | 149 | # This file is compatible with both classic and new-style classes. 150 | 151 | 152 | -------------------------------------------------------------------------------- /app/snowboy/snowboythreaded.py: -------------------------------------------------------------------------------- 1 | import snowboydecoder 2 | import threading 3 | import Queue 4 | 5 | 6 | class ThreadedDetector(threading.Thread): 7 | """ 8 | Wrapper class around detectors to run them in a separate thread 9 | and provide methods to pause, resume, and modify detection 10 | """ 11 | 12 | def __init__(self, models, **kwargs): 13 | """ 14 | Initialize Detectors object. **kwargs is for any __init__ keyword 15 | arguments to be passed into HotWordDetector __init__() method. 16 | """ 17 | threading.Thread.__init__(self) 18 | self.models = models 19 | self.init_kwargs = kwargs 20 | self.interrupted = True 21 | self.commands = Queue.Queue() 22 | self.vars_are_changed = True 23 | self.detectors = None # Initialize when thread is run in self.run() 24 | self.run_kwargs = None # Initialize when detectors start in self.start_recog() 25 | 26 | def initialize_detectors(self): 27 | """ 28 | Returns initialized Snowboy HotwordDetector objects 29 | """ 30 | self.detectors = snowboydecoder.HotwordDetector(self.models, **self.init_kwargs) 31 | 32 | def run(self): 33 | """ 34 | Runs in separate thread - waits on command to either run detectors 35 | or terminate thread from commands queue 36 | """ 37 | try: 38 | while True: 39 | command = self.commands.get(True) 40 | if command == "Start": 41 | self.interrupted = False 42 | if self.vars_are_changed: 43 | # If there is an existing detector object, terminate it 44 | if self.detectors is not None: 45 | self.detectors.terminate() 46 | self.initialize_detectors() 47 | self.vars_are_changed = False 48 | # Start detectors - blocks until interrupted by self.interrupted variable 49 | self.detectors.start(interrupt_check=lambda: self.interrupted, **self.run_kwargs) 50 | elif command == "Terminate": 51 | # Program ending - terminate thread 52 | break 53 | finally: 54 | if self.detectors is not None: 55 | self.detectors.terminate() 56 | 57 | def start_recog(self, **kwargs): 58 | """ 59 | Starts recognition in thread. Accepts kwargs to pass into the 60 | HotWordDetector.start() method, but does not accept interrupt_callback, 61 | as that is already set up. 62 | """ 63 | assert "interrupt_check" not in kwargs, \ 64 | "Cannot set interrupt_check argument. To interrupt detectors, use Detectors.pause_recog() instead" 65 | self.run_kwargs = kwargs 66 | self.commands.put("Start") 67 | 68 | def pause_recog(self): 69 | """ 70 | Halts recognition in thread. 71 | """ 72 | self.interrupted = True 73 | 74 | def terminate(self): 75 | """ 76 | Terminates recognition thread - called when program terminates 77 | """ 78 | self.pause_recog() 79 | self.commands.put("Terminate") 80 | 81 | def is_running(self): 82 | return not self.interrupted 83 | 84 | def change_models(self, models): 85 | if self.is_running(): 86 | print("Models will be changed after restarting detectors.") 87 | if self.models != models: 88 | self.models = models 89 | self.vars_are_changed = True 90 | 91 | def change_sensitivity(self, sensitivity): 92 | if self.is_running(): 93 | print("Sensitivity will be changed after restarting detectors.") 94 | if self.init_kwargs['sensitivity'] != sensitivity: 95 | self.init_kwargs['sensitivity'] = sensitivity 96 | self.vars_are_changed = True 97 | -------------------------------------------------------------------------------- /app/snowboy/xiaoduxiaodu_all_10022017.umdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/snowboy/xiaoduxiaodu_all_10022017.umdl -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/app/utils/__init__.py -------------------------------------------------------------------------------- /app/utils/mic_data_saver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import Queue as queue 3 | import threading 4 | import wave 5 | 6 | 7 | class MicDataSaver(object): 8 | ''' 9 | 录音数据保存工具类 10 | ''' 11 | 12 | def __init__(self): 13 | # 保存录音数据文件名称 14 | self.file_name = 'mic_save_data.wav' 15 | self.queue = queue.Queue() 16 | self.wf = None 17 | 18 | def put(self, data): 19 | ''' 20 | 录音数据缓存 21 | :param data:录音pcm流 22 | :return: 23 | ''' 24 | 25 | self.queue.put(data) 26 | 27 | def start(self): 28 | ''' 29 | 开始保存录音数据 30 | :return: 31 | ''' 32 | self.wf = wave.open(self.file_name, 'wb') 33 | self.wf.setnchannels(1) 34 | self.wf.setsampwidth(2) 35 | self.wf.setframerate(16000) 36 | 37 | self.done = False 38 | thread = threading.Thread(target=self.__run) 39 | thread.daemon = True 40 | thread.start() 41 | 42 | def stop(self): 43 | ''' 44 | 停止录音数据保存 45 | :return: 46 | ''' 47 | self.done = True 48 | 49 | self.wf.close() 50 | 51 | def __run(self): 52 | ''' 53 | 录音数据保存到文件中 54 | :return: 55 | ''' 56 | while not self.done: 57 | chunk = self.queue.get() 58 | self.wf.writeframes(chunk) 59 | -------------------------------------------------------------------------------- /app/utils/prompt_tone.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from app.framework.player import Player 4 | 5 | 6 | class PromptTone(object): 7 | ''' 8 | 提示音播放类(用于唤醒态提示) 9 | ''' 10 | 11 | def __init__(self): 12 | self.player = Player() 13 | resource = os.path.realpath(os.path.join(os.path.dirname(__file__), '../resources/du.mp3')) 14 | self.resource_uri = 'file://{}'.format(resource) 15 | 16 | def play(self): 17 | ''' 18 | 提示音播放 19 | :return: 20 | ''' 21 | self.player.play(self.resource_uri) 22 | -------------------------------------------------------------------------------- /app/wakeup_trigger_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 通过[小度小度]触发进入唤醒状态 5 | 6 | """ 7 | import threading 8 | import time 9 | 10 | try: 11 | import Queue as queue 12 | except ImportError: 13 | import queueg 14 | 15 | import logging 16 | import app.app_config as app_config 17 | 18 | from sdk.dueros_core import DuerOS 19 | from app.framework.player import Player 20 | from app.framework.mic import Audio 21 | from app.snowboy import snowboydecoder 22 | from app.utils.prompt_tone import PromptTone 23 | 24 | logging.basicConfig(level=app_config.LOGGER_LEVEL) 25 | 26 | 27 | class SnowBoy(object): 28 | ''' 29 | 基于SnowBoy的唤醒类 30 | ''' 31 | 32 | def __init__(self, model): 33 | ''' 34 | SnowBoy初始化 35 | :param model:唤醒词训练模型 36 | ''' 37 | self.calback = None 38 | self.detector = snowboydecoder.HotwordDetector(model, sensitivity=0.5, audio_gain=1) 39 | 40 | def feed_data(self, data): 41 | ''' 42 | 唤醒引擎语音数据输入 43 | :param data: 录音pcm数据流 44 | :return: 45 | ''' 46 | self.detector.feed_data(data) 47 | 48 | def set_callback(self, callback): 49 | ''' 50 | 唤醒状态回调 51 | :param callback:唤醒状态回调函数 52 | :return: 53 | ''' 54 | if not callable(callback): 55 | raise ValueError('注册回调失败[参数不可调用]!') 56 | 57 | self.calback = callback 58 | 59 | def start(self): 60 | ''' 61 | 唤醒引擎启动 62 | :return: 63 | ''' 64 | thread = threading.Thread(target=self.__run) 65 | thread.daemon = True 66 | thread.start() 67 | 68 | def stop(self): 69 | ''' 70 | 唤醒引擎关闭 71 | :return: 72 | ''' 73 | self.detector.terminate() 74 | 75 | def __run(self): 76 | ''' 77 | 唤醒检测线程实体 78 | :return: 79 | ''' 80 | self.detector.start(self.calback) 81 | 82 | 83 | class WakeupEngine(object): 84 | ''' 85 | 唤醒引擎(平台无关) 86 | ''' 87 | 88 | def __init__(self): 89 | self.queue = queue.Queue() 90 | 91 | self.sinks = [] 92 | self.callback = None 93 | 94 | self.done = False 95 | 96 | def set_wakeup_detector(self, detector): 97 | ''' 98 | 设置唤醒引擎 99 | :param detector:唤醒引擎(如SnowBoy) 100 | :return: 101 | ''' 102 | if hasattr(detector, 'feed_data') and callable(detector.feed_data): 103 | self.wakeup_detector = detector 104 | else: 105 | raise ValueError('唤醒引擎设置失败[不存在可调用的feed_data方法]!') 106 | 107 | def put(self, data): 108 | ''' 109 | 录音数据缓存 110 | :param data:录音pcm流 111 | :return: 112 | ''' 113 | self.queue.put(data) 114 | 115 | def start(self): 116 | ''' 117 | 唤醒引擎启动 118 | :return: 119 | ''' 120 | self.done = False 121 | thread = threading.Thread(target=self.__run) 122 | thread.daemon = True 123 | thread.start() 124 | 125 | def stop(self): 126 | ''' 127 | 唤醒引擎关闭 128 | :return: 129 | ''' 130 | self.done = True 131 | 132 | def link(self, sink): 133 | ''' 134 | 连接DuerOS核心实现模块 135 | :param sink:DuerOS核心实现模块 136 | :return: 137 | ''' 138 | if hasattr(sink, 'put') and callable(sink.put): 139 | self.sinks.append(sink) 140 | else: 141 | raise ValueError('link注册对象无put方法') 142 | 143 | def unlink(self, sink): 144 | ''' 145 | 移除DuerOS核心实现模块 146 | :param sink: DuerOS核心实现模块 147 | :return: 148 | ''' 149 | self.sinks.remove(sink) 150 | 151 | def __run(self): 152 | ''' 153 | 唤醒引擎线程实体 154 | :return: 155 | ''' 156 | while not self.done: 157 | chunk = self.queue.get() 158 | self.wakeup_detector.feed_data(chunk) 159 | 160 | for sink in self.sinks: 161 | sink.put(chunk) 162 | 163 | 164 | def directive_listener(directive_content): 165 | ''' 166 | 云端下发directive监听器 167 | :param directive_content:云端下发directive内容 168 | :return: 169 | ''' 170 | content = u'云端下发directive:%s' % (directive_content) 171 | logging.info(content) 172 | 173 | 174 | def main(): 175 | # 创建录音设备(平台相关) 176 | audio = Audio() 177 | # 创建唤醒引擎 178 | wakeup_engine = WakeupEngine() 179 | # 创建播放器(平台相关) 180 | player = Player() 181 | # 创建duerOS核心处理模块 182 | dueros = DuerOS(player) 183 | dueros.set_directive_listener(directive_listener) 184 | 185 | # [小度小度] SnowBoy唤醒引擎 186 | model = 'app/snowboy/xiaoduxiaodu_all_10022017.umdl' 187 | # SnowBoy唤醒引擎实体 188 | snowboy = SnowBoy(model) 189 | 190 | audio.link(wakeup_engine) 191 | wakeup_engine.link(dueros) 192 | wakeup_engine.set_wakeup_detector(snowboy) 193 | 194 | prompt_tone_player = PromptTone() 195 | 196 | def wakeup(): 197 | ''' 198 | 唤醒回调 199 | :return: 200 | ''' 201 | print '[小度]已唤醒,我能为你做些什么..........' 202 | # 唤醒态提示音 203 | prompt_tone_player.play() 204 | dueros.listen() 205 | 206 | snowboy.set_callback(wakeup) 207 | 208 | dueros.start() 209 | wakeup_engine.start() 210 | snowboy.start() 211 | audio.start() 212 | 213 | print '请说[小度小度]来唤醒我.......' 214 | 215 | while True: 216 | try: 217 | time.sleep(1) 218 | except KeyboardInterrupt: 219 | break 220 | 221 | dueros.stop() 222 | wakeup_engine.stop() 223 | audio.stop() 224 | snowboy.stop() 225 | 226 | 227 | if __name__ == '__main__': 228 | main() 229 | -------------------------------------------------------------------------------- /auth.sh: -------------------------------------------------------------------------------- 1 | # 认证授权 2 | 3 | WORK_PATH="${PWD}" 4 | export PYTHONPATH=${WORK_PATH}:${PYTHONPATH} 5 | 6 | python ./app/auth.py -------------------------------------------------------------------------------- /enter_trigger_start.sh: -------------------------------------------------------------------------------- 1 | # 通过"Enter"键来触发唤醒 2 | 3 | WORK_PATH="${PWD}" 4 | export PYTHONPATH=${WORK_PATH}:${PYTHONPATH} 5 | 6 | python ./app/enter_trigger_main.py -------------------------------------------------------------------------------- /readme_resources/代码结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/readme_resources/代码结构.png -------------------------------------------------------------------------------- /sdk/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for Python Alexa Voice Service.""" 4 | 5 | __author__ = """Yihui Xiong""" 6 | __email__ = 'yihui.xiong@hotmail.com' 7 | __version__ = '0.0.7' 8 | -------------------------------------------------------------------------------- /sdk/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 认证授权模块 4 | ''' 5 | import tornado.httpserver 6 | import tornado.ioloop 7 | import tornado.web 8 | import time 9 | import json 10 | import requests 11 | import datetime 12 | 13 | import sdk.configurate as configurate 14 | 15 | # 开发者默认注册信息 16 | CLIENT_ID = "5GFgMRfHOhIvI0B8AZB78nt676FeWA9n" 17 | CLIENT_SECRET = "eq2eCNfbtOrGwdlA4vB1N1EaiwjBMu7i" 18 | 19 | # 百度token服务器url 20 | TOKEN_URL = 'https://openapi.baidu.com/oauth/2.0/token' 21 | # 百度oauth服务器url 22 | OAUTH_URL = 'https://openapi.baidu.com/oauth/2.0/authorize' 23 | 24 | 25 | class MainHandler(tornado.web.RequestHandler): 26 | ''' 27 | Tornado webServer请求处理类 28 | ''' 29 | 30 | def initialize(self, output, client_id, client_secret): 31 | ''' 32 | 处理类初始化 33 | :param output:配置文件保存地址 34 | :param client_id: 开发者注册信息 35 | :param client_secret: 开发者注册信息 36 | :return: 37 | ''' 38 | self.config = configurate.load(client_id, client_secret) 39 | self.output = output 40 | 41 | self.token_url = TOKEN_URL 42 | self.oauth_url = OAUTH_URL 43 | 44 | @tornado.web.asynchronous 45 | def get(self): 46 | ''' 47 | get 请求处理 48 | :return: 49 | ''' 50 | redirect_uri = self.request.protocol + "://" + self.request.host + "/authresponse" 51 | if self.request.path == '/authresponse': 52 | code = self.get_argument("code") 53 | payload = { 54 | "client_id": self.config['client_id'], 55 | "client_secret": self.config['client_secret'], 56 | "code": code, 57 | "grant_type": "authorization_code", 58 | "redirect_uri": redirect_uri 59 | } 60 | 61 | r = requests.post(self.token_url, data=payload) 62 | config = r.json() 63 | print(r.text) 64 | self.config['refresh_token'] = config['refresh_token'] 65 | 66 | if 'access_token' in config: 67 | date_format = "%a %b %d %H:%M:%S %Y" 68 | expiry_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=config['expires_in']) 69 | self.config['expiry'] = expiry_time.strftime(date_format) 70 | self.config['access_token'] = config['access_token'] 71 | 72 | # print(json.dumps(self.config, indent=4)) 73 | configurate.save(self.config, configfile=self.output) 74 | 75 | self.write('Succeed to login DuerOS Voice Service') 76 | self.finish() 77 | tornado.ioloop.IOLoop.instance().stop() 78 | else: 79 | payload = { 80 | "client_id": self.config['client_id'], 81 | "scope": 'basic', 82 | "response_type": "code", 83 | "redirect_uri": redirect_uri 84 | } 85 | 86 | req = requests.Request('GET', self.oauth_url, params=payload) 87 | p = req.prepare() 88 | self.redirect(p.url) 89 | 90 | 91 | def login(client_id, client_secret): 92 | ''' 93 | 初始化Tornado web server 94 | :param client_id: 开发者信息 95 | :param client_secret: 开发者信息 96 | :return: 97 | ''' 98 | application = tornado.web.Application([(r".*", MainHandler, 99 | dict(output=configurate.DEFAULT_CONFIG_FILE, client_id=client_id, 100 | client_secret=client_secret))]) 101 | http_server = tornado.httpserver.HTTPServer(application) 102 | http_server.listen(3000) 103 | tornado.ioloop.IOLoop.instance().start() 104 | tornado.ioloop.IOLoop.instance().close() 105 | 106 | 107 | def auth_request(client_id=CLIENT_ID, client_secret=CLIENT_SECRET): 108 | ''' 109 | 发起认证 110 | :param client_id:开发者注册信息 111 | :param client_secret: 开发者注册信息 112 | :return: 113 | ''' 114 | try: 115 | import webbrowser 116 | except ImportError: 117 | print('Go to http://{your device IP}:3000 to start') 118 | login(client_id, client_secret) 119 | return 120 | 121 | import threading 122 | webserver = threading.Thread(target=login, args=(client_id, client_secret)) 123 | webserver.daemon = True 124 | webserver.start() 125 | print("A web page should is opened. If not, go to http://127.0.0.1:3000 to start") 126 | webbrowser.open('http://127.0.0.1:3000') 127 | 128 | while webserver.is_alive(): 129 | try: 130 | time.sleep(1) 131 | except KeyboardInterrupt: 132 | break 133 | -------------------------------------------------------------------------------- /sdk/configurate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 认证授权信息持久化 4 | ''' 5 | import json 6 | import os 7 | import uuid 8 | 9 | # 配置文件保存位置 10 | DEFAULT_CONFIG_FILE = os.path.join(os.path.expanduser('~'), '.dueros.json') 11 | 12 | 13 | def load(client_id=None, client_secret=None): 14 | ''' 15 | 认证授权信息加载 16 | :param client_id:开发者注册信息 17 | :param client_secret: 开发者注册信息 18 | :return: 19 | ''' 20 | if os.path.isfile(DEFAULT_CONFIG_FILE): 21 | configfile = DEFAULT_CONFIG_FILE 22 | else: 23 | product_id = "EddyLiu-" + uuid.uuid4().hex 24 | 25 | return { 26 | "dueros-device-id": product_id, 27 | "client_id": client_id, 28 | "client_secret": client_secret 29 | } 30 | 31 | with open(configfile, 'r') as f: 32 | config = json.load(f) 33 | require_keys = ['dueros-device-id', 'client_id', 'client_secret'] 34 | for key in require_keys: 35 | if not ((key in config) and config[key]): 36 | raise KeyError('{} should include "{}"'.format(configfile, key)) 37 | 38 | return config 39 | 40 | 41 | def save(config, configfile=None): 42 | ''' 43 | 认证授权信息保存 44 | :param config: 45 | :param configfile: 46 | :return: 47 | ''' 48 | if configfile is None: 49 | configfile = DEFAULT_CONFIG_FILE 50 | 51 | with open(configfile, 'w') as f: 52 | json.dump(config, f, indent=4) 53 | -------------------------------------------------------------------------------- /sdk/dueros_core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | DuerOS服务核心模块 4 | ''' 5 | 6 | import cgi 7 | import io 8 | import json 9 | import logging 10 | import os 11 | import sys 12 | import tempfile 13 | import uuid 14 | 15 | import requests 16 | 17 | try: 18 | import Queue as queue 19 | except ImportError: 20 | import queue 21 | import threading 22 | import datetime 23 | 24 | import hyper 25 | 26 | import sdk.configurate 27 | import sdk.sdk_config as sdk_config 28 | 29 | from sdk.interface.alerts import Alerts 30 | from sdk.interface.audio_player import AudioPlayer 31 | from sdk.interface.speaker import Speaker 32 | from sdk.interface.speech_recognizer import SpeechRecognizer 33 | from sdk.interface.speech_synthesizer import SpeechSynthesizer 34 | from sdk.interface.system import System 35 | 36 | logging.basicConfig(level=sdk_config.LOGGER_LEVEL) 37 | logger = logging.getLogger(__name__) 38 | 39 | 40 | class DuerOSStateListner(object): 41 | ''' 42 | DuerOS状态监听类 43 | ''' 44 | 45 | def __init__(self): 46 | pass 47 | 48 | def on_listening(self): 49 | ''' 50 | 监听状态回调 51 | :return: 52 | ''' 53 | logging.info('[DuerOS状态]正在倾听..........') 54 | 55 | def on_thinking(self): 56 | ''' 57 | 语义理解状态回调 58 | :return: 59 | ''' 60 | logging.info('[DuerOS状态]正在思考.........') 61 | 62 | def on_speaking(self): 63 | ''' 64 | 播放状态回调 65 | :return: 66 | ''' 67 | logging.info('[DuerOS状态]正在播放........') 68 | 69 | def on_finished(self): 70 | ''' 71 | 处理结束状态回调 72 | :return: 73 | ''' 74 | logging.info('[DuerOS状态]结束') 75 | 76 | 77 | class DuerOS(object): 78 | ''' 79 | DuerOS核心模块类,实现功能包括: 80 | 录音数据上传 81 | 本地状态上报 82 | 长链接建立与维护(Ping) 83 | Directive下发 84 | ''' 85 | 86 | def __init__(self, player): 87 | ''' 88 | 类初始化 89 | :param player:播放器 90 | ''' 91 | self.event_queue = queue.Queue() 92 | self.speech_recognizer = SpeechRecognizer(self) 93 | self.speech_synthesizer = SpeechSynthesizer(self, player) 94 | self.audio_player = AudioPlayer(self, player) 95 | self.speaker = Speaker(self) 96 | self.alerts = Alerts(self, player) 97 | self.system = System(self) 98 | 99 | self.state_listener = DuerOSStateListner() 100 | 101 | # handle audio to speech recognizer 102 | self.put = self.speech_recognizer.put 103 | 104 | # listen() will trigger SpeechRecognizer's Recognize event 105 | # self.listen = self.speech_recognizer.recognize 106 | 107 | self.done = False 108 | 109 | self.requests = requests.Session() 110 | 111 | self.__config = sdk.configurate.load() 112 | 113 | self.__config['host_url'] = 'dueros-h2.baidu.com' 114 | 115 | self.__config['api'] = 'dcs/v1' 116 | self.__config['refresh_url'] = 'https://openapi.baidu.com/oauth/2.0/token' 117 | 118 | self.last_activity = datetime.datetime.utcnow() 119 | self.__ping_time = None 120 | 121 | self.directive_listener = None 122 | 123 | def set_directive_listener(self, listener): 124 | ''' 125 | directive监听器设置 126 | :param listener: directive监听器 127 | :return: 128 | ''' 129 | if callable(listener): 130 | self.directive_listener = listener 131 | else: 132 | raise ValueError('directive监听器注册失败[参数不可回调]!') 133 | 134 | def start(self): 135 | ''' 136 | DuerOS模块启动 137 | :return: 138 | ''' 139 | self.done = False 140 | 141 | t = threading.Thread(target=self.run) 142 | t.daemon = True 143 | t.start() 144 | 145 | def stop(self): 146 | ''' 147 | DuerOS模块停止 148 | :return: 149 | ''' 150 | self.done = True 151 | 152 | def listen(self): 153 | ''' 154 | DuerOS进入语音识别状态 155 | :return: 156 | ''' 157 | self.speech_recognizer.recognize() 158 | 159 | def send_event(self, event, listener=None, attachment=None): 160 | ''' 161 | 状态上报 162 | :param event:上传状态 163 | :param listener:VAD检测回调[云端识别语音输入结束] 164 | :param attachment:录音数据 165 | :return: 166 | ''' 167 | self.event_queue.put((event, listener, attachment)) 168 | 169 | def run(self): 170 | ''' 171 | DuerOS线程实体 172 | :return: 173 | ''' 174 | while not self.done: 175 | try: 176 | self.__run() 177 | except AttributeError as e: 178 | logger.exception(e) 179 | continue 180 | except hyper.http20.exceptions.StreamResetError as e: 181 | logger.exception(e) 182 | continue 183 | except ValueError as e: 184 | logging.exception(e) 185 | # failed to get an access token, exit 186 | sys.exit(1) 187 | except Exception as e: 188 | logging.exception(e) 189 | continue 190 | 191 | def __run(self): 192 | ''' 193 | run方法实现 194 | :return: 195 | ''' 196 | conn = hyper.HTTP20Connection('{}:443'.format(self.__config['host_url']), force_proto='h2') 197 | 198 | headers = {'authorization': 'Bearer {}'.format(self.token)} 199 | if 'dueros-device-id' in self.__config: 200 | headers['dueros-device-id'] = self.__config['dueros-device-id'] 201 | 202 | downchannel_id = conn.request('GET', '/{}/directives'.format(self.__config['api']), headers=headers) 203 | downchannel_response = conn.get_response(downchannel_id) 204 | 205 | if downchannel_response.status != 200: 206 | raise ValueError("/directive requests returned {}".format(downchannel_response.status)) 207 | 208 | ctype, pdict = cgi.parse_header(downchannel_response.headers['content-type'][0].decode('utf-8')) 209 | downchannel_boundary = '--{}'.format(pdict['boundary']).encode('utf-8') 210 | downchannel = conn.streams[downchannel_id] 211 | downchannel_buffer = io.BytesIO() 212 | eventchannel_boundary = 'baidu-voice-engine' 213 | 214 | # ping every 5 minutes (60 seconds early for latency) to maintain the connection 215 | self.__ping_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=240) 216 | 217 | self.event_queue.queue.clear() 218 | 219 | self.system.synchronize_state() 220 | 221 | while not self.done: 222 | # logger.info("Waiting for event to send to AVS") 223 | # logger.info("Connection socket can_read %s", conn._sock.can_read) 224 | try: 225 | event, listener, attachment = self.event_queue.get(timeout=0.25) 226 | except queue.Empty: 227 | event = None 228 | 229 | # we want to avoid blocking if the data wasn't for stream downchannel 230 | while conn._sock.can_read: 231 | conn._single_read() 232 | 233 | while downchannel.data: 234 | framebytes = downchannel._read_one_frame() 235 | self.__read_response(framebytes, downchannel_boundary, downchannel_buffer) 236 | 237 | if event is None: 238 | self.__ping(conn) 239 | continue 240 | 241 | headers = { 242 | ':method': 'POST', 243 | ':scheme': 'https', 244 | ':path': '/{}/events'.format(self.__config['api']), 245 | 'authorization': 'Bearer {}'.format(self.token), 246 | 'content-type': 'multipart/form-data; boundary={}'.format(eventchannel_boundary) 247 | } 248 | if 'dueros-device-id' in self.__config: 249 | headers['dueros-device-id'] = self.__config['dueros-device-id'] 250 | 251 | stream_id = conn.putrequest(headers[':method'], headers[':path']) 252 | default_headers = (':method', ':scheme', ':authority', ':path') 253 | for name, value in headers.items(): 254 | is_default = name in default_headers 255 | conn.putheader(name, value, stream_id, replace=is_default) 256 | conn.endheaders(final=False, stream_id=stream_id) 257 | 258 | metadata = { 259 | 'clientContext': self.context, 260 | 'event': event 261 | } 262 | logger.debug('metadata: {}'.format(json.dumps(metadata, indent=4))) 263 | 264 | json_part = '--{}\r\n'.format(eventchannel_boundary) 265 | json_part += 'Content-Disposition: form-data; name="metadata"\r\n' 266 | json_part += 'Content-Type: application/json; charset=UTF-8\r\n\r\n' 267 | json_part += json.dumps(metadata) 268 | 269 | conn.send(json_part.encode('utf-8'), final=False, stream_id=stream_id) 270 | 271 | if attachment: 272 | attachment_header = '\r\n--{}\r\n'.format(eventchannel_boundary) 273 | attachment_header += 'Content-Disposition: form-data; name="audio"\r\n' 274 | attachment_header += 'Content-Type: application/octet-stream\r\n\r\n' 275 | conn.send(attachment_header.encode('utf-8'), final=False, stream_id=stream_id) 276 | 277 | # AVS_AUDIO_CHUNK_PREFERENCE = 320 278 | for chunk in attachment: 279 | conn.send(chunk, final=False, stream_id=stream_id) 280 | # print '===============send(attachment.chunk)' 281 | 282 | # check if StopCapture directive is received 283 | while conn._sock.can_read: 284 | conn._single_read() 285 | 286 | while downchannel.data: 287 | framebytes = downchannel._read_one_frame() 288 | self.__read_response(framebytes, downchannel_boundary, downchannel_buffer) 289 | 290 | self.last_activity = datetime.datetime.utcnow() 291 | 292 | end_part = '\r\n--{}--'.format(eventchannel_boundary) 293 | conn.send(end_part.encode('utf-8'), final=True, stream_id=stream_id) 294 | 295 | logger.info("wait for response") 296 | resp = conn.get_response(stream_id) 297 | logger.info("status code: %s", resp.status) 298 | 299 | if resp.status == 200: 300 | self.__read_response(resp) 301 | elif resp.status == 204: 302 | pass 303 | else: 304 | logger.warning(resp.headers) 305 | logger.warning(resp.read()) 306 | 307 | if listener and callable(listener): 308 | listener() 309 | 310 | def __read_response(self, response, boundary=None, buffer=None): 311 | ''' 312 | 云端回复数据读取解析 313 | :param response:包含http header信息 314 | :param boundary:multipart boundary 315 | :param buffer:包含http body数据 316 | :return: 317 | ''' 318 | if boundary: 319 | endboundary = boundary + b"--" 320 | else: 321 | ctype, pdict = cgi.parse_header( 322 | response.headers['content-type'][0].decode('utf-8')) 323 | boundary = "--{}".format(pdict['boundary']).encode('utf-8') 324 | endboundary = "--{}--".format(pdict['boundary']).encode('utf-8') 325 | 326 | on_boundary = False 327 | in_header = False 328 | in_payload = False 329 | first_payload_block = False 330 | content_type = None 331 | content_id = None 332 | 333 | def iter_lines(response, delimiter=None): 334 | pending = None 335 | for chunk in response.read_chunked(): 336 | # logger.debug("Chunk size is {}".format(len(chunk))) 337 | if pending is not None: 338 | chunk = pending + chunk 339 | if delimiter: 340 | lines = chunk.split(delimiter) 341 | else: 342 | lines = chunk.splitlines() 343 | 344 | if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]: 345 | pending = lines.pop() 346 | else: 347 | pending = None 348 | 349 | for line in lines: 350 | yield line 351 | 352 | if pending is not None: 353 | yield pending 354 | 355 | # cache them up to execute after we've downloaded any binary attachments 356 | # so that they have the content available 357 | directives = [] 358 | if isinstance(response, bytes): 359 | buffer.seek(0) 360 | lines = (buffer.read() + response).split(b"\r\n") 361 | buffer.flush() 362 | else: 363 | lines = iter_lines(response, delimiter=b"\r\n") 364 | for line in lines: 365 | # logger.debug("iter_line is {}...".format(repr(line)[0:30])) 366 | if line == boundary or line == endboundary: 367 | # logger.debug("Newly on boundary") 368 | on_boundary = True 369 | if in_payload: 370 | in_payload = False 371 | if content_type == "application/json": 372 | logger.info("Finished downloading JSON") 373 | utf8_payload = payload.getvalue().decode('utf-8') 374 | if utf8_payload: 375 | json_payload = json.loads(utf8_payload) 376 | logger.debug(json_payload) 377 | if 'directive' in json_payload: 378 | directives.append(json_payload['directive']) 379 | else: 380 | logger.info("Finished downloading {} which is {}".format(content_type, content_id)) 381 | payload.seek(0) 382 | # TODO, start to stream this to speakers as soon as we start getting bytes 383 | # strip < and > 384 | content_id = content_id[1:-1] 385 | with open(os.path.join(tempfile.gettempdir(), '{}.mp3'.format(content_id)), 'wb') as f: 386 | f.write(payload.read()) 387 | 388 | logger.info('write audio to {}.mp3'.format(content_id)) 389 | 390 | continue 391 | elif on_boundary: 392 | # logger.debug("Now in header") 393 | on_boundary = False 394 | in_header = True 395 | elif in_header and line == b"": 396 | # logger.debug("Found end of header") 397 | in_header = False 398 | in_payload = True 399 | first_payload_block = True 400 | payload = io.BytesIO() 401 | continue 402 | 403 | if in_header: 404 | # logger.debug(repr(line)) 405 | if len(line) > 1: 406 | header, value = line.decode('utf-8').split(":", 1) 407 | ctype, pdict = cgi.parse_header(value) 408 | if header.lower() == "content-type": 409 | content_type = ctype 410 | if header.lower() == "content-id": 411 | content_id = ctype 412 | 413 | if in_payload: 414 | # add back the bytes that our iter_lines consumed 415 | logger.info("Found %s bytes of %s %s, first_payload_block=%s", 416 | len(line), content_id, content_type, first_payload_block) 417 | if first_payload_block: 418 | first_payload_block = False 419 | else: 420 | payload.write(b"\r\n") 421 | # TODO write this to a queue.Queue in self._content_cache[content_id] 422 | # so that other threads can start to play it right away 423 | payload.write(line) 424 | 425 | if buffer is not None: 426 | if in_payload: 427 | logger.info( 428 | "Didn't see an entire directive, buffering to put at top of next frame") 429 | buffer.write(payload.read()) 430 | else: 431 | buffer.write(boundary) 432 | buffer.write(b"\r\n") 433 | 434 | for directive in directives: 435 | self.__handle_directive(directive) 436 | 437 | def __handle_directive(self, directive): 438 | ''' 439 | directive处理 440 | :param directive: 441 | :return: 442 | ''' 443 | if 'directive_listener' in dir(self): 444 | self.directive_listener(directive) 445 | 446 | logger.debug(json.dumps(directive, indent=4)) 447 | try: 448 | namespace = directive['header']['namespace'] 449 | 450 | namespace = self.__namespace_convert(namespace) 451 | if not namespace: 452 | return 453 | 454 | name = directive['header']['name'] 455 | name = self.__name_convert(name) 456 | if hasattr(self, namespace): 457 | interface = getattr(self, namespace) 458 | directive_func = getattr(interface, name, None) 459 | if directive_func: 460 | directive_func(directive) 461 | else: 462 | logger.info('{}.{} is not implemented yet'.format(namespace, name)) 463 | else: 464 | logger.info('{} is not implemented yet'.format(namespace)) 465 | 466 | except KeyError as e: 467 | logger.exception(e) 468 | except Exception as e: 469 | logger.exception(e) 470 | 471 | def __ping(self, connection): 472 | ''' 473 | 长链接维护,ping操作 474 | :param connection:链接句柄 475 | :return: 476 | ''' 477 | if datetime.datetime.utcnow() >= self.__ping_time: 478 | # ping_stream_id = connection.request('GET', '/ping', 479 | # headers={'authorization': 'Bearer {}'.format(self.token)}) 480 | # resp = connection.get_response(ping_stream_id) 481 | # if resp.status != 200 and resp.status != 204: 482 | # logger.warning(resp.read()) 483 | # raise ValueError("/ping requests returned {}".format(resp.status)) 484 | 485 | connection.ping(uuid.uuid4().hex[:8]) 486 | 487 | logger.debug('ping at {}'.format(datetime.datetime.utcnow().strftime("%a %b %d %H:%M:%S %Y"))) 488 | 489 | # ping every 5 minutes (60 seconds early for latency) to maintain the connection 490 | self.__ping_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=240) 491 | 492 | @property 493 | def context(self): 494 | ''' 495 | 模块当前上下文(当前状态集合) 496 | :return: 497 | ''' 498 | return [self.speech_synthesizer.context, self.speaker.context, self.audio_player.context, self.alerts.context] 499 | 500 | @property 501 | def token(self): 502 | ''' 503 | token获取 504 | :return: 505 | ''' 506 | date_format = "%a %b %d %H:%M:%S %Y" 507 | 508 | if 'access_token' in self.__config: 509 | if 'expiry' in self.__config: 510 | expiry = datetime.datetime.strptime(self.__config['expiry'], date_format) 511 | # refresh 60 seconds early to avoid chance of using expired access_token 512 | if (datetime.datetime.utcnow() - expiry) > datetime.timedelta(seconds=60): 513 | logger.info("Refreshing access_token") 514 | else: 515 | return self.__config['access_token'] 516 | 517 | payload = { 518 | 'client_id': self.__config['client_id'], 519 | 'client_secret': self.__config['client_secret'], 520 | 'grant_type': 'refresh_token', 521 | 'refresh_token': self.__config['refresh_token'] 522 | } 523 | 524 | response = None 525 | 526 | # try to request an access token 3 times 527 | for i in range(3): 528 | try: 529 | response = self.requests.post(self.__config['refresh_url'], data=payload) 530 | if response.status_code != 200: 531 | logger.warning(response.text) 532 | else: 533 | break 534 | except Exception as e: 535 | logger.exception(e) 536 | continue 537 | 538 | if (response is None) or (not hasattr(response, 'status_code')) or response.status_code != 200: 539 | raise ValueError("refresh token request returned {}".format(response.status)) 540 | 541 | config = response.json() 542 | self.__config['access_token'] = config['access_token'] 543 | 544 | expiry_time = datetime.datetime.utcnow() + datetime.timedelta(seconds=config['expires_in']) 545 | self.__config['expiry'] = expiry_time.strftime(date_format) 546 | logger.debug(json.dumps(self.__config, indent=4)) 547 | 548 | sdk.configurate.save(self.__config, configfile=self._configfile) 549 | 550 | return self.__config['access_token'] 551 | 552 | def __namespace_convert(self, namespace): 553 | ''' 554 | 将namespace字段内容与interface中的模块进行一一对应 555 | :param namespace: directive中namespace字段 556 | :return: 557 | ''' 558 | if namespace == 'ai.dueros.device_interface.voice_output': 559 | return 'speech_synthesizer' 560 | elif namespace == 'ai.dueros.device_interface.voice_input': 561 | return 'speech_recognizer' 562 | elif namespace == 'ai.dueros.device_interface.alerts': 563 | return 'alerts' 564 | elif namespace == 'ai.dueros.device_interface.audio_player': 565 | return 'audio_player' 566 | elif namespace == 'ai.dueros.device_interface.speaker_controller': 567 | return 'speaker' 568 | elif namespace == 'ai.dueros.device_interface.system': 569 | return 'system' 570 | else: 571 | return None 572 | 573 | def __name_convert(self, name): 574 | ''' 575 | 将name字段内容与interface中的模块方法进行一一对应 576 | :param name: directive中name字段 577 | :return: 578 | ''' 579 | # 语音输入模块[speech_recognizer] 580 | if name == 'StopListen': 581 | return 'stop_listen' 582 | elif name == 'Listen': 583 | return 'listen' 584 | # 语音输出模块[speech_synthesizer] 585 | elif name == 'Speak': 586 | return 'speak' 587 | # 扬声器控制模块[speaker] 588 | elif name == 'SetVolume': 589 | return 'set_volume' 590 | elif name == 'AdjustVolume': 591 | return 'adjust_volume' 592 | elif name == 'SetMute': 593 | return 'set_mute' 594 | # 音频播放器模块[audio_player] 595 | elif name == 'Play': 596 | return 'play' 597 | elif name == 'Stop': 598 | return 'stop' 599 | elif name == 'ClearQueue': 600 | return 'clear_queue' 601 | # 播放控制[playback_controller] 602 | # 闹钟模块[alerts] 603 | elif name == 'SetAlert': 604 | return 'set_alert' 605 | elif name == 'DeleteAlert': 606 | return 'delete_alert' 607 | # 屏幕展示模块[screen_display] 608 | elif name == 'HtmlView': 609 | return 'html_view' 610 | # 系统模块 611 | elif name == 'ResetUserInactivity': 612 | return 'reset_user_inactivity' 613 | elif name == 'SetEndpoint': 614 | return 'set_end_point' 615 | elif name == 'ThrowException': 616 | return 'throw_exception' 617 | -------------------------------------------------------------------------------- /sdk/interface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/sdk/interface/__init__.py -------------------------------------------------------------------------------- /sdk/interface/alerts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Alert模块 5 | 参考:http://open.duer.baidu.com/doc/dueros-conversational-service/device-interface/alerts_markdown 6 | """ 7 | 8 | import os 9 | import datetime 10 | import dateutil.parser 11 | from threading import Timer 12 | import uuid 13 | 14 | 15 | class Alerts(object): 16 | ''' 17 | Alert类 18 | ''' 19 | STATES = {'IDLE', 'FOREGROUND', 'BACKGROUND'} 20 | 21 | def __init__(self, dueros, player): 22 | ''' 23 | 类初始化 24 | :param dueros:DuerOS核心处理模块 25 | :param player: 播放器 26 | ''' 27 | self.namespace = 'ai.dueros.device_interface.alerts' 28 | self.dueros = dueros 29 | self.player = player 30 | 31 | self.player.add_callback('eos', self.stop) 32 | self.player.add_callback('error', self.stop) 33 | 34 | alarm = os.path.realpath(os.path.join(os.path.dirname(__file__), '../resources/alarm.wav')) 35 | self.alarm_uri = 'file://{}'.format(alarm) 36 | 37 | self.all_alerts = {} 38 | self.active_alerts = {} 39 | 40 | def stop(self): 41 | """ 42 | 停止所有激活的Alert 43 | """ 44 | for token in self.active_alerts.keys(): 45 | self.__alert_stopped(token) 46 | 47 | self.active_alerts = {} 48 | 49 | def set_alert(self, directive): 50 | ''' 51 | 设置闹钟(云端directive name方法) 52 | :param directive:云端下发的directive 53 | :return: 54 | ''' 55 | payload = directive['payload'] 56 | token = payload['token'] 57 | scheduled_time = dateutil.parser.parse(payload['scheduledTime']) 58 | 59 | # Update the alert 60 | if token in self.all_alerts: 61 | pass 62 | 63 | self.all_alerts[token] = payload 64 | 65 | interval = scheduled_time - datetime.datetime.now(scheduled_time.tzinfo) 66 | Timer(interval.seconds, self.__start_alert, (token,)).start() 67 | 68 | self.__set_alert_succeeded(token) 69 | 70 | def delete_alert(self, directive): 71 | ''' 72 | 删除闹钟(云端directive name方法) 73 | :param directive: 云端下发的directive 74 | :return: 75 | ''' 76 | token = directive['payload']['token'] 77 | 78 | if token in self.active_alerts: 79 | self.__alert_stopped(token) 80 | 81 | if token in self.all_alerts: 82 | del self.all_alerts[token] 83 | 84 | self.__delete_alert_succeeded(token) 85 | 86 | def __start_alert(self, token): 87 | ''' 88 | 开始响铃 89 | :param self: 90 | :param token: 91 | :return: 92 | ''' 93 | if token in self.all_alerts: 94 | self.__alert_started(token) 95 | 96 | # TODO: repeat play alarm until user stops it or timeout 97 | self.player.play(self.alarm_uri) 98 | 99 | # { 100 | # "directive": { 101 | # "header": { 102 | # "namespace": "Alerts", 103 | # "name": "SetAlert", 104 | # "messageId": "{{STRING}}", 105 | # "dialogRequestId": "{{STRING}}" 106 | # }, 107 | # "payload": { 108 | # "token": "{{STRING}}", 109 | # "type": "{{STRING}}", 110 | # "scheduledTime": "2017-08-07T09:02:58+0000", 111 | # } 112 | # } 113 | # } 114 | 115 | def __set_alert_succeeded(self, token): 116 | ''' 117 | 闹铃设置成功事件上报 118 | :param token: 119 | :return: 120 | ''' 121 | event = { 122 | "header": { 123 | "namespace": self.namespace, 124 | "name": "SetAlertSucceeded", 125 | "messageId": uuid.uuid4().hex 126 | }, 127 | "payload": { 128 | "token": token 129 | } 130 | } 131 | 132 | self.dueros.send_event(event) 133 | 134 | def __set_alert_failed(self, token): 135 | ''' 136 | 闹铃设置失败事件上报 137 | :param token: 138 | :return: 139 | ''' 140 | event = { 141 | "header": { 142 | "namespace": self.namespace, 143 | "name": "SetAlertFailed", 144 | "messageId": uuid.uuid4().hex 145 | }, 146 | "payload": { 147 | "token": token 148 | } 149 | } 150 | 151 | self.dueros.send_event(event) 152 | 153 | # { 154 | # "directive": { 155 | # "header": { 156 | # "namespace": "Alerts", 157 | # "name": "DeleteAlert", 158 | # "messageId": "{{STRING}}", 159 | # "dialogRequestId": "{{STRING}}" 160 | # }, 161 | # "payload": { 162 | # "token": "{{STRING}}" 163 | # } 164 | # } 165 | # } 166 | 167 | def __delete_alert_succeeded(self, token): 168 | ''' 169 | 删除闹铃成功事件上报 170 | :param token: 171 | :return: 172 | ''' 173 | event = { 174 | "header": { 175 | "namespace": self.namespace, 176 | "name": "DeleteAlertSucceeded", 177 | "messageId": uuid.uuid4().hex 178 | }, 179 | "payload": { 180 | "token": token 181 | } 182 | } 183 | 184 | self.dueros.send_event(event) 185 | 186 | def __delete_alert_failed(self, token): 187 | ''' 188 | 删除闹铃失败事件上传 189 | :param token: 190 | :return: 191 | ''' 192 | event = { 193 | "header": { 194 | "namespace": self.namespace, 195 | "name": "DeleteAlertFailed", 196 | "messageId": uuid.uuid4().hex 197 | }, 198 | "payload": { 199 | "token": token 200 | } 201 | } 202 | 203 | self.dueros.send_event(event) 204 | 205 | def __alert_started(self, token): 206 | ''' 207 | 响铃开始事件上报 208 | :param token: 209 | :return: 210 | ''' 211 | self.active_alerts[token] = self.all_alerts[token] 212 | 213 | event = { 214 | "header": { 215 | "namespace": self.namespace, 216 | "name": "AlertStarted", 217 | "messageId": uuid.uuid4().hex 218 | }, 219 | "payload": { 220 | "token": token 221 | } 222 | } 223 | 224 | self.dueros.send_event(event) 225 | 226 | def __alert_stopped(self, token): 227 | ''' 228 | 响铃结束事件上报 229 | :param token: 230 | :return: 231 | ''' 232 | if token in self.active_alerts: 233 | del self.active_alerts[token] 234 | 235 | if token in self.all_alerts: 236 | del self.all_alerts[token] 237 | 238 | event = { 239 | "header": { 240 | "namespace": self.namespace, 241 | "name": "AlertStopped", 242 | "messageId": "{STRING}" 243 | }, 244 | "payload": { 245 | "token": token 246 | } 247 | } 248 | 249 | self.dueros.send_event(event) 250 | 251 | def __alert_entered_foreground(self, token): 252 | ''' 253 | 响铃进入前台事件上报 254 | :param token: 255 | :return: 256 | ''' 257 | event = { 258 | "header": { 259 | "namespace": self.namespace, 260 | "name": "AlertEnteredForeground", 261 | "messageId": uuid.uuid4().hex 262 | }, 263 | "payload": { 264 | "token": token 265 | } 266 | } 267 | 268 | self.dueros.send_event(event) 269 | 270 | def __alert_entered_background(self, token): 271 | ''' 272 | 响铃进入后台事件上报 273 | :param token: 274 | :return: 275 | ''' 276 | event = { 277 | "header": { 278 | "namespace": self.namespace, 279 | "name": "AlertEnteredBackground", 280 | "messageId": uuid.uuid4().hex 281 | }, 282 | "payload": { 283 | "token": token 284 | } 285 | } 286 | 287 | self.dueros.send_event(event) 288 | 289 | @property 290 | def context(self): 291 | ''' 292 | 获取模块上下文(模块状态) 293 | :return: 294 | ''' 295 | return { 296 | "header": { 297 | "namespace": self.namespace, 298 | "name": "AlertsState" 299 | }, 300 | "payload": { 301 | "allAlerts": list(self.all_alerts.values()), 302 | "activeAlerts": list(self.active_alerts.values()) 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /sdk/interface/audio_player.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 音乐播放模块 5 | 参考:http://open.duer.baidu.com/doc/dueros-conversational-service/device-interface/audio-player_markdown 6 | """ 7 | 8 | import os 9 | import tempfile 10 | import uuid 11 | 12 | 13 | class AudioPlayer(object): 14 | ''' 15 | 音乐播放类 16 | ''' 17 | STATES = {'IDLE', 'PLAYING', 'STOPPED', 'PAUSED', 'BUFFER_UNDERRUN', 'FINISHED'} 18 | 19 | def __init__(self, dueros, player): 20 | ''' 21 | 类初始化 22 | :param dueros:DuerOS核心模块实例 23 | :param player: 播放器实例(平台相关) 24 | ''' 25 | self.namespace = 'ai.dueros.device_interface.audio_player' 26 | self.dueros = dueros 27 | self.token = '' 28 | self.state = 'IDLE' 29 | 30 | self.player = player 31 | self.player.add_callback('eos', self.__playback_finished) 32 | self.player.add_callback('error', self.__playback_failed) 33 | 34 | # { 35 | # "directive": { 36 | # "header": { 37 | # "namespace": "AudioPlayer", 38 | # "name": "Play", 39 | # "messageId": "{{STRING}}", 40 | # "dialogRequestId": "{{STRING}}" 41 | # }, 42 | # "payload": { 43 | # "playBehavior": "{{STRING}}", 44 | # "audioItem": { 45 | # "audioItemId": "{{STRING}}", 46 | # "stream": { 47 | # "url": "{{STRING}}", 48 | # "streamFormat": "AUDIO_MPEG" 49 | # "offsetInMilliseconds": {{LONG}}, 50 | # "expiryTime": "{{STRING}}", 51 | # "progressReport": { 52 | # "progressReportDelayInMilliseconds": {{LONG}}, 53 | # "progressReportIntervalInMilliseconds": {{LONG}} 54 | # }, 55 | # "token": "{{STRING}}", 56 | # "expectedPreviousToken": "{{STRING}}" 57 | # } 58 | # } 59 | # } 60 | # } 61 | # } 62 | def pause(self): 63 | ''' 64 | 暂停播放 65 | :return: 66 | ''' 67 | self.player.pause() 68 | self.__playback_paused() 69 | 70 | def resume(self): 71 | ''' 72 | 恢复播放 73 | :return: 74 | ''' 75 | self.player.resume() 76 | self.__playback_resumed() 77 | 78 | def play(self, directive): 79 | ''' 80 | 播放(云端directive name方法) 81 | :param directive:云端下发directive 82 | :return: 83 | ''' 84 | behavior = directive['payload']['playBehavior'] 85 | self.token = directive['payload']['audioItem']['stream']['token'] 86 | audio_url = directive['payload']['audioItem']['stream']['url'] 87 | if audio_url.startswith('cid:'): 88 | mp3_file = os.path.join(tempfile.gettempdir(), audio_url[4:] + '.mp3') 89 | if os.path.isfile(mp3_file): 90 | # os.system('mpv "{}"'.format(mp3_file)) 91 | # os.system('rm -rf "{}"'.format(mp3_file)) 92 | self.player.play('file://{}'.format(mp3_file)) 93 | self.__playback_started() 94 | else: 95 | # os.system('mpv {}'.format(audio_url)) 96 | self.player.play(audio_url) 97 | self.__playback_started() 98 | 99 | def stop(self, directive): 100 | ''' 101 | 停止(云端directive name方法) 102 | :param directive: 云端下发directive 103 | :return: 104 | ''' 105 | self.player.stop() 106 | self.__playback_stopped() 107 | 108 | def clear_queue(self, directive): 109 | ''' 110 | 播放队列清除(云端directive name方法) 111 | :param directive: 云端下发directive 112 | :return: 113 | ''' 114 | self.__playback_queue_cleared() 115 | behavior = directive['payload']['clearBehavior'] 116 | if behavior == 'CLEAR_ALL': 117 | self.player.stop() 118 | elif behavior == 'CLEAR_ENQUEUED': 119 | pass 120 | 121 | def __playback_started(self): 122 | ''' 123 | 开始播放状态上报 124 | :return: 125 | ''' 126 | self.state = 'PLAYING' 127 | 128 | event = { 129 | "header": { 130 | "namespace": self.namespace, 131 | "name": "PlaybackStarted", 132 | "messageId": uuid.uuid4().hex 133 | }, 134 | "payload": { 135 | "token": self.token, 136 | "offsetInMilliseconds": self.player.position 137 | } 138 | } 139 | self.dueros.send_event(event) 140 | 141 | def __playback_nearly_finished(self): 142 | ''' 143 | 播放即将结束状态上报 144 | :return: 145 | ''' 146 | event = { 147 | "header": { 148 | "namespace": self.namespace, 149 | "name": "PlaybackNearlyFinished", 150 | "messageId": uuid.uuid4().hex 151 | }, 152 | "payload": { 153 | "token": self.token, 154 | "offsetInMilliseconds": self.player.position 155 | } 156 | } 157 | self.dueros.send_event(event) 158 | 159 | def __playback_finished(self): 160 | ''' 161 | 播放结束事件上报 162 | :return: 163 | ''' 164 | self.state = 'FINISHED' 165 | 166 | event = { 167 | "header": { 168 | "namespace": self.namespace, 169 | "name": "PlaybackFinished", 170 | "messageId": uuid.uuid4().hex 171 | }, 172 | "payload": { 173 | "token": self.token, 174 | "offsetInMilliseconds": self.player.position 175 | } 176 | } 177 | self.dueros.send_event(event) 178 | 179 | def __playback_failed(self): 180 | ''' 181 | 播放失败 182 | :return: 183 | ''' 184 | self.state = 'STOPPED' 185 | 186 | # { 187 | # "directive": { 188 | # "header": { 189 | # "namespace": "AudioPlayer", 190 | # "name": "Stop", 191 | # "messageId": "{{STRING}}", 192 | # "dialogRequestId": "{{STRING}}" 193 | # }, 194 | # "payload": { 195 | # } 196 | # } 197 | # } 198 | 199 | def __playback_stopped(self): 200 | ''' 201 | 播放结束状态上报 202 | :return: 203 | ''' 204 | self.state = 'STOPPED' 205 | event = { 206 | "header": { 207 | "namespace": self.namespace, 208 | "name": "PlaybackStopped", 209 | "messageId": uuid.uuid4().hex 210 | }, 211 | "payload": { 212 | "token": self.token, 213 | "offsetInMilliseconds": self.player.position 214 | } 215 | } 216 | self.dueros.send_event(event) 217 | 218 | def __playback_paused(self): 219 | ''' 220 | 播放暂停状态上报 221 | :return: 222 | ''' 223 | self.state = 'PAUSED' 224 | event = { 225 | "header": { 226 | "namespace": self.namespace, 227 | "name": "PlaybackPaused", 228 | "messageId": uuid.uuid4().hex 229 | }, 230 | "payload": { 231 | "token": self.token, 232 | "offsetInMilliseconds": self.player.position 233 | } 234 | } 235 | self.dueros.send_event(event) 236 | 237 | def __playback_resumed(self): 238 | ''' 239 | 播放恢复状态上报 240 | :return: 241 | ''' 242 | self.state = 'PLAYING' 243 | event = { 244 | "header": { 245 | "namespace": self.namespace, 246 | "name": "PlaybackResumed", 247 | "messageId": uuid.uuid4().hex 248 | }, 249 | "payload": { 250 | "token": self.token, 251 | "offsetInMilliseconds": self.player.position 252 | } 253 | } 254 | self.dueros.send_event(event) 255 | 256 | # { 257 | # "directive": { 258 | # "header": { 259 | # "namespace": "AudioPlayer", 260 | # "name": "ClearQueue", 261 | # "messageId": "{{STRING}}", 262 | # "dialogRequestId": "{{STRING}}" 263 | # }, 264 | # "payload": { 265 | # "clearBehavior": "{{STRING}}" 266 | # } 267 | # } 268 | # } 269 | 270 | def __playback_queue_cleared(self): 271 | ''' 272 | 播放队列数据清除事件上报 273 | :return: 274 | ''' 275 | event = { 276 | "header": { 277 | "namespace": self.namespace, 278 | "name": "PlaybackQueueCleared", 279 | "messageId": uuid.uuid4().hex 280 | }, 281 | "payload": {} 282 | } 283 | self.dueros.send_event(event) 284 | 285 | def __progress_report_delay_elapsed(self): 286 | ''' 287 | ProgressReportDelayElapsed事件上报 288 | :return: 289 | ''' 290 | pass 291 | 292 | def __progress_report_interval_elapsed(self): 293 | ''' 294 | ProgressReportIntervalElapsed事件上报 295 | :return: 296 | ''' 297 | pass 298 | 299 | def __playback_stutter_started(self): 300 | ''' 301 | PlaybackStutterStarted事件上报 302 | :return: 303 | ''' 304 | pass 305 | 306 | def __playback_stutter_finished(self): 307 | ''' 308 | PlaybackStutterFinished事件上报 309 | :return: 310 | ''' 311 | pass 312 | 313 | def __stream_metadata_extracted(self): 314 | ''' 315 | StreamMetadataExtracted事件上报 316 | :return: 317 | ''' 318 | pass 319 | 320 | @property 321 | def context(self): 322 | ''' 323 | 获取模块上下文(模块状态) 324 | :return: 325 | ''' 326 | if self.state != 'PLAYING': 327 | offset = 0 328 | else: 329 | offset = self.player.position 330 | 331 | return { 332 | "header": { 333 | "namespace": self.namespace, 334 | "name": "PlaybackState" 335 | }, 336 | "payload": { 337 | "token": self.token, 338 | "offsetInMilliseconds": offset, 339 | "playerActivity": self.state 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /sdk/interface/notifications.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/sdk/interface/notifications.py -------------------------------------------------------------------------------- /sdk/interface/playback_controller.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/sdk/interface/playback_controller.py -------------------------------------------------------------------------------- /sdk/interface/screen_display.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/sdk/interface/screen_display.py -------------------------------------------------------------------------------- /sdk/interface/speaker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 扬声器控制模块 4 | 参考:http://open.duer.baidu.com/doc/dueros-conversational-service/device-interface/speaker-controller_markdown 5 | ''' 6 | 7 | 8 | class Speaker(object): 9 | ''' 10 | 扬声器控制类 11 | ''' 12 | 13 | def __init__(self, event_queue): 14 | ''' 15 | 类初始化 16 | :param event_queue: 17 | ''' 18 | self.namespace = 'ai.dueros.device_interface.speaker_controller' 19 | 20 | def set_volume(self): 21 | ''' 22 | 音量设置(云端directive name方法) 23 | :return: 24 | ''' 25 | pass 26 | 27 | def adjust_volume(self): 28 | ''' 29 | 音量调整(云端directive name方法) 30 | :return: 31 | ''' 32 | pass 33 | 34 | def set_mute(self): 35 | ''' 36 | 设置静音(云端directive name方法) 37 | :return: 38 | ''' 39 | pass 40 | 41 | def __volume_changed(self): 42 | ''' 43 | 音量改变事件上报 44 | :return: 45 | ''' 46 | pass 47 | 48 | def __mute_changed(self): 49 | ''' 50 | 静音状态改变事件上报 51 | :return: 52 | ''' 53 | pass 54 | 55 | @property 56 | def context(self): 57 | ''' 58 | 获取模块上下文(模块状态) 59 | :return: 60 | ''' 61 | return { 62 | "header": { 63 | "namespace": self.namespace, 64 | "name": "VolumeState" 65 | }, 66 | "payload": { 67 | "volume": 50, 68 | "muted": False 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sdk/interface/speech_recognizer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 语音识别功能模块(语音输入) 5 | 参考:http://open.duer.baidu.com/doc/dueros-conversational-service/device-interface/voice-input_markdown 6 | """ 7 | 8 | import logging 9 | import uuid 10 | 11 | try: 12 | import Queue as queue 13 | except ImportError: 14 | import queue 15 | 16 | import sdk.sdk_config as sdk_config 17 | 18 | logging.basicConfig(level=sdk_config.LOGGER_LEVEL) 19 | logger = logging.getLogger('SpeechRecognizer') 20 | 21 | 22 | class SpeechRecognizer(object): 23 | ''' 24 | 语音识别类(语音输入) 25 | ''' 26 | STATES = {'IDLE', 'RECOGNIZING', 'BUSY', 'EXPECTING SPEECH'} 27 | PROFILES = {'CLOSE_TALK', 'NEAR_FIELD', 'FAR_FIELD'} 28 | PRESS_AND_HOLD = {'type': 'PRESS_AND_HOLD', 'payload': {}} 29 | TAP = {'type': 'TAP', 'payload': {}} 30 | 31 | def __init__(self, dueros): 32 | ''' 33 | 类初始化 34 | :param dueros:DuerOS核心实现模块实例 35 | ''' 36 | self.namespace = 'ai.dueros.device_interface.voice_input' 37 | self.dueros = dueros 38 | self.profile = 'NEAR_FIELD' 39 | 40 | self.dialog_request_id = '' 41 | 42 | self.listening = False 43 | self.audio_queue = queue.Queue() 44 | 45 | def put(self, audio): 46 | """ 47 | 语音pcm输入 48 | :param audio: S16_LE format, sample rate 16000 bps audio data 49 | :return: None 50 | """ 51 | if self.listening: 52 | self.audio_queue.put(audio) 53 | 54 | def recognize(self, dialog=None, timeout=10000): 55 | """ 56 | 语音识别 57 | :param dialog:会话ID 58 | :param timeout:超时时间(单位毫秒) 59 | :return: 60 | """ 61 | 62 | if self.listening: 63 | return 64 | 65 | self.audio_queue.queue.clear() 66 | self.listening = True 67 | 68 | self.dueros.state_listener.on_listening() 69 | 70 | def on_finished(): 71 | self.dueros.state_listener.on_finished() 72 | 73 | if self.dueros.audio_player.state == 'PAUSED': 74 | self.dueros.audio_player.resume() 75 | 76 | # Stop playing if Xiaoduxiaodu is speaking or AudioPlayer is playing 77 | if self.dueros.speech_synthesizer.state == 'PLAYING': 78 | self.dueros.speech_synthesizer.stop() 79 | elif self.dueros.audio_player.state == 'PLAYING': 80 | self.dueros.audio_player.pause() 81 | 82 | self.dialog_request_id = dialog if dialog else uuid.uuid4().hex 83 | 84 | event = { 85 | "header": { 86 | "namespace": self.namespace, 87 | "name": "ListenStarted", 88 | "messageId": uuid.uuid4().hex, 89 | "dialogRequestId": self.dialog_request_id 90 | }, 91 | "payload": { 92 | "profile": self.profile, 93 | "format": "AUDIO_L16_RATE_16000_CHANNELS_1", 94 | } 95 | } 96 | 97 | def gen(): 98 | time_elapsed = 0 99 | while self.listening or time_elapsed >= timeout: 100 | try: 101 | chunk = self.audio_queue.get(timeout=1.0) 102 | except queue.Empty: 103 | break 104 | 105 | yield chunk 106 | time_elapsed += 10 # 10 ms chunk 107 | 108 | self.listening = False 109 | self.dueros.state_listener.on_thinking() 110 | 111 | self.dueros.send_event(event, listener=on_finished, attachment=gen()) 112 | 113 | # { 114 | # "directive": { 115 | # "header": { 116 | # "namespace": "SpeechRecognizer", 117 | # "name": "StopCapture", 118 | # "messageId": "{{STRING}}", 119 | # "dialogRequestId": "{{STRING}}" 120 | # }, 121 | # "payload": { 122 | # } 123 | # } 124 | # } 125 | def stop_listen(self, directive): 126 | ''' 127 | 停止录音监听(云端directive name方法) 128 | :param directive: 云端下发的directive 129 | :return: 130 | ''' 131 | self.listening = False 132 | logger.info('StopCapture') 133 | 134 | # { 135 | # "directive": { 136 | # "header": { 137 | # "namespace": "SpeechRecognizer", 138 | # "name": "ExpectSpeech", 139 | # "messageId": "{{STRING}}", 140 | # "dialogRequestId": "{{STRING}}" 141 | # }, 142 | # "payload": { 143 | # "timeoutInMilliseconds": {{LONG}}, 144 | # "initiator": "{{STRING}}" 145 | # } 146 | # } 147 | # } 148 | def listen(self, directive): 149 | ''' 150 | 启动录音监听(云端directive name方法) 151 | :param directive: 云端下发的directive 152 | :return: 153 | ''' 154 | dialog = directive['header']['dialogRequestId'] 155 | timeout = directive['payload']['timeoutInMilliseconds'] 156 | 157 | self.recognize(dialog=dialog, timeout=timeout) 158 | 159 | def expect_speech_timeout(self): 160 | ''' 161 | 超时时间上报 162 | :return: 163 | ''' 164 | event = { 165 | "header": { 166 | "namespace": self.namespace, 167 | "name": "ExpectSpeechTimedOut", 168 | "messageId": uuid.uuid4().hex, 169 | }, 170 | "payload": {} 171 | } 172 | self.dueros.send_event(event) 173 | 174 | @property 175 | def context(self): 176 | ''' 177 | 获取模块上下文(模块状态) 178 | :return: 179 | ''' 180 | return { 181 | "header": { 182 | "namespace": self.namespace, 183 | "name": "ListenStarted" 184 | }, 185 | "payload": { 186 | "wakeword": "xiaoduxiaodu" 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /sdk/interface/speech_synthesizer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 语音输出模块(TTS) 4 | 参考:http://open.duer.baidu.com/doc/dueros-conversational-service/device-interface/voice-output_markdown 5 | ''' 6 | import os 7 | import tempfile 8 | import threading 9 | import uuid 10 | 11 | 12 | class SpeechSynthesizer(object): 13 | ''' 14 | 语音输出模块功能类 15 | ''' 16 | STATES = {'PLAYING', 'FINISHED'} 17 | 18 | def __init__(self, dueros, player): 19 | ''' 20 | 类初始化 21 | :param dueros:DuerOS核心模块实例 22 | :param player: 播放器实例 23 | ''' 24 | self.namespace = 'ai.dueros.device_interface.voice_output' 25 | self.dueros = dueros 26 | self.token = '' 27 | self.state = 'FINISHED' 28 | self.finished = threading.Event() 29 | 30 | self.player = player 31 | self.player.add_callback('eos', self.__speech_finished) 32 | self.player.add_callback('error', self.__speech_finished) 33 | 34 | def stop(self): 35 | ''' 36 | 停止播放 37 | :return: 38 | ''' 39 | self.finished.set() 40 | self.player.stop() 41 | self.state = 'FINISHED' 42 | 43 | # { 44 | # "directive": { 45 | # "header": { 46 | # "namespace": "SpeechSynthesizer", 47 | # "name": "Speak", 48 | # "messageId": "{{STRING}}", 49 | # "dialogRequestId": "{{STRING}}" 50 | # }, 51 | # "payload": { 52 | # "url": "{{STRING}}", 53 | # "format": "AUDIO_MPEG", 54 | # "token": "{{STRING}}" 55 | # } 56 | # } 57 | # } 58 | # Content-Type: application/octet-stream 59 | # Content-ID: {{Audio Item CID}} 60 | # {{BINARY AUDIO ATTACHMENT}} 61 | def speak(self, directive): 62 | ''' 63 | 播放TTS(云端directive name方法) 64 | :param directive: 云端下发directive 65 | :return: 66 | ''' 67 | # directive from dueros may not have the dialogRequestId 68 | if 'dialogRequestId' in directive['header']: 69 | dialog_request_id = directive['header']['dialogRequestId'] 70 | if self.dueros.speech_recognizer.dialog_request_id != dialog_request_id: 71 | return 72 | 73 | self.token = directive['payload']['token'] 74 | url = directive['payload']['url'] 75 | if url.startswith('cid:'): 76 | mp3_file = os.path.join(tempfile.gettempdir(), url[4:] + '.mp3') 77 | if os.path.isfile(mp3_file): 78 | self.finished.clear() 79 | # os.system('mpv "{}"'.format(mp3_file)) 80 | self.player.play('file://{}'.format(mp3_file)) 81 | self.__speech_started() 82 | 83 | self.dueros.state_listener.on_speaking() 84 | 85 | # will be set at SpeechFinished() if the player reaches the End Of Stream or gets a error 86 | self.finished.wait() 87 | 88 | os.system('rm -rf "{}"'.format(mp3_file)) 89 | 90 | def __speech_started(self): 91 | ''' 92 | speech开始状态上报 93 | :return: 94 | ''' 95 | self.state = 'PLAYING' 96 | event = { 97 | "header": { 98 | "namespace": self.namespace, 99 | "name": "SpeechStarted", 100 | "messageId": uuid.uuid4().hex 101 | }, 102 | "payload": { 103 | "token": self.token 104 | } 105 | } 106 | self.dueros.send_event(event) 107 | 108 | def __speech_finished(self): 109 | self.dueros.state_listener.on_finished() 110 | 111 | self.finished.set() 112 | self.state = 'FINISHED' 113 | event = { 114 | "header": { 115 | "namespace": self.namespace, 116 | "name": "SpeechFinished", 117 | "messageId": uuid.uuid4().hex 118 | }, 119 | "payload": { 120 | "token": self.token 121 | } 122 | } 123 | self.dueros.send_event(event) 124 | 125 | @property 126 | def context(self): 127 | ''' 128 | 获取模块上下文(模块状态) 129 | :return: 130 | ''' 131 | offset = self.player.position if self.state == 'PLAYING' else 0 132 | 133 | return { 134 | "header": { 135 | "namespace": self.namespace, 136 | "name": "SpeechState" 137 | }, 138 | "payload": { 139 | "token": self.token, 140 | "offsetInMilliseconds": offset, 141 | "playerActivity": self.state 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /sdk/interface/system.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 系统模块 4 | 参考:http://open.duer.baidu.com/doc/dueros-conversational-service/device-interface/system_markdown 5 | ''' 6 | import uuid 7 | import datetime 8 | 9 | 10 | class System(object): 11 | ''' 12 | 系统控制类 13 | ''' 14 | 15 | def __init__(self, dueros): 16 | ''' 17 | 类初始化 18 | :param dueros:DuerOS核心模块实例 19 | ''' 20 | self.namespace = 'ai.dueros.device_interface.system' 21 | self.dueros = dueros 22 | 23 | def reset_user_inactivity(self, directive): 24 | ''' 25 | 重置“用户最近一次交互”的时间点为当前时间(云端directive name方法) 26 | :param directive:云端下发directive 27 | :return: 28 | ''' 29 | self.dueros.last_activity = datetime.datetime.utcnow() 30 | 31 | # { 32 | # "directive": { 33 | # "header": { 34 | # "namespace": "System", 35 | # "name": "SetEndpoint", 36 | # "messageId": "{{STRING}}" 37 | # }, 38 | # "payload": { 39 | # "endpoint": "{{STRING}}" 40 | # } 41 | # } 42 | # } 43 | 44 | def set_endpoint(self, directive): 45 | ''' 46 | 设置服务端接入地址,重置连接(云端directive name方法) 47 | :param directive:云端下发directive 48 | :return: 49 | ''' 50 | pass 51 | 52 | def throw_exception(self, directive): 53 | ''' 54 | 当设备端发送的请求格式不正确、登录的认证信息过期等错误情况发生时,服务端会返回ThrowException指令给设备端 55 | (云端directive name方法) 56 | :param directive: 云端下发directive 57 | :return: 58 | ''' 59 | pass 60 | 61 | def synchronize_state(self): 62 | ''' 63 | SynchronizeState状态上报(dueros_core中会使用) 64 | :return: 65 | ''' 66 | event = { 67 | "header": { 68 | "namespace": self.namespace, 69 | "name": "SynchronizeState", 70 | "messageId": uuid.uuid4().hex 71 | }, 72 | "payload": { 73 | } 74 | } 75 | 76 | self.dueros.send_event(event) 77 | 78 | def __user_Inactivity_report(self): 79 | ''' 80 | UserInactivityReport状态上报 81 | :return: 82 | ''' 83 | inactive_time = datetime.datetime.utcnow() - self.dueros.last_activity 84 | 85 | event = { 86 | "header": { 87 | "namespace": self.namespace, 88 | "name": "UserInactivityReport", 89 | "messageId": uuid.uuid4().hex 90 | }, 91 | "payload": { 92 | "inactiveTimeInSeconds": inactive_time.seconds 93 | } 94 | 95 | } 96 | 97 | self.dueros.send_event(event) 98 | 99 | # { 100 | # "directive": { 101 | # "header": { 102 | # "namespace": "System", 103 | # "name": "ResetUserInactivity", 104 | # "messageId": "{{STRING}}" 105 | # }, 106 | # "payload": { 107 | # } 108 | # } 109 | # } 110 | 111 | 112 | def __exception_encountered(self): 113 | ''' 114 | ExceptionEncountered状态上报 115 | :return: 116 | ''' 117 | event = { 118 | "header": { 119 | "namespace": self.namespace, 120 | "name": "ExceptionEncountered", 121 | "messageId": "{{STRING}}" 122 | }, 123 | "payload": { 124 | "unparsedDirective": "{{STRING}}", 125 | "error": { 126 | "type": "{{STRING}}", 127 | "message": "{{STRING}}" 128 | } 129 | } 130 | } 131 | self.dueros.send_event(event) 132 | -------------------------------------------------------------------------------- /sdk/resources/README.md: -------------------------------------------------------------------------------- 1 | 2 | Resources from https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/content/alexa-voice-service-ux-design-guidelines 3 | -------------------------------------------------------------------------------- /sdk/resources/alarm.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MyDuerOS/DuerOS-Python-Client/71b3482f00cfb11b6d6d8a33065cb33e05ba339e/sdk/resources/alarm.wav -------------------------------------------------------------------------------- /sdk/sdk_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | sdk配置 4 | ''' 5 | import logging 6 | 7 | # sdk打印log等级 8 | LOGGER_LEVEL = logging.INFO 9 | 10 | # snowboy唤醒词灵敏度设置 11 | SNOWBOAY_SENSITIVITY = '0.35, 0.35, 0.45' 12 | -------------------------------------------------------------------------------- /wakeup_trigger_start.sh: -------------------------------------------------------------------------------- 1 | # 通过"小度小度"来触发唤醒 2 | WORK_PATH="${PWD}" 3 | export PYTHONPATH=${WORK_PATH}:${PYTHONPATH} 4 | 5 | python ./app/wakeup_trigger_main.py 6 | --------------------------------------------------------------------------------