RaspberryPiのマイクで会話してみる【ヒミツのクマちゃん 完結編】

RaspberryPiのマイクで会話してみる【ヒミツのクマちゃん 完結編】

これまでのエントリの内容に最後に「今日会った出来事を教えてね」感じで会話するようにします。

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

uepon.hatenadiary.com

動作の概要は以下のような感じになります。

f:id:ueponx:20161226015507j:plain

これまでのソースファイルに音声認識APIへのアクセスをいれ、そのあとにNobyAPIに送信することで 会話を行うことになります。少しアレンジした点は音声認識の開始と終了に効果音をいれたところになります。

                # pygame
                pygame.mixer.music.load("./Q.mp3")
                pygame.mixer.music.play()
                time.sleep(1)
                pygame.mixer.music.stop()

このように音声認識開始と終了の部分に短い効果音をいれると話す人の戸惑いが少ないと思います。(ペッパーなどでも入れてますね。) 他のロボットと違い、目などの色で話している人にロボットがどのような状態かを知らせる手段がないので こういう工夫が必要だと思います。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import json
import types
import time
import picamera
import os
import sys
import pyaudio
import pygame.mixer
import wave

# MSEmotionAPIのwait
SHORTSLEEP = 10
LONGSLEEP = 60

# 音声録音のパラメータ
chunk = 1024*2
FORMAT = pyaudio.paInt16
CHANNELS = 1
#サンプリングレート、マイク性能に依存
RATE = 16000
#録音時間
RECORD_SECONDS = 5

# pygame
pygame.mixer.init()

try:
  while True:
        # MS Emotion APIで顔の認識を行う
        with picamera.PiCamera() as camera:
                camera.start_preview()
                camera.hflip = True
                camera.vflip = True
                time.sleep(2) #カメラ初期化
                camera.capture('foo.jpg')

        url = 'https://api.projectoxford.ai/emotion/v1.0/recognize'
        MS_APIKEY = '【Microsoft Emotion APIのキー】'
        headers = {'Content-type': 'application/octet-stream', 'Ocp-Apim-Subscription-Key': MS_APIKEY}
        payload = open('./foo.jpg', 'rb').read()

        r = requests.post(url, data=payload, headers=headers)
        data = r.json()

        ninshiki = len(data)

        # 顔認識が出来たらNoby APIにアクセスしておみくじを引く

        if ninshiki > 0:
                dict = data[0]['scores']
                max_key = max(dict, key=(lambda x: dict[x]))

                print "key: %s,\t value:%f" %(max_key, dict[max_key])

                ENDPOINT = 'https://www.cotogoto.ai/webapi/noby.json'
                NOBY_APIKEY = '【NobyのAPIキー】'

                payload = {'text': 'おみくじ引きたいな。', 'app_key': NOBY_APIKEY}
                r = requests.get(ENDPOINT, params=payload)
                data = r.json()

                response = data['text']
                # print "response: %s" %(response)

                uranai = response.split("\r\n")[1]
                print uranai

                # おみくじのフレーズを話す
                voice1 = 'こんにちは、今日は来てくれてありがとう。今日の運勢を占うね。'
                # shを呼び出す。
                cmdtext = './jtalk2.sh ' + uranai
                # print cmdtext

                os.system('./jtalk2.sh ' + voice1) #成功すると0 # import os
                os.system(cmdtext.encode('utf-8')) #成功すると0 # import os

                os.system('./jtalk2.sh ' + '最近あったできごとを教えて?')
                print 'マイクに5秒間話しかけてください >>>'

                # pygame
                pygame.mixer.music.load("./Q.mp3")
                pygame.mixer.music.play()
                time.sleep(1)
                pygame.mixer.music.stop()

                # pyaudio
                p = pyaudio.PyAudio()
                #マイク0番からの入力を5秒間録音し、ファイル名:voice.wavで保存する。
                #マイク0番を設定
                input_device_index = 0
                #マイクからデータ取得
                stream = p.open(format = FORMAT,
                                channels = CHANNELS,
                                rate = RATE,
                                input = True,
                                frames_per_buffer = chunk)
                all = []
                for i in range(0, RATE / chunk * RECORD_SECONDS):
                        data = stream.read(chunk)
                        all.append(data)

                stream.close()
                data = ''.join(all)
                out = wave.open('voice.wav','w')
                out.setnchannels(1) #mono
                out.setsampwidth(2) #16bits
                out.setframerate(RATE)
                out.writeframes(data)
                out.close()

                p.terminate()
                print '<<< 録音完了'

                pygame.mixer.music.load("./OK.mp3")
                pygame.mixer.music.play()
                time.sleep(1)
                pygame.mixer.music.stop()

                path = 'voice.wav'
                DOCOMO_APIKEY = '【docomo音声認識APIのキー】'
                url = "https://api.apigw.smt.docomo.ne.jp/amiVoice/v1/recognize?APIKEY={}".format(DOCOMO_APIKEY)
                files = {"a": open(path, 'rb'), "v":"on"}
                r = requests.post(url, files=files)
                print r.json()['text']

                payload = {'text': r.json()['text'], 'app_key': NOBY_APIKEY}
                r = requests.get(ENDPOINT, params=payload)
                print r.json()['text']

                cmdtext = './jtalk2.sh ' + r.json()['text']
                os.system(cmdtext.encode('utf-8')) #成功すると0 # import os

                os.system('./jtalk2.sh ' + '教えてくれてありがとう。また会いにきてね!')

                print 'LONG_SLEEP_MODE'
                time.sleep(LONGSLEEP)
        else:
                print 'SHORT_SLEEP_MODE'
                time.sleep(SHORTSLEEP)

