dingdang-robot 之殤前端
wukong-robot 重生之路python
項目管理:Github project boardsgit
熱詞喚醒:snowboyweb
技能插件重構:AbstractPlugindocker
後臺管理端:tornadojson
更新器:git tag + SCFbootstrap
總結和展望後端
dingdang-robot 之殤
在兩年前,我作了第一個智能音箱項目 dingdang-robot 。在去年 7 月加入上報統計後,在不到一年的時間裏,這個項目已經運行在 1000+ 臺設備中,被喚醒了 128,000+ 次。截至今天,這個項目的我的版和社區版在 Github 上總共得到了 2,600+ 個 stars ,820+ 次 forks。瀏覽器
在我去年的一篇年度總結中,我提到由於 dingdang-robot 自己維護上的困難,我將項目遷移到了 dingdang-robot 組織交由社區進行維護。很遺憾的是,即便遷到了 dingdang-robot 組織,因爲組織維護者們都並非全職維護這個項目,並且硬件和操做系統上的差別始終給 dingdang-robot 的維護帶來了很大的問題,因此取得的效果並不理想。並且隨着本身能力的不斷提高,我對 dingdang-robot 裏頭的代碼也愈加不滿意:安全
dingdang-robot 是基於 Python 2 的,在 Python 3 環境裏跑不起來。而 Python 2 已經中止維護了。
dingdang-robot 的熱詞喚醒(KWS)複用的是 jasper-client 的那套,基於 PyAudio 本身實現錄音和 VAD ,基於 PocketSphinx 實現熱詞喚醒。然而那套錄音和VAD代碼我我的以爲寫得並不魯棒,爲了不各類邊界狀況我不得不加了一些
try...catch
,雖然沒人發現這一點,但我本身是過不了本身那一關的,往往想到本身在用一套有問題的代碼做爲別人的入口就像是留一個坑叫別人跳進來,心裏以爲頗有罪惡感;另外 PocketSphinx 的安裝很是複雜,雖然我提供了樹莓派的鏡像,可是不少人仍是但願手動安裝,而 PocketSphinx 對環境要求也很苛刻,因此總會遇到各類奇怪的問題,而我又不能復現;還有一些使用上的便利性問題。好比沒有更新提示,有時候修了一個bug,別人不知道,提了issue後我得告訴他請更新到最新;再好比使用YAML做爲配置文件,可是不少用戶不懂YAML的語法格式,常見的好比把半角冒號(
:
)打成全角(:
),或者冒號後沒有跟着空格再寫鍵值;再好比當初我處理 log 的打印也設計得比較傻逼,爲了寫到文件裏頭,直接用的是重定向,徹底沒有考慮用 FileHandler 這種東西。
到了今年,我決定對 dingdang-robot 進行徹底重寫,作出一個更加 優雅、靈活、魯棒 的版本。
爲了區別於之前的版本,我決定起給這個新版本起一個新的名字。我以爲三個字的喚醒詞誤喚醒率和長度都是比較理想的,因此我想取一個三個字的名字,另外還要能提現新版本的強大之處。因而我想到了「孫悟空」(後來才發現又一次跟優必選和騰訊叮噹的合做項目重名了,real尷尬)。因而,利用整個春節的假期(你沒看錯,我整個春節都用來寫代碼去了)。正月初五那天,wukong-robot 1.0 正式發佈了。
如下是一段 wukong-robot 的定製版本 ycy-robot 的演示視頻:
wukong-robot 重生之路
按照慣例,下面總結一下這個項目的一些開發心得。
項目管理:Github project boards
project boards 是 Github 近期推出的一個新功能,它最大的用處是提供了相似 trello 的看板。我在開發維護 wukong-robot 的時候,也使用 project boards 管理這個項目。因而建了一個 wukong-project 。
[ wukong project boards ]
我把項目分紅了 To do
、In Progress
、Done
、Pending
幾個狀態。在規劃第一個版本的時候,我就在 To do
欄中提了 10 個左右的需求。這使得個人項目能夠朝着明確的目標演進。不過,在開發的時候,時常還會有一些新的想法冒出來,這時候我也會盡快寫入需求池中。到真正發佈 1.0 的時候,我已經完成了 21 個需求。
project boards 的另外一個做用在於充當了項目的 roadmap 。你能夠看到這個項目有哪些計劃要作的需求,有哪些則是我正在開發中的需求。有興趣的朋友還能參與進來幫忙完成其中的部分需求任務。
project boards 還有一個頗有意思的特性:能夠和 Github 的 issues 和 pull requests 等板塊打通。當有人給你提 issue 或 pull request 的時候,能夠設置自動追加到 To do
欄裏。而當 issue 被 close 或者 pull request 被 accept 後,相應的條目能夠自動挪入 Done
一欄。
[ project boards 的 automation 特性 ]
不過,project boards 實際用起來仍是有一些問題:由於整個 wukong-robot 項目不只包括本體,還包含了第三方插件庫 wukong-contrib ,以及未來可能有的其餘一些衍生客戶端。因此我但願用一個 wukong-project 來同時管理幾個倉庫。因此 wukong-project 並非掛靠在 wukong-robot 倉庫下的,而是直接掛在個人帳戶下。但不知道是否是 Github 設計上的疏忽:即便我在 wukong-project 裏 link 了多個倉庫,那些倉庫下的 project 頁面並無展現 wukong-project :dizzy_face: 。這種狀況下,project boards 的 automation 也玩不起來 —— wukong-robot 的新 issue 也不會自動往 wukong-project 裏新增 To do
條目。等我發現這個問題的時候,我早已建立了幾十個條目,而 Github 又不支持將 transfer project boards ,因此只能將就着這麼用下去了。
熱詞喚醒:snowboy
如前面所述,dingdang-robot 早期沿用了 jasper-client 的那套熱詞喚醒和靜音檢測的邏輯。雖而後來我也嘗試給 dingdang-robot 加入了 snowboy 的支持,但讓我很失望的是它在樹莓派上使用效果很糟糕,因此我一直沒有把 snowboy 做爲默認的熱詞喚醒引擎。後來我發現其實我錯怪了 snowboy :官方文檔已經清楚地提到了問題的緣由:而樹莓派上或者其餘板子上接的麥克風可能和 PC 上的麥克風的聲音畸變差別很是大,因此現有的模型更加不能直接在樹莓派上工做,不然效果會很是糟糕。
This is due to the acoustic distortion that results from the different microphones. If you record your voice with two different microphones (one on your laptop and the other on your Pi) and then play them (play t.wav), you will hear that they sound very differently (even though it is the same voice)!
瞭解到緣由後,我在這個版本中去除了安裝繁瑣且中文識別較差的 PocketSphinx ,將 snowboy 做爲主要的熱詞喚醒引擎。由於 snowboy 還提供了靜音檢測(VAD)的功能,因此我把原來 VAD 的代碼所有去除,改成了直接使用 snowboy 的 VAD 。通過改寫後,整個系統的穩定性和響應速度都有了質的提高。
不過,接入了 snowboy 後,整個交互模式就是先熱詞喚醒觸發一個 detected_callback
的響應,說完指令後經過 audio_callback
將語音指令返回。有些時候咱們並不想徹底遵循這個形式:例如當咱們但願 wukong-robot 能主動詢問並澄清話術的時候,老是要求用戶喚醒再說指令就顯得整個交互很不智能了。因而我對 snowboydecorder 作了一點 hack :仿照 HotwordDetector 寫了一個 ActiveListener 用來實現主動詢問用戶的功能。有了這個 ActiveListener 以後,當插件須要主動詢問用戶問題時,能夠在 self.say()
的 onCompleted
回調方法中直接執行 self.activeListen()
方法獲得即拿到用戶的指令內容。例如:
def onAsk(input):
if not input:
self.say("指令有誤,請從新嘗試", cache=True)
return
# 執行響應
...
self.say("開始家庭助手控制,請在滴一聲後說明內容", cache=True, onCompleted=lambda: onAsk(self.activeListen()))
利用這個方法能夠很方便地實現多輪對話以及極客模式。
關於如何在 Python 工程中接入 snowboy ,我在一門 Python 課程中有詳細的介紹。若是你感興趣的話,能夠前往觀看。課程的免費體驗課部分已經包含了熱詞喚醒的完整內容。
技能插件重構:AbstractPlugin
原來的 dingdang-robot 在處理插件接口的時候,並無考慮到多輪對話的狀況。每一次 query 都會輪詢一遍全部插件。若是要讓某個插件在用戶指示退出前持續響應用戶的 query ,那麼就得爲這個插件實現一個內部循環。而在這個內部循環裏頭,用戶只能響應有限的指令。
例如,NetEaseMusic 插件在一個 handleForever
方法中進入了一個循環,在這個循環裏頭,只能響應「上一首」、「下一首」等音樂播放相關的指令。而有時候,咱們在播放音樂的時候,也會忽然間想問一下天氣再回來繼續播放。對於這種狀況,dingdang-robot 的插件交互模式就只能先退出音樂播放,再問天氣,再從新要求播放音樂。這樣的設計並不夠人性化。
wukong-robot 從新考慮了插件的設計。你能夠爲 wukong-robot 開發兩類技能插件:
普通技能插件,適用於普通的查詢、助手類技能。一般的交互模式是喚醒 wukong-robot 後,說出指令並觸發該技能插件,由其完成處理並彙報結果。若是須要詢問用戶問題,則能夠利用
self.activeListen()
方法進入主動聆聽,從而實現多輪對話。沉浸式技能插件,適用於音樂、電臺等技能。一般的交互模式是喚醒 wukong-robot 後,說出指令並觸發該技能插件,由其進入該技能的沉浸式場景中。在該技能的沉浸式場景下,用戶喚醒 wukong-robot 後,容許響應更多指令以完成更豐富的操做(例如「下一首歌」、「這是什麼歌」等指令)。若是喚醒後只是簡單的聊天,還容許 wukong-robot 在回答後恢復該技能的沉浸式場景(例如,用戶在音樂場景中喚醒 wukong-robot 並問完時間後,wukong-robot 能夠自動恢復音樂播放)。
不管是哪一種類型的插件,都只需繼承同一個基類 robot.sdk.AbstractPlugin
,並實現相應相關接口便可。其中:
普通技能插件只需實現
isValid()
和handle()
兩個接口,分別用來判斷用戶指令是否適合交給該技能插件處理,以及如何處理;沉浸式技能插件在普通技能插件的基礎上,還須要設置
IS_IMMERSIVE
成員屬性爲True
,此外還能夠根據需求實現isValidImmersive()
和restore()
兩個方法,分別用來支持沉浸模式下更多指令的響應以及恢復技能。
class AbstractPlugin(metaclass=ABCMeta):
""" 技能插件基類 """
SLUG = 'AbstractPlugin'
IS_IMMERSIVE = False
def __init__(self, con):
if self.IS_IMMERSIVE is not None:
self.isImmersive = self.IS_IMMERSIVE
else:
self.isImmersive = False
self.priority = 0
self.con = con
self.nlu = self.con.nlu
def play(self, src, delete=False, onCompleted=None, volume=1):
self.con.play(src, delete, onCompleted, volume)
def say(self, text, cache=False, onCompleted=None):
self.con.say(text, cache=cache, plugin=self.SLUG, onCompleted=onCompleted)
def activeListen(self, silent=False):
return self.con.activeListen(silent)
def clearImmersive(self):
self.con.setImmersiveMode(None)
@abstractmethod
def isValid(self, query, parsed):
"""
是否適合由該插件處理
參數:
query -- 用戶的指令字符串
parsed -- 用戶指令通過 NLU 解析後的結果
返回:
True: 適合由該插件處理
False: 不適合由該插件處理
"""
return False
@abstractmethod
def handle(self, query, parsed):
"""
處理邏輯
參數:
query -- 用戶的指令字符串
parsed -- 用戶指令通過 NLU 解析後的結果
"""
pass
def isValidImmersive(self, query, parsed):
"""
是否適合在沉浸模式下處理,
僅適用於有沉浸模式的插件(如音樂等)
當用戶喚醒時,能夠響應更多指令集。
例如:「"上一首"、"下一首" 等
"""
return False
def pause(self):
"""
暫停當前正在處理的任務,
當處於該沉浸模式下且被喚醒時,
將自動觸發這個方法,
能夠用於強制暫停一個耗時的操做
"""
return
def restore(self):
"""
恢復當前插件,
僅適用於有沉浸模式的插件(如音樂等)
當用戶誤喚醒或者喚醒進行閒聊後,
能夠自動恢復當前插件的處理邏輯
"""
return
通過此次重構,全部的插件都繼承自同一個基類。即便是須要多輪交互的沉浸式插件,用戶再也不須要爲其編寫相似 handleForever()
的循環,只須要關注核心的 query 處理便可。在沉浸式插件工做期間,wukong-robot 也支持響應其餘技能的 query ,交給其餘適合處理的技能插件處理,並在處理完成後根據狀況恢復當前沉浸式插件的處理。做爲對比,你能夠看看 LocalPlayer 插件,它的可讀性要比 NetEaseMusic 插件強不少。
關於如何爲 wukong-robot 開發技能插件,能夠閱讀 wukong-robot 的 插件開發教程 。另外,在個人 Python 課程的「大腦模塊和技能系統實現」一章中將更加深刻地介紹 wukong-robot 插件機制的實現原理。
後臺管理端:tornado
早在 dingdang-robot 發佈初期,我就有爲它配套開發一個後臺管理端的想法。但由於種種緣由(主要是由於懶),這個想法一直拖着沒有去作。因而藉着此次項目重寫,趁熱打鐵就把後臺管理端也完成了。
由於對 Jinja 比較有好感,因此我起初是打算用 Flask 來寫後臺管理端。但後面發現 Flask 的信號機制不能直接在非主線程裏工做,而直接放主線程又會跟另外一個必須工做在主線程的 snowboy 有衝突。折騰了半天后我決定改成直接支持在非主線程工做的 tornado 。
後臺管理端的技術棧主要包括:
開發框架:tornado
前端框架:twitter-bootstrap + jQuery
Material Design 風格的懸浮錄音按鈕:material-floating-button
錄音:opus-recorder
錄音過程當中的 spin:spin.js
右上角的 toast 提示:toastr
進度條:progress.js
[ wukong-robot 的後臺管理端 ]
比較費腦的是鑑權部分。除了後臺管理端須要設計登陸界面以免非法訪問以外,我但願後臺的接口可以開放 API 以支持其餘配套客戶端的接入,因此後端代碼須要考慮兩種訪問來源的鑑權。
最初我使用 cookie 來鑑權,管理端登陸成功後,就把用戶設置的鑑權密鑰 validation
字段存到 cookie 裏頭。前端在 Ajax 調用後端 API 時,能夠直接從 cookie
裏取出 validation
而後做爲鑑權字段發給後臺。然而 cookie 自己是明文保存的,這種作法會直接暴露用戶的密鑰,所以是一種很不安全的作法。
而後我嘗試了使用 secure_cookie
來保存鑑權信息,然而由於 secure_cookie
是加了密的字段,前端沒辦法直接解析並傳回給後端,因此又暫時放棄了這個作法。
再後來我發現還有一個 csrf_cookies
,能夠用來防止跨站請求的問題。因而我很興奮地加入了這個校驗。但後面我發現這個跨站請求保護也適用於站點自己的保護,由於 xsrf_cookies
的校驗會在調用咱們的接口實現方法前就完成,一旦加了這個校驗後,其餘客戶端在調用 API 時也必須帶上 csrf_cookies
,不然會直接拋出 '_xsrf' argument missing from POST
的錯誤。所以這個校驗更適合用於純 Web 站點,而不適合用於開放 API 的應用。
最後我轉念一想:雖然前端沒辦法直接解析 secure_cookie
獲得 validation ,可是 secure_cookie
也只是一個加了密的 cookie ,我依然能夠取出 secure_cookie
裏這個加了密的 validation
的值而後傳給後臺,然後臺則可使用 get_cookie
(而不是 get_secure_cookie
)取出指望的加了密後的 validation
的值並與前端傳過來的值進行比對,這樣就實現了前端頁面的鑑權;對於 API 的鑑權,則能夠直接使用明文的 validation
並將其做爲第三方客戶端的一個配置。後端在鑑權時直接判斷這個 validation
與後端的配置裏的 validation
值是否相等便可。因此最終我完成了以下的一個帶鑑權的基類:
class BaseHandler(tornado.web.RequestHandler):
def isValidated(self):
if not self.get_secure_cookie('validation'):
return False
return str(self.get_secure_cookie("validation"), encoding='utf-8') == config.get('/server/validate', '')
def validate(self, validation):
if '"' in validation:
validation = validation.replace('"', '')
return validation == config.get('/server/validate', '') or validation == str(self.get_cookie('validation'))
在配置頁面,我在保存配置的時候加了 yaml.load()
檢查,若是用戶修改 YAML 有格式問題,將會被拒絕寫入配置。另外,我還基於 watchdog 加入了對配置文件的監聽:一旦配置文件發生修改,就觸發配置的從新讀取,從而實現無需重啓更新大部分的配置。
# -*- coding: utf-8-*-
from robot import config
from watchdog.events import FileSystemEventHandler
class ConfigMonitor(FileSystemEventHandler):
def __init__(self, conversation):
FileSystemEventHandler.__init__(self)
self._conversation = conversation
# 文件修改
def on_modified(self, event):
if not event.is_directory:
config.reload()
self._conversation.reload()
要說不太滿意的地方,主要是首頁的聊天消息更新機制。目前我是直接使用輪詢的方式實現的 —— 前端會每隔 5 秒調用一次 /gethistory
接口,從而更新聊天記錄。這種方式無疑是低效且浪費資源的作法。我曾經嘗試將更新機制改爲用 websocket 來實現,但後來發現手機端的瀏覽器幾乎都不支持 websocket ,考慮到便攜性的重要程度,我就放棄了這種實現。
後面我將嘗試使用 tordano 的 coroutine 來實現長鏈接通訊以及後端的主動更新,這會是一種更好的實現方案。
個人 Python 課程的整個 Part 3 將更加系統地介紹 wukong-robot 的後臺管理端開發過程,歡迎前往瞭解。
更新器:git tag + SCF
在即將發佈 wukong-robot 的時候,我忽然想到應該給 wukong-robot 一個提示升級的功能。當檢測到版本更新時,提示用戶進行升級。
[ wukong-robot 的提示升級 ]
因而我給 wukong-robot 的主倉庫和插件倉庫設計了一套基於 git 的更新機制:
在兩個倉庫的根目錄各維護一個
VERSION
文件用於記錄當前的版本號,版本號使用 Semantic Versioning 標準;當要發佈新版本時,更新
VERSION
的版本號,併爲其打一個新的 tag ;客戶端檢查到有更新時,拉取到最新的代碼,而後再切到對應的 tag 。實際執行的命令爲
git checkout master && git pull && git checkout TAG名
。
剩下的主要問題是檢查更新的服務應該部署到哪裏。固然,簡單的搭一個更新檢查服務器並不複雜,但服務器的維護成本比較高。若是後面我換了服務器,又得從新到另外一個服務器搭一遍更新服務。另外,我並不太但願每次要發佈新版本都得打開終端登陸到個人服務器進行修改。最理想的應該是有個能夠隨時修改的 <q>雲 json 串</q> 。因而我選擇使用了騰訊雲的無服務器函數(SCF):把最新版本信息寫成一個SCF,經過向SCF發請求完成版本更新檢查。這樣的好處是無需購買和維護服務器,無需到服務器發佈代碼,並且SCF提供了方便的在線編輯、版本管理和測試驗證的能力,這比本身發版本還要靠譜的多。
[ 使用騰訊雲SCF實現更新檢查 ]
總結和展望
wukong-robot 的改動以下:
徹底重寫了 dingdang-robot 的大部分代碼,新的架構我我的以爲足夠漂亮。
原來的版本只能在 Linux 平臺運行,並且 PocketSphinx 安裝很苛刻,失敗率很高,PocketSphinx 對中文的識別率也很通常。新版本使用 snowboy 取代 PocketSphinx ,不管是安裝成本、穩定性、喚醒成功率都是質的飛躍。
提供了可視化的後臺管理端,而且開放API。配套了配置頁面、日誌查看頁面等管理頁,大部分配置作到了免重啓即改即生效。利用它能夠輕鬆作出漂亮的交互界面,甚至開發出新的客戶端,你能夠類比爲 Echo 一代到 Echo Show 的飛躍。
基於騰訊雲 SCF 實現了版本更新檢查,向專業的開源框架標準邁進。
docker 鏡像安裝支持,另外金輝同窗也爲它貢獻了一個一鍵式安裝腳本。
對技能插件接口進行了重構,支持了沉浸式插件,開發者能夠輕鬆實現多輪對話、音樂播放,我近期支持的極客模式特性也是使用了沉浸式插件。另外還加入了NLU支持,開發者能夠寫出更加智能的插件,處理更復雜的語義。
將一些我認爲有侵權嫌疑的特性移出倉庫本體。例如再也不自帶網易雲音樂技能,另外我也把微信功能移出了本體,而是改成利用 API 實現了一個基於 itchat 的客戶端。因此 wukong-robot 是一個比 dingdang-robot 更加 「君子」 的版本。
wukong-robot 後續的重要計劃是訓練本地的 ASR 、TTS 、NLU 及對話系統,並引入 RNN 降噪來改善環境較嘈雜的狀況下難以喚醒的問題。關於項目的計劃,能夠關注 wukong project board 。
而在近期,我正在騰訊課堂上推出一套 Python 開發教程,其中會用到 wukong-robot 做爲一個開發案例。
[ Python 從入門到實戰課程 ]
這套視頻課程將從零開始,一步步教你如何使用 Python 開發出 wukong-robot 。涉及 Python 的基礎語法,以及離線喚醒、靜音檢測、語音識別、語音合成、對話機器人等知識背景的介紹及相關sdk和服務的接入,並在這個基礎上如何經過一步步的重構優化,開發出一個靈活可配置的 wukong-robot 。另外,還介紹瞭如何使用 tornado + twitter bootstrap + jQuery + Ajax 開發後臺管理端及前端頁面。進階版中還包括了爬蟲技術及 Flask 等技術的相關實戰。
如今這門課的基礎篇和完整篇都有打折優惠,想要學習 Python 開發的朋友千萬別錯過。
基礎篇:https://ke.qq.com/course/387931?tuin=1b8113f4
完整篇:https://ke.qq.com/course/384790?tuin=1b8113f4
這門課的準備和錄製幾乎佔據了我所有的業餘時間,錄製的過程是很是痛苦和煎熬的。好比,爲了講好 subprocess ,我把 subprocess 的老版本高級 API 、新的高級 API,再到底層的 Popen 以及涉及到的 Linux 的標準輸入輸出和管道的概念都講了一遍。對於講授的方式,我比較提倡授人以魚不如授人以漁的主張,因此我並非直接貼 API ,而是帶着讀者一塊兒看 Python 的官方文檔,着重培養閱讀文檔的能力。這種講法很是的累,但倒是我認爲每一個工程師應該掌握的學習方式。
[ wukong-robot開發 ]
參與這門課的製做也是爲了完成我在去年的我的總結中立下的 flag 。Python 一直是我業餘時間最經常使用的玩具語言,它很是適合用於原型開發。我有很多開源項目,好比 wukong-robot、dingdang-robot、LiveCV 都是用 Python 寫的。而在個人工做中,它也幫助我完成了大量的工具和項目,這些工具和項目對我的或團隊起到了很是大的做用(例如加班統計平臺、已經在上百家中小銀行中使用的fmanager),所以 Python 也無疑給個人職業發展起到了很大的推進做用。把我所掌握的 Python 知識分享給更多人,讓更多人可以自如的使用這門語言來知足他們的需求,那也算是我對 Python 這門語言的回饋。
本文分享自微信公衆號 - HaHack(gh_12d2fe363c80)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。