Python的paramiko,wxPython库的应用

Sound eXchange 命令行

需求

最近在给一个做语音识别的项目做QA工作。众所周知,此类人工智能方面的项目都需要一些数据收集的工作。作为一个比较小型的团队,暂时还没有使用外包团队来做大量的数据,前期数据来源都是团队内的同事录音。

我们的录音工具是树莓派+麦克风阵列,通过ssh,可以用电脑操作树莓派开始录音和停止录音。虽然从QA的角度来说,操作非常固定,但是团队同事有近一半同事不是程序猿,看到secureCRT这么恐怖的界面(此处应有图)大多还是有心理障碍。

所以为了提升用户体验(其实是没有工具就得我一个人来录了),用python做了个简单的工具,达到了点点就能用效果。顺带还整合了两三波新的需求

基本功能

可以从语料库中随机出一句话,展示在界面上给朗读人看

可以点击按钮开始录音,点击按钮结束录音。留了一个取消录音功能防止嘴飘。

有一些环境参数(性别,安静环境/嘈杂环境,距离等)可以手动填入

结束录音后可以按照格式保存为xxxx.wav文件

升级功能

支持播放已有语音的功能,用于做环境噪音合成

原理

麦克风阵列通过USB连接到树莓派,树莓派通过无线连接到插在电脑上的360随身wifi上。PC通过secureCRT或者mac的ssh语句可以远程到树莓派上执行指令。

连上以后执行下述命令即可开始录音。

$arecord -D "plughw:1,0" -r 16000 -c 10 -f S16_LE test.wav

录音指令是阻塞的,ctrl+C停止录音。

Version.1 Flask + paramiko

第一个版本就是基于上述原理的。而且因为要做个界面,所以就选了比较熟悉的Bootstrap做网页,Flask做后端,通过paramiko来执行操作。

paramiko

连接

paramiko创建ssh连接的方式如下:
import paramiko
IP = "xxx.xxx.xxx.xxx"
PORT = 22
USERNAME = "user"
PASSWORD = "pwd"
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(IP, PORT, USERNAME, PASSWORD)
执行指令
paramiko中执行指令的类有2个,一个是上述代码中的ssh,可以用stdin, stdout, stderr = ssh.exec_command(cmd)来执行cmd指令,还有一个是可以用channel来执行。
chan = ssh.get_Transport().open_session()
chan = send(cmd)
str = chan.recv(recv_buffer)

考虑到上述录音指令是阻塞的,因此如果采用ssh.exec_command(cmd)没有办法通过ctrl+C来停止录音。因此考虑采用channel.send的方法。ctrl+C可以用channel.send('\x03')来执行。

但是各种花式调用以后,一直都没能停掉录音的进程,原因不明。

因此尝试了一下直接kill掉进程可以保存数据以后,选择了简单粗暴地kill掉arecord所在进程的方法。那么接下来的问题就是获取arecord所在进程id的方法了。

执行$ps -ef | grep \"arecord\" | grep -v grep | awk '{print $2}'可以获取到arecord所在进程pid,再拼接指令$kill -9 xxxx即可杀掉进程

前端

前端其实没啥好说,提交表单,ajax刷新页面。不过之前其实也没有写过ajax的get方法,所以记录一下

function randomclick(){
$.get("/random", function(data){document.getElementById('scentence').innerHTML = data;})
};

整体框架

解决了上述执行指令的问题以后,基本就是给网页提供接口了。除了随机语句的功能以外,就是

开始录音:执行录音语句

结束录音:结束录音进程,并将语音文件重命名为指定格式的wav文件

取消录音:结束录音进程,删除临时语音文件

Version.2 wxPython+sox

后来因为有了新的需求,主要是播放wav文件和文件命名优化。回过头来看了看,觉得这种需求做成网页其实是不大合适的,毕竟根本没法处理并发的情况。所以决定用python的UI库来做成一个小工具。

之前用过python的pyqt库,但是不知道为什么安装不上pyqt(我觉得就是那阵特别不顺,没别的!),于是换成了wxPython。后来发现wxPython的media还自带音频播放功能。

wxPython

UI设计

大概描述一下需求吧

文本展示框,附带2个功能按钮,只改变该文本框的内容

退出时保存一些运行信息到文件中

几个信息输入框,可能有select,也有可能有文本输入

开始录音、结束录音和取消录音按钮

选择播放录音目录的按钮和播放录音的按钮

接下来就一个一个需求点来说吧

文本展示框

sentence = wx.TextCtrl(self, style = wx.TE_MULTILINE, size = (xx, xx))
sentence.SetValue(str)
str = sentence.GetValue()
style = wx.TE_MULTILINE表示该文本框为多行文本框,会自动换行。默认为单行文本框。可以用SetValue方法设置其值,也可以用GetValue来读取文本框的值。
退出时保存信息
def __init__(self, parent, title)
self.Bind(wx.EVT_CLOSE, self.onExit, parent)
def onExit(self, evt):
# do sth
evt.Skip()
pass

在init函数中通过self.Bind将wx.EVT_CLOSE事件绑定到self.onExit函数上。

在self.onExit函数中将运行时的参数保存于文档中。

注意最后需要evt.Skip()来执行关闭界面的功能

特别需要注意的是,当前类需要继承wx.Frame才能使得关闭界面与当前类的wx.EVT_CLOSE事件绑定。

选择框

category_list = ["男", "女"]
classify = wx.Choice(self, -1, choices=category_list, size = (100, 48))
classify.SetSelection(0)
wx.Choice组件需要提供下拉选择列表,比如category_list,可以用SetSelection(idx)将列表中的idx项设为默认值。如果没有设定默认值,默认显示为空。
目录选择按钮
def onClickSelectDir(self, evt):
dlg = wx.DirDialog(self, "Choose a directory:")
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
self.updatelist(path)
dlg.Destroy()

将目录选择按钮的wx.EVT_BUTTON绑定至onClickSelectDir函数上。通过path = dlg.GetPath()获取到选择的目录以后,执行自定义的updatelist(path)来完成保存数据的功能。

播放功能

def __init__(self, parent, title):
try:
self.mc = wx.media.MediaCtrl(self, style=wx.SIMPLE_BORDER)
except NotImplementedError:
self.Destroy()
raise
self.Bind(EVT_BUTTON, onPlay, playbutton)
def onPlay(self, evt):
self.mc.Load(self.filename)
time.sleep(0.5)
self.mc.Play()
在init中定义MediaCtrl控件

把playbutton的点击事件和onPlay函数绑定

onPlay函数中,首先用MediaCtrl组件的Load函数加载媒体文件,sleep(0.5)后执行Play函数。

其实这种写法并不是非常专业,之前调研的结果是需要处理EVT_MEDIA_LOADED事件,但是尝试发现将此事件与函数绑定后并未触发该事件,因此简单粗暴sleep解决。