except KeyboardInterrupt:
  sys.exit()

もう少し関数とかクラスとかを使用するときれいにまとまりますが、それは今後の自分のpythonスキルの向上のために今後修正していこうと思います。 また、音声認識に関しては非常にネットワークAPIとの相性が良くない(特にリアルタイムのレスポンスを求めるものは難しい)のでスレッドなどを使用したりなどのイライラさせない工夫がカギになってくると思います。

まとめ

やっと音声関係というか「ヒミツのクマちゃん」を使ったもので完結編になりました。 軽い気持ちで購入したヒミツのクマちゃんですが、ちゃんとした会話のできるロボットになってくれました。 ぬいぐるみの出来がよいので、最終的にはRaspberryPiをどういう風に隠すか、マイクをどのようにするかなど考えればきりがないのですが。 それも含めて工作や工夫の醍醐味のあるテーマだったかなと思います。

Raspberry Pixelを古いPCにインストールしてみる

RASPBIAN JESSIE WITH PIXELが発表されましたが、更にこれにPCとMACに対応したバージョンが登場しました。 Raspberry Pixelは軽量デスクトップOSとして使用できそうです。昔から古いPCにUbuntuにをインストールしようと思っていたので これを機にPixelをインストールしようと思います。

掲載ページ

www.raspberrypi.org

The MagPi magazineという雑誌のおまけについたDVDの公開されたもののようですが、そのイメージをダウンロードできるようになっています。

ダウンロードリンク

http://downloads.raspberrypi.org/pixel_x86/images/pixel_x86-2016-12-13/2016-12-13-pixel-x86-jessie.iso

今回はUSBメモリに関してはSANDISKのコンパクトタイプのものAmazonでは1,000円ほどですが、秋葉原あたりではもう少し安く購入できました。 出っ張りも少ないし、アクセススピードも結構早いかなと思います。

そしてターゲットとなる古いPCは以下になります。もう購入してから5~6年ほど立ったものかなと思いますが、【Acer Aspire One 753】となります。 スペックとしては以下のようになります。以前からメモリは増設してあったものなので4Gほど搭載されています。Windows7のプリインストールモデルだったのですが、 Windows10ではSSDに換装してもCPUが100%になることもしばしばという状態だったので、ほどんど使っていませんでした。

www.notebookcheck.net

かなり旧式のCeleronですし、しかたないです。

公式サイドからISOファイルをダウンロードしたらサイト推奨の【Etcher】というツールを使ってイメージ書き込みをUSBにするわけですが、 【Win32DiskImager】でも試してみましたが問題なくできました。

etcher.io

sourceforge.net

あとは、USBを挿して起動すれば問題なくOSが起動できます。

https://www.raspberrypi.org/wp-content/uploads/2016/12/VirtualBox_PIXEL-CD_18_12_2016_10_25_32.png

https://www.raspberrypi.org/wp-content/uploads/2016/12/splash-768x576.jpeg

f:id:ueponx:20161225214324p:plain

f:id:ueponx:20161225214328p:plain

ChromiumVLCもインストールされているのでそれなりに使用できるようです。使用したPCでも負荷もほとんどないようなので常用できそうです。ただし、キーマップとか日本語化はしないといけないようですし、コマンド関係もかなり絞られているようなので別途インストールは必要の様です。

使用してみた感想としてはわりといい感じです。むしろPCで使用するよりもVMとして使用したほうがいいのかも?

【関連】

uepon.hatenadiary.com

RaspberryPiのマイクで録音した音声をテキスト化する【ヒミツのクマちゃん その3】

RaspberryPiのマイクで録音した音声をテキスト化する

ヒミツのくまちゃんと話すための企画その2になります。

前回のエントリーではマイクで録音するところまで来ました。

uepon.hatenadiary.com

後は録音した音声をテキスト化することができれば、拡張性(雑談APIやNobyに渡すことができるので会話っぽい表現を実装可)が増えそうです。

音声のテキスト化(音声認識)のAPI

あたりがいいかなと思いました。(docomoさんのは内部的にはAmiVoiceっぽい) ということでdocomoさんのAPIを使用します。(後日、GoogleさんのAPIも触る予定ですが)

基本的な情報は以下のURLにあります。

dev.smt.docomo.ne.jp

基本動作は音声データを送信すると音声認識したデータをテキスト化してJSON化して返信するものです。つまり、データの送り方(REST)とデータの形式(ファイルのバイナリフォーマット)が分かれば概ね処理ができます。

f:id:ueponx:20161217091728j:plain

今回は特別なSDKなどを使用せず、RESTで通信を行いたかったのでdocomoさんのAPIの中の音声認識API【Powered by アドバンスト・メディア】を使用します。

以下は上記サイトの引用です。

