├── examples └── raspi_assistant │ ├── __init__.py │ ├── assistant.py │ ├── test.py │ ├── settings.py.example │ ├── README.md │ ├── utils.py │ └── handler.py ├── voicetools ├── __init__.py ├── exceptions.py ├── utils.py ├── constants.py ├── clients.py └── api.py ├── README.md ├── tests └── test_GPIO.py ├── .gitignore ├── setup.py └── LICENSE /examples/raspi_assistant/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /voicetools/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | voicetools library 4 | ===================== 5 | 6 | 7 | """ 8 | __title__ = 'voicetools' 9 | __version__ = '0.0.1' 10 | __author__ = 'namco1992' 11 | __license__ = 'Apache 2.0' 12 | 13 | from .api import Wolfram, TuringRobot, BaiduVoice 14 | from .clients import BaseClient 15 | from . import utils 16 | from .exceptions import ( 17 | APIError, RespError, RecognitionError, VerifyError, QuotaError) 18 | -------------------------------------------------------------------------------- /voicetools/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from requests import RequestException 3 | 4 | 5 | class RespError(RequestException): 6 | """The response status code is not 200""" 7 | 8 | 9 | class APIError(RequestException): 10 | """Third-party API error occured.""" 11 | 12 | 13 | class RecognitionError(APIError): 14 | """The request for Baidu Voice ASR API failed.""" 15 | 16 | 17 | class VerifyError(APIError): 18 | """Auth failed.""" 19 | 20 | 21 | class QuotaError(APIError): 22 | """The number of requests exceeds the quota.""" 23 | -------------------------------------------------------------------------------- /voicetools/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import uuid 3 | import wave 4 | import urllib 5 | 6 | 7 | def get_mac_address(): 8 | mac = uuid.UUID(int=uuid.getnode()).hex[-12:] 9 | return ":".join([mac[e:e+2] for e in range(0, 11, 2)]) 10 | 11 | 12 | def get_audio_info(file_): 13 | try: 14 | f = wave.open(file_, 'rb') 15 | params = f.getparams() 16 | audio_info = {} 17 | audio_info['nchannels'], audio_info['sampwidth'], audio_info['framerate'], audio_info['nframes']\ 18 | = params[:4] 19 | audio_info['content'] = f.readframes(audio_info['nframes']) 20 | f.close() 21 | except Exception, e: 22 | raise e 23 | return audio_info 24 | 25 | 26 | def concat_url(url, params): 27 | return url + '?' + urllib.urlencode(params) 28 | 29 | 30 | if __name__ == '__main__': 31 | pass 32 | -------------------------------------------------------------------------------- /voicetools/constants.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from .exceptions import RecognitionError, VerifyError, QuotaError, APIError 3 | 4 | 5 | class BaiduUrl(object): 6 | token_url = 'https://openapi.baidu.com/oauth/2.0/token' 7 | tts_url = 'http://tsn.baidu.com/text2audio' 8 | asr_url = 'http://vop.baidu.com/server_api' 9 | baike_url = 'http://baike.baidu.com/search/word' 10 | 11 | 12 | # turing robot config 13 | class TuringUrl(object): 14 | turing_url = 'http://www.tuling123.com/openapi/api' 15 | 16 | 17 | # error no 18 | class ErrNo(object): 19 | tts_err_no = { 20 | 500: ValueError, 21 | 501: ValueError, 22 | 502: VerifyError, 23 | 503: APIError 24 | } 25 | asr_err_no = { 26 | 3300: ValueError, 27 | 3301: RecognitionError, 28 | 3302: VerifyError, 29 | 3303: APIError, 30 | 3304: QuotaError, 31 | 3305: QuotaError 32 | } 33 | -------------------------------------------------------------------------------- /examples/raspi_assistant/assistant.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | import sys 4 | import time 5 | 6 | import RPi.GPIO as GPIO 7 | 8 | HOME = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 9 | sys.path.append(HOME) 10 | 11 | from raspi_assistant.settings import GPIOConfig 12 | from raspi_assistant.utils import init_logging_handler 13 | from raspi_assistant.handler import BaseHandler 14 | 15 | logger = init_logging_handler() 16 | 17 | 18 | def set_GPIO(): 19 | GPIO.setmode(GPIO.BCM) 20 | set_voice_sensor() 21 | 22 | 23 | def set_voice_sensor(): 24 | GPIO.setup(GPIOConfig.VOICE_SENSOR, GPIO.IN, pull_up_down=GPIO.PUD_UP) 25 | GPIO.add_event_detect(GPIOConfig.VOICE_SENSOR, GPIO.FALLING) 26 | 27 | 28 | def loop(): 29 | # 初始化 30 | handler = BaseHandler() 31 | try: 32 | while True: 33 | # 下降沿检测 34 | if GPIO.event_detected(GPIOConfig.VOICE_SENSOR): 35 | GPIO.remove_event_detect(GPIOConfig.VOICE_SENSOR) 36 | handler.worker() 37 | GPIO.add_event_detect(GPIOConfig.VOICE_SENSOR, GPIO.FALLING) 38 | time.sleep(0.5) 39 | except KeyboardInterrupt: 40 | pass 41 | GPIO.cleanup() 42 | 43 | if __name__ == '__main__': 44 | set_GPIO() 45 | loop() 46 | -------------------------------------------------------------------------------- /examples/raspi_assistant/test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | import sys 4 | import wave 5 | import pyaudio 6 | from io import BytesIO 7 | HOME = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | sys.path.append(HOME) 9 | 10 | from raspi_assistant.utils import init_logging_handler 11 | from raspi_assistant.handler import BaseHandler, ActionHandler 12 | from tempfile import TemporaryFile 13 | 14 | 15 | def main(): 16 | logger = init_logging_handler() 17 | handler = BaseHandler('24.08be72600a465c9ea5a03bbb1615fbb2.2592000.1473492202.282335-8403190') 18 | func, result = handler.process(['今天的天气', ]) 19 | print func 20 | # content = handler.execute(func, result) 21 | # handler.feedback(content) 22 | 23 | # handler.feedback('你是谁啊你是谁') 24 | # handler.worker() 25 | # handler.audio_handler.arecord(5) 26 | # handler.audio_handler.aplay('output.wav') 27 | # audio_handler = AudioHandler() 28 | # audio_handler.record(3, f) 29 | 30 | # with open('test_record.wav', 'wb') as ff: 31 | # print f.read() 32 | # ff.write(f.read()) 33 | 34 | # with open('test_tts.wav', 'rb') as f: 35 | # audio = BytesIO(f.read()) 36 | # handler.audio_handler.play(audio) 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # voicetools 2 | voicetools: 语音识别和语音合成的基础包。 3 | ## 简介 4 | 以百度语音 API 为基础的语音识别和语音合成的基础包,同时集成了 wolfram API 和图灵机器人 API。附带树莓派语音助手示例代码。 5 | 6 | ## 用法 7 | ### 安装 8 | ```shell 9 | pip install voicetools 10 | ``` 11 | 12 | ### 语音识别及语音合成 13 | ```python 14 | from voicetools import BaiduVoice 15 | # api key 及 secret key 请在百度语音官方网站注册获取 16 | token = BaiduVoice.get_baidu_token('YOUR_VOICE_API_KEY', 'YOUR_VOICE_SECRET') # 该方法返回百度 API 返回的完整 json 17 | bv = BaiduVoice(token['access_token']) # 在上述方法获取的 json 中得到 access_token 18 | # 语音识别 19 | results = bv.asr('path/to/your/audio/file') # 返回识别结果列表,可选参数见百度语音文档 20 | # 语音合成 21 | audio = bv.tts('你好') # 返回 MP3 格式二进制数据,可选参数见百度语音文档 22 | ``` 23 | 24 | ### wolfram API 25 | wolfram 是一个功能强大的搜索引擎,可以直接返回问题的答案,而不是返回页面。 26 | 由于国内网络原因,接口稳定性差,且只支持英文搜索。 27 | ```python 28 | from voicetools import Wolfram 29 | # api key 请在 wolfram 网站注册获取 30 | robot = Wolfram('YOUR_WOLFRAM_KEY') 31 | result = robot.ask_wolfram('Who is Bill Gates?') # 返回文字信息 32 | ``` 33 | 34 | ### 图灵机器人 API 35 | 国产 AI 的 API。 36 | ```python 37 | from voicetools import TuringRobot 38 | # api key 请在图灵机器人网站注册获取 39 | robot = TuringRobot('YOUR_TURING_KEY') 40 | result = robot.ask_turing('给我讲个笑话') # 返回文字信息 41 | ``` 42 | 43 | ## 依赖 44 | - requests 45 | - wolframalpha 46 | 47 | ## 树莓派语音助手示例程序 48 | 请点击[这里](https://github.com/namco1992/voicetools/tree/master/examples/raspi_assistant) 49 | -------------------------------------------------------------------------------- /tests/test_GPIO.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import os 3 | import sys 4 | import logging 5 | 6 | HOME = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | print HOME 8 | sys.path.append(HOME) 9 | 10 | from raspi_assistant import BaiduVoice, TuringRobot, Wolfram 11 | bv = BaiduVoice('24.c732785d895d118884937d4d2321d51e.2592000.1471771233.282335-8403190') 12 | # test asr 13 | # bv.asr(file_='err_audio.wav', lan='en', ptc=2) 14 | 15 | # test tts 16 | # with open('test_tts.wav', 'wb') as f: 17 | # f.write(bv.tts('你好')) 18 | 19 | # test turing robot 20 | # robot = TuringRobot('4d639fbc3c156ce966e91191fe60c531') 21 | # print robot.ask_turing('成都天气') 22 | 23 | # test wolfram 24 | wolfram = Wolfram('QW9W43-2JJ9KK54KV') 25 | print wolfram.ask_wolfram('who is the president of United States') 26 | 27 | # import RPi.GPIO 28 | # import time 29 | 30 | # # 声音感应器OUT口连接的GPIO口 31 | # SENSOR = 4 32 | 33 | # RPi.GPIO.setmode(RPi.GPIO.BCM) 34 | 35 | # # 指定GPIO4(声音感应器的OUT口连接的GPIO口)的模式为输入模式 36 | # # 默认拉高到高电平,低电平表示OUT口有输出 37 | # RPi.GPIO.setup(SENSOR, RPi.GPIO.IN, pull_up_down=RPi.GPIO.PUD_UP) 38 | # RPi.GPIO.add_event_detect(SENSOR, RPi.GPIO.FALLING) 39 | # try: 40 | # while True: 41 | # # 检测声音感应器是否输出低电平,若是低电平,表示声音被检测到,点亮或关闭LED灯 42 | # if (RPi.GPIO.input(SENSOR) == RPi.GPIO.LOW): 43 | # print 'Tested.' 44 | # if RPi.GPIO.event_detected(SENSOR): 45 | # print 'tested' 46 | # time.sleep(0.5) 47 | # except KeyboardInterrupt: 48 | # pass 49 | 50 | # RPi.GPIO.cleanup() 51 | 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | *.log* 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # audio files 93 | *.wav 94 | *.mp3 95 | 96 | # settings 97 | settings.py -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: UTF-8 -*- 2 | import re 3 | from setuptools import setup, find_packages 4 | 5 | PACKAGE = "voicetools" 6 | NAME = 'voicetools' 7 | 8 | with open('voicetools/__init__.py', 'r') as fd: 9 | VERSION = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', 10 | fd.read(), re.MULTILINE).group(1) 11 | 12 | if not VERSION: 13 | raise RuntimeError('Cannot find version information') 14 | 15 | setup(name=NAME, 16 | version=VERSION, 17 | description="All-in-one voice tools library", 18 | long_description='All-in-one voice tools library', 19 | classifiers=( 20 | 'Development Status :: 4 - Beta', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: Apache Software License', 23 | 'Programming Language :: Python', 24 | 'Programming Language :: Python :: 2.6', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: Implementation :: CPython', 27 | 'Topic :: Multimedia :: Sound/Audio :: Sound Synthesis', 28 | 'Topic :: Multimedia :: Sound/Audio :: Speech' 29 | ), # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 30 | keywords='python baidu tts asr wolfram turing', 31 | author='namco1992', 32 | author_email='namco1992@gmail.com', 33 | url='https://github.com/namco1992/voicetools', 34 | license='MIT', 35 | packages=find_packages(), 36 | include_package_data=True, 37 | zip_safe=True, 38 | install_requires=[ 39 | 'requests', 'wolframalpha' 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /examples/raspi_assistant/settings.py.example: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import logging 3 | 4 | from voicetools import ( 5 | APIError, RespError, RecognitionError, VerifyError, QuotaError) 6 | 7 | # from . import handler 8 | # from .utils import Keyword 9 | 10 | 11 | class RedisConfig(object): 12 | """docstring for RedisConfig""" 13 | HOST_ADDR = 'localhost' 14 | PORT = 6379 15 | DB = 0 16 | SOCKET_TIMEOUT = 1 17 | 18 | 19 | class BasicConfig(object): 20 | """Basic config for the raspi_assistant.""" 21 | # Your location, only used in weather queries 22 | LOCATION = '成都' 23 | TURING_KEY = 'YOUR_TURING_KEY' 24 | VOICE_API_KEY = 'YOUR_API_KEY' 25 | VOICE_SECRET = 'YOUR_API_SECRET' 26 | HAPPINESS_THRESHOLD = 0.6 27 | KEYWORDS = {'提醒', '备忘录', '播放', '今天', '明天', '天气', '删除', '最后', '第一条'} 28 | INPUT_NAME = 'record.wav' 29 | OUTPUT_NAME = 'output.wav' 30 | POSITIVE_ANSWER = { 31 | 1: '等候多时', 32 | 2: '你回来了', 33 | 3: '你需要什么', 34 | 4: '今天工作辛苦吗', 35 | 5: '你今天又变帅了', 36 | 6: '需要我帮你做点什么?' 37 | } 38 | # The negative_answer won't negative your command, just the emotion expression 39 | NEGATIVE_ANSWER = { 40 | 1: '别吵了,真烦人', 41 | 2: '好了好了,我听见了', 42 | 3: '我今天心情不太好', 43 | 4: '我想静静,也别问我静静是谁', 44 | 5: '每天都来烦我,你什么时候给我找个男朋友' 45 | } 46 | 47 | 48 | class GPIOConfig(object): 49 | """GPIO config""" 50 | VOICE_SENSOR = 4 51 | 52 | 53 | class LogConfig(object): 54 | LOGGING_FORMAT = '%(asctime)s %(funcName)s:%(lineno)d [%(levelname)s] %(message)s' 55 | LOGGING_LOCATION = './log/raspi_assistant.log' 56 | LOGGING_LEVEL = logging.DEBUG 57 | 58 | 59 | class BaiduAPIConfig(object): 60 | """Baidu API Store is used for weather.""" 61 | API_KEY = 'YOUR_BAIDU_API_KEY' 62 | WEATHER_URL = 'http://apis.baidu.com/heweather/weather/free' 63 | TODAY_WEATHER_TEXT = \ 64 | u'当前天气{cond},体感温度{fl}摄氏度,空气湿度百分之{hum}。今日温度为{min}到{max}摄氏度,{txt_d}转{txt_n},降水概率百分之{pop},空气质量{qlty}。' 65 | 66 | TOMO_WEATHER_TEXT = \ 67 | u'明天的气温是{min}到{max}摄氏度,{txt_d}转{txt_n},降水概率百分之{pop}。' 68 | 69 | 70 | class ErrNo(object): 71 | ExceptionMap = { 72 | 3001: QuotaError, 73 | 3002: VerifyError, 74 | 3003: APIError, 75 | } 76 | -------------------------------------------------------------------------------- /examples/raspi_assistant/README.md: -------------------------------------------------------------------------------- 1 | ## 树莓派语音助手 2 | 3 | ### 简介 4 | 你的树莓派还在吃灰吗?来试试把它改造成语音助手吧!该示例程序基于 voicetools,遵循最简单的 one-in-one-out,只需要在该示例程序的基础上添加关键词和对应执行的动作,就可以扩展成为个性化的专属助手。 5 | 目前示例程序实现的功能有**语音提醒**及**今明两天天气预报查询**。 6 | 7 | ### 特性 8 | - 使用 redis 作缓存,提升语音助手的反应速度。同样的问题,第二遍不再发生网络请求。 9 | - 使用图灵机器人 API,除了预设功能外,所有问题都会有答案。 10 | - 非常简单的“receive-process-execute-feedback”逻辑,易于扩展。 11 | - 有一个心情阈值,目前是预设在配置文件中的。不一定会对你的命令言听计从。(只是为了好玩) 12 | 13 | ### 需要准备什么? 14 | - 一块树莓派 15 | - 一个麦克风 16 | - 一个扬声器 17 | - 一个传感器(用于唤醒语音助手,我使用的是声音传感器,当然任何传感器都可以) 18 | 19 | ### 使用方法 20 | 首先,安装 voicetools,可通过 pip 安装或直接通过源码安装。 21 | 22 | ```shell 23 | pip install voicetools 24 | // or 25 | git clone git@github.com:namco1992/voicetools.git 26 | ``` 27 | 28 | 安装依赖库: 29 | 30 | ```shell 31 | jieba==0.38 32 | PyAudio==0.2.9 33 | redis==2.10.5 34 | requests==2.11.0 35 | RPi.GPIO==0.6.2 36 | wolframalpha==2.4 37 | ``` 38 | 39 | 如果你的树莓派的开发环境未经配置,可参考如下步骤: 40 | 41 | ``` 42 | // python 编译环境 43 | sudo apt-get install python-dev 44 | 45 | // 用于音频转换的 ffmpeg,该方法适用于取消了 ffmpeg 源的 RASPBIAN JESSIE 46 | sudo sh -c 'echo "deb http://www.deb-multimedia.org jessie main" >> /etc/apt/sources.list' 47 | sudo apt-get update 48 | sudo apt-get install deb-multimedia-keyring 49 | sudo apt-get install ffmpeg 50 | 51 | // 音频相关 52 | sudo apt-get install libjack-jackd2-dev portaudio19-dev 53 | sudo apt-get install alsa-utils 54 | 55 | // 系统声音设置 56 | sudo modprobe snd_bcm2835 57 | 58 | // 安装并启动 redis 59 | ... 60 | ``` 61 | 62 | 硬件安装。将你的传感器接在 GPIO 上,我的信号输入是4,你可以自由修改,但是要记得修改配置文件中的信号输入端口。 63 | 64 | 参考`settings.py.example`设置你自己的配置文件`settings.py`,主要需要设置的参数如下: 65 | 66 | ```python 67 | # BasicConfig 类中 68 | LOCATION = '你的地址' # 天气预报的地区 69 | TURING_KEY = 'YOUR_TURING_KEY' # 图灵机器人 key 70 | VOICE_API_KEY = 'YOUR_API_KEY' # 百度语音 api key 71 | VOICE_SECRET = 'YOUR_API_SECRET' # 百度语音 secret key 72 | 73 | # BaiduAPIConfig 类中 74 | API_KEY = 'YOUR_BAIDU_API_KEY' # 天气预报使用了百度 APIStore 中的服务,需要百度 APIStore 的 key,你也可以选择任何你喜欢的服务提供商 75 | 76 | # GPIOConfig 类中 77 | VOICE_SENSOR = 4 # 修改成你的传感器信号输入口 78 | ``` 79 | 80 | Enjoy! 81 | 82 | ```shell 83 | // cd 到示例程序目录 84 | cd to/your/project/path 85 | //建立 log 文件夹 86 | mkdir log 87 | // 运行 88 | python assistant.py 89 | ``` 90 | 91 | ## 如何扩展 92 | 1.在配置文件的`BasicConfig`类中的关键词列表`KEYWORDS`中加入你的关键词; 93 | ```python 94 | KEYWORDS = {'提醒', '备忘录', '播放', '今天', '明天', '天气', '删除', '最后', '第一条'} 95 | ``` 96 | 2.在`handler.py`的`FUNCTION_MAP`映射中加入你的关键词与执行方法名称的映射。比如你想在识别出关键词“明天”和“天气”后,对应执行天气查询的方法,方法名称是“weather_today”: 97 | ```python 98 | FUNC_MAP = { 99 | Keyword(['今天', '天气']).value: 'weather_today' 100 | } 101 | ``` 102 | 3.在`handler.py`的`ActionHandler`中加入你需要执行的方法。 103 | ```python 104 | class ActionHandler(object): 105 | 106 | @staticmethod 107 | def your_method(base_handler, result): 108 | """ 109 | 该类中的方法均是 staticmethod。 110 | args: 111 | base_handler: `BaseHandler`实例 112 | result: 语音识别内容 113 | returns: 114 | 需要语音播放的内容或回答。 115 | """ 116 | pass 117 | ``` 118 | 4.大功告成。 119 | 120 | ## LICENCE 121 | Apache 2.0 122 | -------------------------------------------------------------------------------- /voicetools/clients.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import json 3 | import requests 4 | from .constants import BaiduUrl, TuringUrl, ErrNo 5 | from .exceptions import RespError, APIError, VerifyError 6 | from .utils import concat_url 7 | 8 | 9 | class BaseClient(object): 10 | """BaseClient for requesting APIs.""" 11 | 12 | def __init__(self, **kwargs): 13 | self.connect_timeout = kwargs.get('connect_timeout', 5) 14 | self.request_timeout = kwargs.get('request_timeout', 30) 15 | 16 | def post_request(self, url, params, headers=None, connect_timeout=None, request_timeout=None): 17 | connect_timeout = connect_timeout or self.connect_timeout 18 | request_timeout = request_timeout or self.request_timeout 19 | try: 20 | resp = requests.post( 21 | url=url, 22 | data=params, 23 | headers=headers, 24 | timeout=(connect_timeout, request_timeout) 25 | ) 26 | except Exception, e: 27 | raise e 28 | else: 29 | if resp.status_code != 200: 30 | raise RespError('The resp status code is %s' % resp.status_code) 31 | else: 32 | return resp 33 | 34 | def get_request(self, url, params, headers=None, connect_timeout=None, request_timeout=None): 35 | connect_timeout = connect_timeout or self.connect_timeout 36 | request_timeout = request_timeout or self.request_timeout 37 | try: 38 | resp = requests.get( 39 | url=url, 40 | params=params, 41 | headers=headers, 42 | timeout=(connect_timeout, request_timeout) 43 | ) 44 | return resp 45 | except Exception, e: 46 | raise e 47 | else: 48 | if resp.status_code != 200: 49 | raise RespError('The resp status code is %s' % resp.status_code) 50 | else: 51 | return resp 52 | 53 | 54 | class BaiduClient(BaseClient): 55 | """Client for handling the process of Baidu Voice requests.""" 56 | def __init__(self, **kwargs): 57 | super(BaiduClient, self).__init__(**kwargs) 58 | 59 | def get_token(self, params): 60 | try: 61 | resp = self.get_request(BaiduUrl.token_url, params) 62 | except Exception, e: 63 | raise e 64 | else: 65 | r = resp.json() 66 | if 'error' in r: 67 | raise VerifyError(r.get('error_description', 'unknown error')) 68 | return r 69 | 70 | def tts(self, params): 71 | try: 72 | resp = self.get_request(BaiduUrl.tts_url, params) 73 | except Exception, e: 74 | raise e 75 | content_type = resp.headers.get('Content-type') 76 | if content_type == 'application/json': 77 | response = resp.json() 78 | err_msg = 'err_msg: %s' % response.get('err_msg', 'baidu tts service error') 79 | raise ErrNo.tts_err_no.get(response['err_no'], APIError)(err_msg) 80 | elif content_type == 'audio/mp3': 81 | return resp.content 82 | else: 83 | raise APIError('baidu tts service unknown error') 84 | 85 | def asr(self, speech, params): 86 | headers = { 87 | 'Content-Type': 'audio/%s;rate=%s' % (params.pop('format'), params.pop('rate')), 88 | 'Content-length': str(params.pop('len')) 89 | } 90 | url = concat_url(BaiduUrl.asr_url, params) 91 | try: 92 | resp = self.post_request(url, speech, headers=headers) 93 | print params, resp.json() 94 | except Exception, e: 95 | raise e 96 | response = resp.json() 97 | if response.get('err_no') != 0: 98 | err_msg = 'err_msg: %s' % response.get('err_msg', 'baidu asr service error') 99 | raise ErrNo.asr_err_no.get(response['err_no'], APIError)(err_msg) 100 | else: 101 | return response.get('result') 102 | 103 | def baidu_pedia(self, params): 104 | try: 105 | resp = self.get_request(url=BaiduUrl.baike_url, params=params) 106 | except Exception, e: 107 | raise e 108 | return resp 109 | 110 | baiduclient = BaiduClient() 111 | 112 | 113 | class TuringClient(BaseClient): 114 | """Client for handling the process of Turing requests.""" 115 | def __init__(self, **kwargs): 116 | super(TuringClient, self).__init__(**kwargs) 117 | 118 | def query_turing(self, params): 119 | try: 120 | resp = self.get_request(url=TuringUrl.turing_url, params=params) 121 | except Exception, e: 122 | raise e 123 | return resp.json() 124 | 125 | turingclient = TuringClient() 126 | -------------------------------------------------------------------------------- /voicetools/api.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import wolframalpha 3 | from .exceptions import APIError 4 | from .clients import turingclient, baiduclient 5 | from .utils import get_mac_address, get_audio_info 6 | 7 | 8 | class Wolfram(object): 9 | """A client for request Wolfram. 10 | 11 | Attributes: 12 | key: The key string got from https://www.wolframalpha.com. 13 | """ 14 | def __init__(self, key): 15 | self.key = key 16 | 17 | def ask_wolfram(self, question): 18 | client = wolframalpha.Client(self.key) 19 | res = client.query(question) 20 | if len(res.pods) > 0: 21 | pod = res.pods[1] 22 | if pod.text: 23 | texts = pod.text 24 | else: 25 | raise APIError('Wolfram API failed.') 26 | # to skip ascii character in case of error 27 | texts = texts.encode('ascii', 'ignore') 28 | return texts 29 | else: 30 | raise APIError('Wolfram API failed.') 31 | 32 | 33 | class TuringRobot(object): 34 | """A client for request Turing Robot. 35 | 36 | Attributes: 37 | key: The key string got from http://www.tuling123.com. 38 | """ 39 | def __init__(self, key): 40 | self.key = key 41 | 42 | def ask_turing(self, question): 43 | params = { 44 | 'key': self.key, 45 | 'info': question 46 | } 47 | ret = turingclient.query_turing(params) 48 | code = ret.get('code') 49 | if code == 100000: 50 | return ret['text'].encode('utf-8') 51 | else: 52 | raise APIError('Cannot handle this ret code: %s' % code) 53 | 54 | 55 | class BaiduVoice(object): 56 | """A client for request Turing Robot. 57 | 58 | Attributes: 59 | token: The token string got from https://openapi.baidu.com/oauth/2.0/token. 60 | cuid: Unique identification of user, default is MAC address. 61 | """ 62 | def __init__(self, token): 63 | self.token = token 64 | self.cuid = get_mac_address() 65 | 66 | def asr(self, file_, format_='wav', 67 | cuid=None, ptc=1, lan='zh'): 68 | """Constructs and sends an Automatic Speech Recognition request. 69 | 70 | Args: 71 | file_: the open file with methods write(), close(), tell(), seek() 72 | set through the __init__() method. 73 | format_:(optional) the audio format, default is 'wav' 74 | cuid:(optional) Unique identification of user, default is MAC address. 75 | ptc:(optional) nbest results, the number of results. 76 | lan:(optional) language, default is 'zh'. 77 | Returns: 78 | A list of recognition results. 79 | Raises: 80 | ValueError 81 | RecognitionError 82 | VerifyError 83 | APIError 84 | QuotaError 85 | """ 86 | if format_ != 'wav': 87 | raise ValueError('Unsupported audio format') 88 | params = { 89 | 'format': format_, 90 | 'token': self.token, 91 | 'cuid': cuid or self.cuid, 92 | 'ptc': ptc, 93 | 'lan': lan 94 | } 95 | try: 96 | audio_info = get_audio_info(file_) 97 | except Exception, e: 98 | raise e 99 | params['len'], params['rate'] = audio_info['nframes'], audio_info['framerate'] 100 | return baiduclient.asr(audio_info['content'], params) 101 | 102 | def tts(self, tex, lan='zh', ctp=1, 103 | cuid=None, spd=5, pit=5, vol=5, per=0): 104 | """Constructs and sends an Text To Speech request. 105 | 106 | Args: 107 | tex: The text for conversion. 108 | lan:(optional) language, default is 'zh'. 109 | ctp:(optional) Client type, default is 1. 110 | cuid:(optional) Unique identification of user, default is MAC address. 111 | spd:(optional) speed, range 0-9, default is 5. 112 | pit:(optional) pitch, range 0-9, default is 5. 113 | vol:(optional) volume, range 0-9, default is 5. 114 | per:(optional) voice of male or female, default is 0 for female voice. 115 | Returns: 116 | A binary string of MP3 format audio. 117 | Raises: 118 | ValueError 119 | VerifyError 120 | APIError 121 | """ 122 | params = { 123 | 'tex': tex, 124 | 'lan': lan, 125 | 'tok': self.token, 126 | 'ctp': ctp, 127 | 'cuid': cuid or self.cuid, 128 | 'spd': spd, 129 | 'pit': pit, 130 | 'vol': vol, 131 | 'per': per 132 | } 133 | return baiduclient.tts(params) 134 | 135 | @staticmethod 136 | def get_baidu_token(api_key, secret_key): 137 | """Get Baidu Voice Service token by api key and secret. 138 | 139 | Functions of other args of response are not confirmed, so the whole 140 | response dict will be returned, you can access the token by ret['access_token']. 141 | """ 142 | params = { 143 | 'grant_type': 'client_credentials', 144 | 'client_id': api_key, 145 | 'client_secret': secret_key 146 | } 147 | return baiduclient.get_token(params) 148 | -------------------------------------------------------------------------------- /examples/raspi_assistant/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import logging 3 | from logging.handlers import TimedRotatingFileHandler 4 | import time 5 | import random 6 | import base64 7 | from functools import wraps 8 | from hashlib import md5 9 | from subprocess import Popen, PIPE 10 | from tempfile import TemporaryFile 11 | 12 | import redis 13 | import pyaudio 14 | import wave 15 | from voicetools import BaseClient, APIError 16 | 17 | from .settings import ( 18 | LogConfig as LC, RedisConfig as RC, BaiduAPIConfig as BAC, 19 | BasicConfig as BC, ErrNo) 20 | 21 | conn_pool = redis.ConnectionPool(host=RC.HOST_ADDR, port=RC.PORT, db=RC.DB) 22 | 23 | 24 | def convert_to_wav(file_): 25 | """convert mp3 to wav""" 26 | p = Popen(['ffmpeg', '-y', '-i', '-', '-f', 'wav', BC.OUTPUT_NAME], stdin=file_ , stdout=None, stderr=None) 27 | p.wait() 28 | 29 | 30 | def init_logging_handler(): 31 | handler = TimedRotatingFileHandler(LC.LOGGING_LOCATION, when='MIDNIGHT') 32 | formatter = logging.Formatter(LC.LOGGING_FORMAT) 33 | handler.setFormatter(formatter) 34 | logger = logging.getLogger() 35 | logger.setLevel(LC.LOGGING_LEVEL) 36 | logger.addHandler(handler) 37 | return logger 38 | 39 | 40 | def generate_response(): 41 | """Generate emotional response based on the HAPPINESS_THRESHOLD.""" 42 | if random.random() <= BC.HAPPINESS_THRESHOLD: 43 | return BC.POSITIVE_ANSWER[random.randint(1, len(BC.POSITIVE_ANSWER))] 44 | else: 45 | return BC.NEGATIVE_ANSWER[random.randint(1, len(BC.NEGATIVE_ANSWER))] 46 | 47 | 48 | def unique_id(func, args, kwargs): 49 | return md5(func.__name__ + repr(args) + repr(kwargs)).hexdigest() 50 | 51 | 52 | def timestamp(): 53 | return int(time.time() * 1000) 54 | 55 | 56 | def cache(func): 57 | """Wrapper for cache the audio""" 58 | @wraps(func) 59 | def _(*args, **kwargs): 60 | cache_handler = CacheHandler() 61 | id_ = unique_id(func, *args, **kwargs) 62 | cache = cache_handler.get(id_) 63 | if cache: 64 | audio_handler = AudioHandler() 65 | audio_handler.aplay(base64.b64decode(cache), is_buffer=True) 66 | # return cache 67 | else: 68 | func(*args, **kwargs) 69 | with open('output.wav', 'rb') as f: 70 | encoded_audio = base64.b64encode(f.read()) 71 | cache_handler.set(id_, encoded_audio, 86400*7) 72 | # return buffer_ 73 | return _ 74 | 75 | 76 | class BaiduAPIClient(BaseClient): 77 | """Client for handling the process of Baidu APIStore requests.""" 78 | def __init__(self, **kwargs): 79 | super(BaiduAPIClient, self).__init__(**kwargs) 80 | self.apikey = BAC.API_KEY 81 | self.headers = {'apikey': self.apikey} 82 | 83 | def request_handler(self, url, params): 84 | try: 85 | resp = self.get_request( 86 | url=url, 87 | params=params, 88 | headers=self.headers) 89 | except Exception, e: 90 | raise e 91 | content = resp.json() 92 | if 'errNum' in content: 93 | err_msg = 'err_msg: %s' % content.get('errMsg', 'baidu api unknown error') 94 | raise ErrNo.ExceptionMap.get(content['errNum'][:4], APIError)(err_msg) 95 | else: 96 | return content 97 | 98 | def heweather(self): 99 | try: 100 | content = self.request_handler( 101 | url=BAC.WEATHER_URL, 102 | params={'city': BC.LOCATION}) 103 | except Exception, e: 104 | raise e 105 | weather_info = content['HeWeather data service 3.0'][0] 106 | if weather_info['status'] != 'ok': 107 | raise APIError('query weather api failed.') 108 | return weather_info 109 | 110 | 111 | class AudioHandler(object): 112 | """AudioHandler for processing audio, including recording and playing.""" 113 | def __init__(self, chunk=1024, format_=pyaudio.paInt16, channels=1, rate=16000): 114 | self.CHUNK = chunk 115 | self.FORMAT = format_ 116 | self.CHANNELS = channels 117 | self.RATE = rate 118 | 119 | def record(self, record_seconds, file_): 120 | p = pyaudio.PyAudio() 121 | 122 | stream = p.open(format=self.FORMAT, 123 | channels=self.CHANNELS, 124 | rate=self.RATE, 125 | input=True, 126 | input_device_index=0, 127 | frames_per_buffer=self.CHUNK) 128 | 129 | frames = [] 130 | 131 | for i in range(0, int(self.RATE / self.CHUNK * record_seconds)): 132 | data = stream.read(self.CHUNK) 133 | frames.append(data) 134 | 135 | stream.stop_stream() 136 | stream.close() 137 | p.terminate() 138 | 139 | wf = wave.open(file_, 'wb') 140 | wf.setnchannels(self.CHANNELS) 141 | wf.setsampwidth(p.get_sample_size(self.FORMAT)) 142 | wf.setframerate(self.RATE) 143 | wf.writeframes(''.join(frames)) 144 | wf.close() 145 | 146 | def arecord(self, record_seconds, is_buffer=False, file_=BC.INPUT_NAME): 147 | if is_buffer: 148 | p = Popen( 149 | ['arecord', '-r', '16000', '-D', 'plughw:1,0', '-f', 'S16_LE', '-d', str(record_seconds), '-'], 150 | stdout=PIPE, 151 | stderr=PIPE) 152 | stdout, _ = p.communicate() 153 | return stdout 154 | else: 155 | p = Popen( 156 | ['arecord', '-r', '16000', '-D', 'plughw:1,0', '-f', 'S16_LE', '-d', str(record_seconds), file_]) 157 | p.wait() 158 | 159 | def aplay(self, file_=BC.OUTPUT_NAME, is_buffer=False): 160 | if is_buffer: 161 | temp = TemporaryFile() 162 | temp.write(file_) 163 | temp.seek(0) 164 | p = Popen(['aplay', '-'], stdin=temp) 165 | temp.close() 166 | else: 167 | p = Popen(['aplay', file_]) 168 | p.wait() 169 | 170 | def play(self, file_): 171 | wf = wave.open(file_, 'rb') 172 | p = pyaudio.PyAudio() 173 | stream = p.open(format=p.get_format_from_width(wf.getsampwidth()), 174 | channels=wf.getnchannels(), 175 | rate=wf.getframerate(), 176 | output=True) 177 | 178 | data = wf.readframes(self.CHUNK) 179 | 180 | while data != '': 181 | stream.write(data) 182 | data = wf.readframes(self.CHUNK) 183 | 184 | stream.stop_stream() 185 | stream.close() 186 | 187 | p.terminate() 188 | 189 | 190 | class Keyword(object): 191 | """Object for """ 192 | def __init__(self, list_): 193 | list_.sort() 194 | self.keywords = list_ 195 | self.value = '/'.join(list_) 196 | 197 | def __repr__(self): 198 | return self.value 199 | 200 | 201 | class CacheHandler(object): 202 | """CacheHandler for manipulating redis.""" 203 | def __init__(self): 204 | self.client = redis.StrictRedis( 205 | connection_pool=conn_pool, socket_timeout=RC.SOCKET_TIMEOUT) 206 | 207 | def set(self, name, value, ttl=None): 208 | if ttl: 209 | self.client.setex(name, ttl, value) 210 | else: 211 | self.client.set(name, value) 212 | 213 | def get(self, name): 214 | return self.client.get(name) 215 | 216 | def delete(self, name): 217 | return self.client.delete(name) 218 | 219 | def expire(self, name, ttl): 220 | return self.client.expire(name, ttl) 221 | 222 | def zset(self, name, key, score, ttl=None, is_audio=True): 223 | """zset for audio. if is_audio=True, """ 224 | if is_audio: 225 | key = base64.b64encode(key) 226 | if ttl: 227 | pipeline = self.client.pipeline() 228 | pipeline.zadd(name, score, key) 229 | pipeline.expire(name, ttl) 230 | return pipeline.execute() 231 | else: 232 | self.client.zadd(name, score, key) 233 | 234 | def zget(self, name, start, end, is_audio=True): 235 | ret = self.client.zrange(name, start, end) 236 | if is_audio: 237 | return [base64.b64decode(x) for x in ret] 238 | else: 239 | return ret 240 | 241 | def zdel(self, name, start, end): 242 | # zremrangebyrank 243 | return self.client.zremrangebyrank(name, start, end) 244 | -------------------------------------------------------------------------------- /examples/raspi_assistant/handler.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import logging 3 | import traceback 4 | import datetime 5 | from tempfile import NamedTemporaryFile 6 | 7 | import jieba 8 | from voicetools import BaiduVoice, TuringRobot 9 | 10 | from .settings import BasicConfig as BC, BaiduAPIConfig as BAC 11 | from .utils import ( 12 | AudioHandler, Keyword, cache, CacheHandler, timestamp, BaiduAPIClient, 13 | generate_response, convert_to_wav) 14 | 15 | logger = logging.getLogger() 16 | 17 | FUNC_MAP = { 18 | Keyword(['备忘录', ]).value: 'memo_today', 19 | Keyword(['提醒', ]).value: 'memo_today', 20 | Keyword(['备忘录', '今天']).value: 'memo_today', 21 | Keyword(['提醒', '今天']).value: 'memo_today', 22 | Keyword(['备忘录', '明天']).value: 'memo_tomo', 23 | Keyword(['提醒', '明天']).value: 'memo_tomo', 24 | Keyword(['备忘录', '播放']).value: 'play_memo_today', 25 | Keyword(['提醒', '播放']).value: 'play_memo_today', 26 | Keyword(['备忘录', '播放', '明天']).value: 'play_memo_tomo', 27 | Keyword(['提醒', '播放', '明天']).value: 'play_memo_tomo', 28 | Keyword(['备忘录', '删除']).value: 'del_all_memo', 29 | Keyword(['提醒', '删除']).value: 'del_all_memo', 30 | Keyword(['备忘录', '删除', '最后']).value: 'del_last_memo', 31 | Keyword(['提醒', '删除', '最后']).value: 'del_last_memo', 32 | Keyword(['备忘录', '删除', '第一条']).value: 'del_first_memo', 33 | Keyword(['提醒', '删除', '第一条']).value: 'del_first_memo', 34 | Keyword(['明天', '天气']).value: 'weather_tomo', 35 | Keyword(['今天', '天气']).value: 'weather_today' 36 | } 37 | 38 | 39 | class BaseHandler(object): 40 | 41 | def __init__(self, baidu_token=None): 42 | if not baidu_token: 43 | try: 44 | token = BaiduVoice.get_baidu_token(BC.VOICE_API_KEY, BC.VOICE_SECRET) 45 | self.token = token['access_token'] 46 | except Exception, e: 47 | logger.warn('======Get baidu voice token failed, %s', traceback.format_exc()) 48 | raise e 49 | else: 50 | self.token = baidu_token 51 | self.bv = BaiduVoice(self.token) 52 | self.audio_handler = AudioHandler() 53 | 54 | def __repr__(self): 55 | return '' 56 | 57 | def receive(self, sec=4): 58 | self.feedback(generate_response()) 59 | self.audio_handler.arecord(sec) 60 | try: 61 | return self.bv.asr('record.wav') 62 | except Exception, e: 63 | logger.warn('======Baidu ASR failed, %s', traceback.format_exc()) 64 | 65 | def process(self, results): 66 | seg_list = list(jieba.cut(results[0], cut_all=True)) 67 | # command = Keyword(list(set(seg_list) & BC.KEYWORDS)) 68 | command = Keyword(list(set((x.encode('utf-8') for x in seg_list)) & BC.KEYWORDS)) 69 | logger.info('=======Recognition result: %s', command.value) 70 | return FUNC_MAP.get(command.value, 'default'), results[0] 71 | 72 | def execute(self, func_name, result): 73 | func = getattr(ActionHandler, func_name) 74 | return func(self, result) 75 | 76 | @cache 77 | def feedback(self, content=None): 78 | if content: 79 | try: 80 | data = NamedTemporaryFile() 81 | data.write(self.bv.tts(content)) 82 | data.seek(0) 83 | convert_to_wav(data) 84 | data.close() 85 | except Exception, e: 86 | logger.warn('======Baidu TTS failed, %s', traceback.format_exc()) 87 | else: 88 | self.audio_handler.aplay() 89 | 90 | def worker(self): 91 | try: 92 | results = self.receive() 93 | func, result = self.process(results) 94 | content = self.execute(func, result) 95 | self.feedback(content) 96 | except Exception, e: 97 | self.feedback('出现系统异常,请检查日志') 98 | 99 | 100 | class ActionHandler(object): 101 | """docstring for ActionHandler""" 102 | 103 | @staticmethod 104 | def default(base_handler, result): 105 | print 'turing run' 106 | robot = TuringRobot(BC.TURING_KEY) 107 | try: 108 | content = robot.ask_turing(result) 109 | except Exception, e: 110 | logger.warn(traceback.format_exc()) 111 | return '没有找到问题的答案' 112 | else: 113 | return content 114 | 115 | @staticmethod 116 | def _memo(date, base_handler): 117 | base_handler.feedback('请说出记录内容') 118 | audio = base_handler.audio_handler.arecord(6, is_buffer=True) 119 | cache_handler = CacheHandler() 120 | cache_handler.zset(date, audio, timestamp(), 86400*3) 121 | return '完成记录' 122 | 123 | @staticmethod 124 | def memo_today(base_handler, result): 125 | return ActionHandler._memo( 126 | date=datetime.date.today().strftime('%Y-%m-%d'), 127 | base_handler=base_handler 128 | ) 129 | 130 | @staticmethod 131 | def memo_tomo(base_handler, result): 132 | return ActionHandler._memo( 133 | date=(datetime.date.today() + datetime.timedelta(days=1)).strftime('%Y-%m-%d'), 134 | base_handler=base_handler 135 | ) 136 | 137 | @staticmethod 138 | def play_memo(date, base_handler): 139 | cache_handler = CacheHandler() 140 | audio = cache_handler.zget(date, 0, -1) 141 | if audio: 142 | for item in audio: 143 | base_handler.audio_handler.aplay(item, is_buffer=True) 144 | return '播放结束' 145 | else: 146 | base_handler.feedback('未找到记录') 147 | return None 148 | 149 | @staticmethod 150 | def play_memo_tomo(base_handler, result): 151 | return ActionHandler.play_memo( 152 | date=(datetime.date.today() + datetime.timedelta(days=1)).strftime('%Y-%m-%d'), 153 | base_handler=base_handler 154 | ) 155 | 156 | @staticmethod 157 | def play_memo_today(base_handler, result): 158 | return ActionHandler.play_memo( 159 | date=datetime.date.today().strftime('%Y-%m-%d'), 160 | base_handler=base_handler 161 | ) 162 | 163 | @staticmethod 164 | def del_memo(date, start, end): 165 | cache_handler = CacheHandler() 166 | cache_handler.zdel(date, start, end) 167 | return '删除成功' 168 | 169 | @staticmethod 170 | def del_last_memo(base_handler, result): 171 | return ActionHandler.del_memo( 172 | date=datetime.date.today().strftime('%Y-%m-%d'), 173 | start=-1, 174 | end=-1 175 | ) 176 | 177 | @staticmethod 178 | def del_first_memo(base_handler, result): 179 | return ActionHandler.del_memo( 180 | date=datetime.date.today().strftime('%Y-%m-%d'), 181 | start=0, 182 | end=0 183 | ) 184 | 185 | @staticmethod 186 | def del_all_memo(base_handler, result): 187 | return ActionHandler.del_memo( 188 | date=datetime.date.today().strftime('%Y-%m-%d'), 189 | start=0, 190 | end=-1 191 | ) 192 | 193 | @staticmethod 194 | def weather_tomo(base_handler, result): 195 | return ActionHandler.query_weather('tomo') 196 | 197 | @staticmethod 198 | def weather_today(base_handler, result): 199 | return ActionHandler.query_weather('today') 200 | 201 | @staticmethod 202 | def query_weather(today_or_tomo): 203 | client = BaiduAPIClient() 204 | try: 205 | content = client.heweather() 206 | except Exception, e: 207 | logger.warn(traceback.format_exc()) 208 | return '查询天气失败,请检查日志' 209 | if today_or_tomo == 'today': 210 | now_weather = content.get('now') 211 | today_weather = content.get('daily_forecast')[0] 212 | text = BAC.TODAY_WEATHER_TEXT.format( 213 | cond=now_weather['cond']['txt'], 214 | fl=now_weather['fl'], 215 | hum=now_weather['hum'], 216 | min=today_weather['tmp']['min'], 217 | max=today_weather['tmp']['max'], 218 | txt_d=today_weather['cond']['txt_d'], 219 | txt_n=today_weather['cond']['txt_n'], 220 | pop=today_weather['pop'], 221 | qlty=content['aqi']['city']['aqi'] 222 | ) 223 | elif today_or_tomo == 'tomo': 224 | tomo_weather = content.get('daily_forecast')[1] 225 | text = BAC.TOMO_WEATHER_TEXT.format( 226 | min=tomo_weather['tmp']['min'], 227 | max=tomo_weather['tmp']['max'], 228 | txt_d=tomo_weather['cond']['txt_d'], 229 | txt_n=tomo_weather['cond']['txt_n'], 230 | pop=tomo_weather['pop'] 231 | ) 232 | else: 233 | return '查询天气失败,请检查日志' 234 | return text 235 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------