├── .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 | 
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 |
--------------------------------------------------------------------------------