音声認識API【Powered by アドバンスト・メディア】とは
音声データをREST形式で送信するだけで音声認識をすることができます。

主な特徴
* HTTPで音声データをPOSTするだけなので、AndroidiOSに限らず様々なプラットフォームでのご利用が可能です。
* クライアントアプリケーションに特殊なライブラリを組み込む必要がないため自由度の高い実装が可能です。
* クライアントアプリケーションにライブラリを組み込んでリアルタイム認識を行うタイプの音声認識と比較して、応答速度が遅い為、レスポンスが求められる用途には向いていません。

ドキュメントを眺めてみる

以下のURLがドキュメントページになります。

https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=speech_recognition&p_name=api_amivoice_1#tag01

サービスのエンドポイント

アクセスを行うエンドポイントは以下となります。

https://api.apigw.smt.docomo.ne.jp/amiVoice/v1/recognize

amiVoiceって書いてあるw

リクエストクエリパラメータ

キー 必須 説明
APIKEY APIにアクセスするアプリの認証に利用する

リクエストヘッダのMIME-TYPE

HTTPで送信するデータ形式MIMEタイプはこのようになっている?

キー 必須 説明
Content-Type MIMEタイプmultipart/form-data; boundary=<バウンダリ文字列>

あれRESTアクセスだし、MIMEタイプは単純なOctet-Streamなんじゃねーの?んが。

やられました

とはいってもマルチパートでってことはファイル+制御用のJSONを送ればいいのかなと予想がつきます。

リクエストパラメータ(マルチパート化されたデータの中身)

キー 必須 説明
a 音声のバイナリデータ。10秒を超える音声データは途中で打ち切られます。音声データのフォーマット:PCM(MSB)16khz/16bit
v - 発話区間検出処理。"on"を指定すると、音声データの無音部分を無視して音声認識処理を行います。

このあたりをマルチパート化して送信するようです。

音声ファイルのフォーマットはサンプリングレート16kHz、16bitの10秒以内の音声でいいようなので、PyAudioへは

FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000

のようにパラメータを指定すれば問題なさそうです。

また、vのキーに関しては制度を気にしなければ問題はなさそうなのですが、無音部分は無視してもらわないとかなり精度がわるそうな気がするので、指定をすることにします。

ドキュメントを見てもわかりますが以下のように通信データが送信されます。

POST https://api.apigw.smt.docomo.ne.jp/amiVoice/v1/recognize?APIKEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: multipart/form-data; boundary=<バウンダリ文字列>
--<バウンダリ文字列>
Content-Disposition: form-data; name="v"

on
--<バウンダリ文字列>
Content-Disposition: form-data; name="a"; filename="sample.adc"
Content-Type: application/octet-stream

音声データ(binary)
--<バウンダリ文字列>--

データの格納さえうまく調整できれば、Arduinoとかmbedからでも通信できそうな印象です。

ここまでわかれば、あとはpythonのrequestsモジュールでマルチパート化してPOSTする処理が わかれば、使用できそうです。

pythonのrequestsモジュールでマルチパート化処理を行う

こちらを参考にしました。

クイックスタート — requests-docs-ja 1.0.4 documentation

stackoverflow.com

ふむふむ、複数のデータはマルチパート化してPOSTするときには、以下のどちらかの形式でタプル化してpostの引数に与えれば良いようです。

  • (キー, データ)
  • (キー, データ, Content-Type)
  • (キー, データ, Content-Type, ヘッダー)

例えば、StackOverflowのサンプルをみてみれば、

  • キー=>foo、データ=>bar
  • キー=>spam、データ=>eggs

というデータセットをマルチパート化する場合には以下のような処理になります。

requests.post('http://…', files=(('foo', 'bar'), ('spam', 'eggs')))
# または
files = {'foo':'bar', 'spam':'eggs'}
requests.post('http://…', files=files)

今回のAPIではavというキーにデータを入れることになるので

files = {"a": 【音声ファイルのデータ】, "v":"on"}
r = requests.post(url, files=files)

こんな感じになるでしょう。あとはファイルの読み込みですがopen()で読みすればよさそうです。以下は処理の抜粋になります。

path = 'voice.wav'

files = {"a": open(path, 'rb'), "v":"on"}
r = requests.post(url, files=files)

これで一応大体の構想ができました。

API利用登録

では、登録を行っていきます。 下記のdocomoDeveloperSupportのページからログインを行ってください。SNS系のログインでも問題ありません。

dev.smt.docomo.ne.jp

次に音声認識のページの中ほどにある【申請する】のボタンをクリックします。

f:id:ueponx:20161219232846j:plain

すると、【API使用申請】のページに遷移します。

f:id:ueponx:20161219232944j:plain

ここで使用するアプリケーションの情報を登録することになります。 登録時点で決まっていなくても後で修正ができるので問題ありません。

入力が終わったら【API機能選択へ】のボタンをクリックします。

f:id:ueponx:20161219233148j:plain

API機能選択】の一覧画面に遷移するので、その中から

を選択します。他のAPIを含めても問題ありませんが、今回は1つだけにします。選択が終了したら下の方にある【利用するAPI利用規約に同意して次へ】をクリックします。

f:id:ueponx:20161219233928j:plain

【確認画面】に遷移するので、内容確認後【利用申請する】ボタンをクリックします。

f:id:ueponx:20161219234115j:plain

これで申請が完了しました。

f:id:ueponx:20161219234202j:plain

詳細は【マイページ】にある【API利用申請・管理】画面の【登録アプリケーション一覧】に情報が表示されます。API使用のキーに関してもここで確認ができます。

f:id:ueponx:20161219234228j:plain

これで音声認識APIが使用できるようになりました。

音声認識APIにデータを送信する

音声認識APIにデータを送信して、認識結果のテキスト情報を確認してみます。まずは録音した音声データがあればそれを使用します。 音声のデータフォーマットが10秒以内、サンプリングレート16kHz、サンプル16bitになっていない場合にはエラーとなるので注意が必要です。 今回はvoice.wavというファイルを送信することにします。

前述の通りpythonでデータを送信するコードを作成すると以下のようになります。認識された結果はJSON形式で返されるのですが、その中でtextのデータが認識された文書全体となります。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
path = 'voice.wav'
APIKEY = '【発行されたAPIのキー】'
url = "https://api.apigw.smt.docomo.ne.jp/amiVoice/v1/recognize?APIKEY={}".format(APIKEY)
files = {"a": open(path, 'rb'), "v":"on"}
r = requests.post(url, files=files)
print r.json()['text']

このプログラムを実行すると以下のようになります。

$ python sample.py
、おまえ滑舌悪いんじゃないの。

無事に認識できたようです。(実験中に嫁に言われました…涙)

録音機能と連携させる

先程は事前に収録したものでテストしましたが、今度はPyAudioと連携し、録音後API側に対して要求を出すものにします。とは言っても、データは一度ファイルとして蓄積するので先程の処理とは大きく変わらないと思います。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

#マイクからの入力を5秒間録音し、ファイル名:voice.wavで保存する。

import requests
import pyaudio
import sys
import time
import wave

chunk = 1024*2
FORMAT = pyaudio.paInt16
CHANNELS = 1
#サンプリングレート、マイク性能に依存
RATE = 16000
#録音時間
RECORD_SECONDS = 5 #input('収録時間をしていしてください(秒数) >>>')

print 'マイクに5秒間話しかけてください >>>'

#pyaudio
p = pyaudio.PyAudio()

#マイク0番を設定
input_device_index = 0
#マイクからデータ取得
stream = p.open(format = FORMAT,
all = []
for i in range(0, RATE / chunk * RECORD_SECONDS):
        data = stream.read(chunk)
        all.append(data)

stream.close()
data = ''.join(all)
out = wave.open('voice.wav','w')
out.setnchannels(1) #mono
out.setsampwidth(2) #16bit
out.setframerate(RATE)
out.writeframes(data)
out.close()

p.terminate()

print '<<< 録音完了'

path = 'voice.wav'
APIKEY = '【発行されたAPIのキー】'
url = "https://api.apigw.smt.docomo.ne.jp/amiVoice/v1/recognize?APIKEY={}".format(APIKEY)
files = {"a": open(path, 'rb'), "v":"on"}
r = requests.post(url, files=files)
print r.json()['text']

実行結果

$ python pyaudio_amivoice.py
マイクに5秒間話しかけてください >>>
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
<<< 録音完了
おはようございます。。

いろいろWarningは出ていますが、うまく動きました。

まとめ

一応録音した音声をテキスト化することができました。更にこれを雑談APIやNobyAPIなどに送信すれば会話を行うことができるようになります。 もう少しでクマちゃんとの会話ができるようになるのではないかと思います。

次回は会話っぽいやり取りまで進めたいと思います。

RaspberryPiでマイク録音してみる【ヒミツのクマちゃん その2】

RaspberryPiでマイク録音してみる

前回のエントリでヒミツのクマちゃんが占いをしてくれるようになったので、今度は会話したくなってきました。そこでその第一歩としてRaspberryPiにマイクを接続しようと思います。

qiita.com

こちらの記事を参考にしています。

以下のマイクが実績があるとのことだったので購入することにしました。

値段的にはピン形状のマイク(アナログ)でもいいかなと思うのですが、ノイズが結構入るかなと思ったのでUSB接続にしました。

マイクを接続してみる。

マイクをUSBに接続してみてlsusbで認識状況を確認します。

【接続前】

$ lsusb
Bus 001 Device 006: ID 0a12:0001 Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode)
Bus 001 Device 005: ID 2019:1201 PLANEX
Bus 001 Device 004: ID 05e3:0608 Genesys Logic, Inc. USB-2.0 4-Port HUB
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp. LAN9500 Ethernet 10/100 Adapter / SMSC9512/9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

【接続後】

$ lsusb
Bus 001 Device 007: ID 0d8c:0134 C-Media Electronics, Inc.
Bus 001 Device 006: ID 0a12:0001 Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode)
Bus 001 Device 005: ID 2019:1201 PLANEX
Bus 001 Device 004: ID 05e3:0608 Genesys Logic, Inc. USB-2.0 4-Port HUB
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp. LAN9500 Ethernet 10/100 Adapter / SMSC9512/9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

比較すると一番上の行に以下のエントリが追加されていることがわかります。

Bus 001 Device 007: ID 0d8c:0134 C-Media Electronics, Inc.

以前使用していたUSBスピーカーもC-Media製だったような気がするのでC-Media製の音声デバイスって多いみたい。

何もしなくてもデバイスの認識はされたような感じですね。

通常であれば

/etc/modprobe.d/alsa-base.confを編集して、使用するオーディオモジュールの優先順位の変更して、RaspberryPiのリブート

の処理を行うことになるのですが、実は行わなくても録音は可能です。

alsaを使用して録音してみる。

基本的なテストはCLIalsaのコマンド群を使用することで確認ができます。

録音に使用できるデバイス一覧の確認

$ arecord -l
**** ハードウェアデバイス CAPTURE のリスト ****
カード 1: Device [USB PnP Audio Device], デバイス 0: USB Audio [USB Audio]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0

複数ある場合にはいくつかのデバイスが列挙できますが、今回は1つしか存在していないのでこのような表示になります。

ここで覚えて置かなければならなのは カード番号が1、デバイス番号が0であることです。

先程オーディオモジュールの優先順位を変更を飛ばしたといいましたが、デフォルト値ではなく、デバイス指定を行うので不要でしたということになります。

録音を行う

$ arecord -D plughw:1,0 test.wav
録音中 WAVE 'test.wav' : Unsigned 8 bit, レート 8000 Hz, モノラル
^Cシグナル 割り込み で中断...

wavファイルはデフォルトの設定で録音します(データは8bit、サンプリングレートは8kHzのモノラル)。 コマンドスイッチで-D plughw:1,0としている部分がデバイスを明示的にしている部分となります。明示的に指定しないとデフォルト値(デバイスの優先順位が高いものを)探しに行くので注意が必要です。

先程のarecord -lコマンドの結果で、「カード番号が1、デバイス番号が0」と検出されていたので、この値を-Dで渡しています。

再生する

再生に関しては以下となります。

$ aplay test.wav
再生中 WAVE 'test.wav' : Unsigned 8 bit, レート 8000 Hz, モノラル

録音のパラメータも正しく認識されているので問題ありません。

録音ボリューム調整

録音すると音量が小さいことがあります。その場合には以下で調整することになります。

$ amixer sset Mic 46  -c 1
Simple mixer control 'Mic',0
  Capabilities: cvolume cvolume-joined cswitch cswitch-joined
  Capture channels: Mono
  Limits: Capture 0 - 62
  Mono: Capture 46 [74%] [14.62dB] [on]

ここで-cで渡している番号カード番号となるので、arecord -lコマンドの結果で、「カード番号が1、デバイス番号が0」と検出されていたので、カード番号の1を渡し、-c 1となります。

最小は0で最大は62まで指定できるようです。

録音ボリュームの値を確認するだけであれば以下で確認できます。

$ amixer sget Mic -c 1
Simple mixer control 'Mic',0
  Capabilities: cvolume cvolume-joined cswitch cswitch-joined
  Capture channels: Mono
  Limits: Capture 0 - 62
  Mono: Capture 46 [74%] [14.62dB] [on]

これでデバイスの基本的な機能の確認ができました。

pythonから録音してみる

これからは毎度のことですが、いつも通りpythonから使用できるかを確認することになります。ネットを調べるとpyaudioというモジュールを使うと良さそうです。

people.csail.mit.edu

pipでインストールしてみる

ではpipコマンドを使用してインストールしてみます。

$ pip install pyaudio
Downloading/unpacking pyaudio
  Downloading PyAudio-0.2.9.tar.gz (289kB): 289kB downloaded
  Running setup.py (path:/tmp/pip-build-uNj4Vs/pyaudio/setup.py) egg_info for package pyaudio

    warning: no files found matching '*.c' under directory 'test'
Installing collected packages: pyaudio
  Running setup.py install for pyaudio
    building '_portaudio' extension
    arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -I/usr/include/python2.7 -c src/_portaudiomodule.c -o build/temp.linux-armv6l-2.7/src/_portaudiomodule.o
    src/_portaudiomodule.c:28:20: fatal error: Python.h: そのようなファイルやデ ィレクトリはありません
     #include "Python.h"
                        ^
    compilation terminated.
    error: command 'arm-linux-gnueabihf-gcc' failed with exit status 1
    Complete output from command /usr/bin/python -c "import setuptools, tokenize;__file__='/tmp/pip-build-uNj4Vs/pyaudio/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-RuGTpw-record/install-record.txt --single-version-externally-managed --compile:
    running install

running build

running build_py

creating build

creating build/lib.linux-armv6l-2.7

copying src/pyaudio.py -> build/lib.linux-armv6l-2.7

running build_ext

building '_portaudio' extension

creating build/temp.linux-armv6l-2.7

creating build/temp.linux-armv6l-2.7/src

arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -I/usr/include/python2.7 -c src/_portaudiomodule.c -o build/temp.linux-armv6l-2.7/src/_portaudiomodule.o

src/_portaudiomodule.c:28:20: fatal error: Python.h: そのようなファイルやディレ クトリはありません

 #include "Python.h"

                    ^

compilation terminated.

error: command 'arm-linux-gnueabihf-gcc' failed with exit status 1

----------------------------------------
Cleaning up...
Command /usr/bin/python -c "import setuptools, tokenize;__file__='/tmp/pip-build-uNj4Vs/pyaudio/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-RuGTpw-record/install-record.txt --single-version-externally-managed --compile failed with error code 1 in /tmp/pip-build-uNj4Vs/pyaudio
Traceback (most recent call last):
  File "/usr/bin/pip", line 9, in <module>
    load_entry_point('pip==1.5.6', 'console_scripts', 'pip')()
  File "/usr/lib/python2.7/dist-packages/pip/__init__.py", line 248, in main
    return command.main(cmd_args)
  File "/usr/lib/python2.7/dist-packages/pip/basecommand.py", line 161, in main
    text = '\n'.join(complete_log)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 57: ordinal not in range(128)

エラー?

先程のサイトをみると

pip will download the PyAudio source and build it for your system. Be sure to install the portaudio library development package (portaudio19-dev) and the python development package (python-all-dev) beforehand. For better isolation from system packages, consider installing PyAudio in a virtualenv.

という記述が。インストール時にビルドするので事前にportaudio19-devpython-all-devのパッケージは事前に入れてねということみたいです。PyAudioはportaudioの機能を使用するモジュールなので確かに必要ですね。

今回はお手軽さを目指したいのでapt-getでインストールします。

apt-getでインストールしてみる

$ sudo apt-get install python-pyaudio python3-pyaudio

パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
  gyp libasn1-8-heimdal libc-ares-dev libc-ares2 libgssapi3-heimdal
  libhcrypto4-heimdal libheimbase1-heimdal libheimntlm0-heimdal
  libhx509-5-heimdal libjs-node-uuid libjs-underscore libkrb5-26-heimdal
  libroken18-heimdal libssl-dev libssl-doc libv8-3.14-dev libv8-3.14.5
  libwind0-heimdal rlwrap
これを削除するには 'apt-get autoremove' を利用してください。
提案パッケージ:
  python-pyaudio-doc
以下のパッケージが新たにインストールされます:
  python-pyaudio python3-pyaudio
アップグレード: 0 個、新規インストール: 2 個、削除: 0 個、保留: 181 個。
46.8 kB のアーカイブを取得する必要があります。
この操作後に追加で 194 kB のディスク容量が消費されます。
取得:1 http://mirrordirector.raspbian.org/raspbian/ jessie/main python-pyaudio armhf 0.2.8-1 [23.2 kB]
取得:2 http://mirrordirector.raspbian.org/raspbian/ jessie/main python3-pyaudio armhf 0.2.8-1 [23.6 kB]
46.8 kB を 1秒 で取得しました (30.8 kB/s)
以前に未選択のパッケージ python-pyaudio を選択しています。
(データベースを読み込んでいます ... 現在 129542 個のファイルとディレクトリがインストールされています。)
.../python-pyaudio_0.2.8-1_armhf.deb を展開する準備をしています ...
python-pyaudio (0.2.8-1) を展開しています...
以前に未選択のパッケージ python3-pyaudio を選択しています。
.../python3-pyaudio_0.2.8-1_armhf.deb を展開する準備をしています ...
python3-pyaudio (0.2.8-1) を展開しています...
python-pyaudio (0.2.8-1) を設定しています ...
python3-pyaudio (0.2.8-1) を設定しています ...

python3はまだ使用する気はないですが、一応入れます。

PyAudioで録音を行う

公式サイトのサンプルを実験してみます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""PyAudio example: Record a few seconds of audio and save to a WAVE file."""

import pyaudio
import wave

CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 44100
RECORD_SECONDS = 5
WAVE_OUTPUT_FILENAME = "output.wav"

p = pyaudio.PyAudio()

stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                frames_per_buffer=CHUNK)

print("* recording")

frames = []

for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
    data = stream.read(CHUNK)
    frames.append(data)

print("* done recording")

stream.stop_stream()
stream.close()
p.terminate()

wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb')
wf.setnchannels(CHANNELS)
wf.setsampwidth(p.get_sample_size(FORMAT))
wf.setframerate(RATE)
wf.writeframes(b''.join(frames))
wf.close()

これを実行させると…

$ python pyaudio_sample.py
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
Expression 'parameters->channelCount <= maxChans' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 1514
Expression 'ValidateParameters( inputParameters, hostApi, StreamDirection_In )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2818
Traceback (most recent call last):
  File "pyaudio_sample.py", line 19, in <module>
    frames_per_buffer=CHUNK)
  File "/usr/lib/python2.7/dist-packages/pyaudio.py", line 747, in open
    stream = Stream(self, *args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/pyaudio.py", line 442, in __init__
    self._stream = pa.open(**arguments)
IOError: [Errno Invalid number of channels] -9998

おや?エラーになってしまいました。エラーメッセージをみるとbuffer関連でエラーが出ています。

  File "pyaudio_sample.py", line 19, in <module>
    frames_per_buffer=CHUNK)

いろいろ実験を行った結果録音のパラメータがハードウエアに対応していなかったりするとこのようなエラーがでるようです。そこでパラメータを以下のように変更してみます。

【冒頭の設定部分】

CHUNK = 1024 * 2
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
RECORD_SECONDS = 5
WAVE_OUTPUT_FILENAME = "output.wav"

バッファのサイズを倍、チャンネル数をモノラル、レートを16kHzへ変更してみました。

再度実行を行うと

$ python pyaudio_sample.py
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
* recording
* done recording

いろいろalsaやportaudioからアラートは出ていますが、無事に録音できました。

PyAudioで再生を行う

PyAudioは再生ももちろんできるのですが… 公式サイトの再生のサンプルを使ってみます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""PyAudio Example: Play a WAVE file."""

import pyaudio
import wave
import sys

CHUNK = 1024

if len(sys.argv) < 2:
    print("Plays a wave file.\n\nUsage: %s filename.wav" % sys.argv[0])
    sys.exit(-1)

wf = wave.open(sys.argv[1], 'rb')

p = pyaudio.PyAudio()

stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                channels=wf.getnchannels(),
                rate=wf.getframerate(),
                output=True)

data = wf.readframes(CHUNK)

while data != '':
    stream.write(data)
    data = wf.readframes(CHUNK)

stream.stop_stream()
stream.close()

p.terminate()

これを実行すると…

$ python pyaudio_sample_play.py output.wav
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started

こちらもいろいろアラートが表示されますが、再生はできます…しかし、なんかノイズのっています。aplayで再生するとノイズが乗っていないので再生時にノイズがのっているようですが、原因はわかりませんでした。(処理能力などの問題でしょうか。)

単に再生するだけならpygameでも再生はできるのでそっちを使ってもいいかもしれません。(ループなど再生機能が多機能なので)

今回はpythonから録音ができたので、次は会話ができるように拡張をしていきたいと思います。

RaspberryPiで Cotogoto::Noby APIを使ってみる【ヒミツのクマちゃん その1】

RaspberryPiでCotogoto::Noby APIを使ってみる

前回のエントリではかなり尻切れトンボのような感じで終わってしまったのですが 今回はその続きになります。

今回もpythonからのアクセスになるので定番のライブラリであるrequestsを使用することになります。Snipetのサンプルである占いの機能を実装させてみます。

Noby APIでは会話だけでなくおみくじをひくことができます。API界隈では占いっぽいものもあるのですが、ほとんどが有料なのでそれに近いおみくじの機能はなかなかイケてると思っています。

以下のように記述します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import json
import types

ENDPOINT = 'https://www.cotogoto.ai/webapi/noby.json'
MY_KEY = '【取得したAPIKEY】'

payload = {'text': 'おみくじ引きたいな。', 'app_key': MY_KEY}
r = requests.get(ENDPOINT, params=payload)
data = r.json()

response = data['text']
print "response: %s" %(response)

DoCoMoの雑談対話APIではPOSTメソッドでリクエストを送っていましたが、Noby APIではGETメソッドを使用するので敷居はかなり低くなります。特にArduinoやmbedなどでもサンプルに類似した形式で簡単に記述ができそうです。

送信したデータのtextにおみくじの内容が格納されているのでそれ使用することになります。 実際には以下のようなJSONデータがレスポンスとして返されます。

{
  "art": null,
  "commandId": "NOBY_004",
  "commandName": "おみくじ",
  "dat": "おはようございます",
  "emotion": {
    "angerFear": 0,
    "joySad": 0,
    "likeDislike": 0,
    "word": null
  },
  "emotionList": [],
  "loc": null,
  "mood": 0,
  "negaposi": 0,
  "negaposiList": [
    {
      "score": -0.30000001192092896,
      "word": "引き"
    }
  ],
  "org": null,
  "psn": null,
  "text": "小吉\r\nあなたの心を平和にして、他人のためにつくせば吉です。\r\nまずはあなたの家庭を平和にしましょう。\r\nそして、あなたの周りからだんだんに平和な気持ちを広げていきます。\r\nそうすれば、周りにどんな波風が立っても、あなたとその周囲だけには春風が吹きます。\r\nその春風を、また周囲に広げていきましょう。\r\n【願い事】他人のため、という気持ちがあればかないます。\r\n【待ち人】遅れますが来ます。\r\n【失し物】男の人に聞いてみましょう。\r\n【旅行】早い旅立ちがいいでしょう。\r\n【ビジネス】いいでしょう。\r\n【学問】もうちょっと勉強しましょう。\r\n【争い事】人に頼んだほうがうまくいきます。\r\n【恋愛】再出発もいいかも知れません。\r\n【縁談】あなたの心が晴れやかならば疑いは晴れてうまくいきます。\r\n【転居】問題ありません。\r\n【病気】いい医師に出会うよう努力しましょう。そうすれば治ります。",
  "tim": null,
  "type": "Command",
  "wordList": [
    {
      "feature": "名詞,一般,*,*,*,*,おみくじ,*,*,",
      "start": "0",
      "surface": "おみくじ"
    },
    {
      "feature": "動詞,自立,*,*,五段・カ行イ音便,連用形,引く,ヒキ,ヒキ,",
      "start": "1",
      "surface": "引き"
    },
    {
      "feature": "助動詞,*,*,*,特殊・タイ,基本形,たい,タイ,タイ,",
      "start": "2",
      "surface": "たい"
    },
    {
      "feature": "助詞,終助詞,*,*,*,*,な,ナ,ナ,",
      "start": "3",
      "surface": ""
    },
    {
      "feature": "記号,句点,*,*,*,*,。,*,。,",
      "start": "4",
      "surface": ""
    }
  ]
}

若干の難点としては結構文面が長いということでしょうか。文面は\r\nで区切られてるので、後は分割すれば使えます。個人的にはおみくじの結果よりも2文節目のフレーズのほうが占いっぽいのでいいかなと思います。

使えたけど…あんまりこれだけでは面白くない。

これではこれまでのサンプルをそのまま使っただけの糞エントリーになってしまうので、今回はもう少し発展させてみたいと思います。

今回はRaspberryPiにつけたカメラで人間の顔が認識できたら、おみくじを引いてフレーズを音声として出力させてみたいと思います。

顔の認識に関しては

uepon.hatenadiary.com

発話(TextToSpeach)に関しては

uepon.hatenadiary.com

といった過去のエントリーを思い出して行うことになります。

そして今回の一番の目玉は音声出力を行う「ヒミツのクマちゃん」になります。

これでぐっとサービスっぽくなります。本当は「BOCCO」とかが入手できるとまた違ったこともできるんですが… 「ヒミツのクマちゃん」はハッカソンで同じチームになった小林さんに教えてもらいました。ありがとうございます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import json
import types
import time
import picamera
import os
import sys

SHORTSLEEP = 10
LONGSLEEP = 60

try:
  while True:
        # MS Emotion APIで顔の認識を行う
        with picamera.PiCamera() as camera:
                camera.start_preview()
                camera.hflip = True
                camera.vflip = True
                time.sleep(2) #カメラ初期化
                camera.capture('foo.jpg')

        url = 'https://api.projectoxford.ai/emotion/v1.0/recognize'
        headers = {'Content-type': 'application/octet-stream', 'Ocp-Apim-Subscription-Key':'【Emotion APIのキー】'}
        payload = open('./foo.jpg', 'rb').read()

        r = requests.post(url, data=payload, headers=headers)
        data = r.json()

        ninshiki = len(data)

        # 顔認識が出来たらNoby APIにアクセスしておみくじを引く

        if ninshiki > 0:
                dict = data[0]['scores']
                max_key = max(dict, key=(lambda x: dict[x]))

                print "key: %s,\t value:%f" %(max_key, dict[max_key])

                ENDPOINT = 'https://www.cotogoto.ai/webapi/noby.json'
                MY_KEY = '【Noby APIのキー】'

                payload = {'text': 'おみくじ引きたいな。', 'app_key': MY_KEY}
                r = requests.get(ENDPOINT, params=payload)
                data = r.json()

                response = data['text']
                # print "response: %s" %(response)

                uranai = response.split("\r\n")[1]
                print uranai

                # おみくじのフレーズを話す
                voice1 = 'こんにちは、今日は来てくれてありがとう。'
                voice2 = '今日の運勢を占うね。'

                # shを呼び出す。
                cmdtext = './jtalk.sh ' + uranai
                # print cmdtext

                os.system('./jtalk.sh ' + voice1) #成功すると0 # import os
                os.system('./jtalk.sh ' + voice2) #成功すると0 # import os
                os.system(cmdtext.encode('utf-8')) #成功すると0 # import os
                print 'LONG_SLEEP_MODE'
                time.sleep(LONGSLEEP)
        else:
                print 'SHORT_SLEEP_MODE'
                time.sleep(SHORTSLEEP)

except KeyboardInterrupt:
  sys.exit()

pythonから呼び出しているjtalk.shのshは以下の通り

#!/bin/bash

HTSVOICE=/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice
#HTSVOICE=/usr/share/hts-voice/mei/mei_normal.htsvoice

voice=`tempfile`
option="-m $HTSVOICE \
  -s 16000 \
  -p 100 \
  -a 0.03 \
  -u 0.0 \
  -jm 1.0 \
  -jf 1.0 \
  -x /var/lib/mecab/dic/open-jtalk/naist-jdic \
  -ow $voice"

if [ -z "$1" ] ; then
  open_jtalk $option
else
  if [ -f "$1" ] ; then
    open_jtalk $option $1
  else
    echo "$1" | open_jtalk $option
  fi
fi

aplay -q $voice
rm $voice

処理の流れとしては以下の様になっています。

  • RaspberryPIのカメラモジュールで画像を撮影
  • 撮影した画像データをEmoion APIへOctetStreamで送信し解析
  • 画像内に人の顔があれば、NobyAPIへおみくじを要求。
  • 画像内に人がいなければ、短時間のスリープ
  • おみくじ結果が得られたらjtalk.sh(OpenJTalkで話す機能をもつシェルスクリプト)の引数におみくじの結果をいれて長時間のスリープ
  • 以上を繰り返す

ネットワーク越しにAPIを叩いているので遅延があることは覚悟する必要はあります。気になる場合には機器を設置するシチュエーションでカバーすれば感覚として軽減はできそうです。(椅子を置いて座ってもらうなど)

実際に動かした状態は以下で


クマ占い

カメラの設置位置が難しいですが、専用の椅子やリュックなどを作成して自然に見せるのも楽しいかなと思います。

/* -----codeの行番号----- */