Ubuntu Weekly Recipe

第360回AquesTalk PiでUbuntuにおしゃべりさせよう

先週のレシピでは、アラート発生時の警告音をAquesTalk2で作成しました。今週のレシピではRaspberry Pi用のAquesTalk、AquesTalk Piを使い、リアルタイムに様々な音声を喋らせてみようと思います。

AquesTalk Piは、Raspberry Pi上で動作する音声合成ソフトウェアです。Rspberry PiとRaspbian(Raspberry Pi向けカスタマイズのDebian)で動作させることを前提としたバイナリですが、armhfなUbuntu上でも動作します。AquesTalk2はライブラリとして提供されており、アプリケーションは各自が用意する必要がありましたが、AquesTalk Piは引数に指定した日本語文字列を音声にするバイナリそのものが配布されているため、プログラミングの知識がなくてもすぐに利用することができます[1]⁠。

筆者は、少々古いですがUbuntu Weekly Topicsでも紹介された(そして先週も使った)Cubox-iにUbuntu 14.04をインストールし、利用することにしました[2]⁠。

Cubox-iへのUbuntuのインストール

まずはCubox-iへUbuntuをインストールしなければなりません。ARMマシンはx86のPCとは違い、USBからインストーラーをブートして、と言うわけにはいきません。その機種に合わせたプリインストールイメージを書き込むか、でなければブートローダー、カーネル、モジュールを自分でビルドし、debootstrapを使って手動でrootfsを構築する必要があります[3]⁠。筆者は手元のx86_64なUbuntu 14.04上で以下の作業を行いました。

まずインストールするmicroSDHCカードにパーティションを切ります。カーネルを置く/dev/sdX1、rootfsを置く/dev/sdX2、swapパーティションの/dev/sdX5を作成しました[4]⁠。次にビルドに必要なパッケージをインストールし、環境変数ARCHに「arm⁠⁠、CROSS_COMPILEに「/usr/bin/arm-linux-gnueabi-」を設定します。

図1 microSDHCカードにパーティションを設定する
図1 microSDHCカードにパーティションを設定する

最初はブートローダーを用意します。u-bootのソースをクローンし、makeしてください。

$ sudo apt-get install gcc-arm-linux-gnueabi u-boot-tools lzop build-essential
$ export ARCH=arm
$ export CROSS_COMPILE=/usr/bin/arm-linux-gnueabi-
$ git clone https://github.com/SolidRun/u-boot-imx6.git
$ cd u-boot-imx6
$ make mx6_cubox-i_config
$ make

ビルドが成功すると「SPL」⁠u-boot.img」という2つのファイルが出来上がります。これをddコマンドでmicroSDHCへ書き込みます。

$ sudo dd if=SPL of=/dev/sdX bs=1K seek=1
$ sudo dd if=u-boot.img of=/dev/sdX bs=1K seek=42

u-bootの次はカーネルです。SolidRunのリポジトリから、カーネル3.14のツリーをクローンしてビルドします。Wikiの手順どおりに色々とビルドしていますが、⁠./arch/arm/boot/zImage」⁠./arch/arm/boot/dts/imx6q-cubox-i.dtb」とmodulesディレクトリ以下にインストールしたモジュール一式が必要な成果物です。

$ git clone https://github.com/SolidRun/linux-imx6-3.14
$ cd linux-imx6-3.14
$ make imx_v7_cbi_hb_defconfig
$ make zImage imx6q-cubox-i.dtb imx6dl-cubox-i.dtb imx6dl-hummingboard.dtb imx6q-hummingboard.dtb
$ make modules
$ mkdir ../modules
$ make modules_install INSTALL_MOD_PATH=../modules

debootstrapでUbuntu 14.04のrootfsを作成します。この他に必要なパッケージがあれば、この段階でincludeに入れてしまうと楽です。

$ sudo apt-get install debootstrap
$ sudo debootstrap --verbose --foreign --arch=armhf --variant=minbase --include=module-init-tools,locales,udev,dialog,ifupdown,procps,iproute,iputils-ping,nano,wget,netbase,net-tools trusty cubox-i http://jp.archive.ubuntu.com/ports

x86マシンのchroot内でarmhfのバイナリを動かすため、qemu-arm-staticをchroot内のツリーに配置してください。作成したrootfsの中にchrootしたら、debootstrapのsecond-stageを実行してパッケージを展開します。その後はユーザーの作成、パスワードの設定、APTラインの設定、ネットワークの設定など、必要な設定を行っておきましょう。chroot環境からexitしたら、qemu-arm-staticは削除して構いません。

$ sudo apt-get install qemu-user-static
$ sudo cp /usr/bin/qemu-arm-static rootfs/usr/bin/
$ sudo chroot rootfs
# ./debootstrap/debootstrap --second-stage
(各種設定)
# exit
$ sudo rm rootfs/usr/bin/qemu-arm-static

上記のdebootstrapで作成したrootfsは、/dev/sdX2にコピーします。カーネルのビルドで作成した「zImage」「imx6q-cubox-i.dtb」は/dev/sdX1に、modules/lib/{firmware,modules}はrootfsの/lib以下にコピーしてください。このmicroSDHCをCubox-iに挿入すれば、Ubuntu 14.04が起動します。

インストールは以上で終了です。ここから先の作業は、すべてCubox-i(Raspberry Piを使う方はもちろんRaspberry Pi上)で行います。

USBスピーカーを接続する

Cubox-iには音声出力がS/PDIFしかないため、USBスピーカーを利用します。alsa-utilsパッケージをインストールしたうえで、音声の再生を行うユーザーをaudioグループに所属させてください。音声のボリュームはalsamixerコマンドで調整します。ミュートになっている場合がありますので、音が鳴らなかった時には確認してみましょう。

$ sudo apt-get install alsa-utils
$ sudo gpassw -a mizuno audio

また筆者のケースでは、⁠~/.asoundrc」ファイルを作成してdmixerの設定を行わないと、音が鳴りませんでした。そこで「aplay -l」コマンドを実行して、システムに接続されているデバイスの一覧からUSBスピーカーのカード番号を調べます。

$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: imxspdif [imx-spdif], device 0: S/PDIF PCM snd-soc-dummy-dai-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: imxhdmisoc [imx-hdmi-soc], device 0: i.MX HDMI Audio Tx hdmi-hifi-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 2: Speaker [USB Speaker], device 0: USB Audio [USB Audio]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

そして以下の内容で、~/.asoundrcを作成しました。

~/.asoundrcの内容
pcm.!default {
    type plug
    slave.pcm "dmixer"
}

pcm.dmixer {
        type dmix
        ipc_key 1024
        slave {
                pcm "hw:2,0"     先ほど調べたハードウェアの番号
                period_time 0
                period_size 1024
                buffer_size 4096
                rate 44100
        }
        bindings {
                0 0
                1 1
        }
}

AquesTalk Piを使う

AquesTalk Piのページの使用許諾を読み、アーカイブをダウンロードしてください。ダウンロードしたaquestalkpi-20130827.tar.gzには「AquesTalkPi」というファイルと「aq_dic」というフォルダーが含まれています。⁠AquesTalkPi」が実行バイナリで、⁠aq_dic」が辞書ファイルです。⁠AquesTalkPi」「aq_dic」は必ず同じフォルダー内に置かれている必要があるため、バイナリのみを/usr/local/binにインストールするといったことはできません。ホームフォルダー内にアーカイブを展開した状態で、そのまま使うのが良いでしょう。

AquesTalkPiコマンドに、引数として発声させたい文字列を指定します。標準出力にwavデータが出力されますので、ファイルにリダイレクトするか、パイプを通してaplayコマンドで再生させてみましょう。⁠-f」オプションを使うと、文字列をファイルから読み込ませることができます[5]⁠。ファイルに標準入力を指定すれば、パイプを通して文字列を与えることが可能ですので、シェルスクリプト内で他のコマンドの出力をリアルタイムに読み上げさせることができます。これが今回のキモになります。なお「-b」は棒読みオプションで、これを指定すると発声時のアクセントが平坦になります。お好みで使用してください。

$ ./AquesTalkPi -b ゆっくりしていってね > yukkuri.wav              wavファイルを作成
$ ./AquesTalkPi -b ゆっくりしていってね | aplay -q                 その場で音声を再生
$ echo ゆっくりしていってね | ./AquesTalkPi -b -f - | aplay -q     他のコマンドの出力をその場で読み上げ

日付と時刻をしゃべらせる

試しに使ってみるとわかるのですが、ソフトウェアが現実世界になんらかのアクションを起こすのは、思った以上に楽しいものです。これを使って何かできないかと考えた結果、入門編としてcronと組み合わせて目覚まし時計を作ってみることにしました。

まず発声とデバッグを簡単にするため、AquesTalk Piのラッパーとして「yukkuri.sh」というシェルスクリプトを用意して、関数を定義しました。⁠datetime.sh」というスクリプトでは、dateコマンドを用いて現在の日時と曜日を取得しています。また何か行事を執り行ううえでは、大安や仏滅といった暦も大切ですよね。しかし六曜を知るには旧暦の日付を計算する必要があり、そのためには前年の冬至から翌年の雨水までの二十四節気と、朔日の日付を求める必要があるらしく、スクリプト内で真面目に計算するのは大変だと判断しました。都合の良いことに旧暦の日付を求められるWeb APIが公開されていますので、今回はこちらを利用させてもらいました。結果はJSONで受け取り、jqコマンドを利用してパースしています。このように、コマンドの実行結果やAPIの結果をパースし、文字列として結合して発声させるのが基本的な仕組みです。

yukkuri.sh
#!/bin/bash

DEBUG=${DEBUG:-0}
ATP=/home/mizuno/aquestalkpi/AquesTalkPi

function yukkuri ()
{
    if [ ${DEBUG} -eq 1 ];then
        echo "$1"
    else
        echo "$1" | ${ATP} -s 90 -b -f - | aplay -q
        sleep 0.3
    fi
}
datetime.sh
#!/bin/bash

. /home/mizuno/bin/yukkuri.sh

# 発生のため曜日をリストで持っておく
# date +%Aはロケールに依存するので使わない
declare -a DOW_LIST=("にちようび" "げつようび" "かようび" "すいようび" "もくようび" "きんようび" "どようび")

# APIは六曜を数字で返すので、リストを持っておく
declare -a ROKUYOU_LIST=("たいあん" "しゃっこう" "せんしょう" "ともびき" "せんぷ" "ぶつめつ")

# jqコマンドの存在確認
JQ=${JQ:-$(which jq)}
if [ ! -x "${JQ}" ]; then
    echo "jq not exist." 1>&2
    echo "Please install jq package." 1>&2
exit 1
fi

# ただしく発音させるために先頭のゼロ詰めを消す
YEAR=$(date +%Y)
MONTH=$(date +%m | sed -e 's/^0//')
DAY=$(date +%-e)
DOW=${DOW_LIST[$(date +%w)]}
HOUR=$(date +%H | sed -e 's/^0//')
MINUTE=$(date +%M | sed -e 's/^0//')

# 六曜を取得
ROKUYOU=${ROKUYOU_LIST[$(curl -s "http://api.sekido.info/qreki?output=json&year=${YEAR}&month=${MONTH}&day=${DAY}" | ${JQ} '.rokuyou')]}

# あいさつする
if [ ${HOUR} -lt 4 -o ${HOUR} -gt 17 ]; then
    yukkuri "こんばんは。"
elif [ ${HOUR} -lt 11 ]; then
    yukkuri "おはようございます。"
else
    yukkuri "こんにちは。"
fi

# 現在時刻をしゃべる
yukkuri "ゆっくりが、${YEAR}年${MONTH}月${DAY}日、${DOW}、${ROKUYOU}、${HOUR}時${MINUTE}分くらいをお知らせします。"

# 引数があった場合、追加でそれをしゃべる
if [ -n "$1" ]; then
    yukkuri "$1"
fi

今日の天気を調べる

皆さん、毎朝家を出る前には天気予報を調べるのではないでしょうか? 今日の天気や気温をあらかじめ知っておけば、会社から出る際に土砂降りの雨を前にして途方に暮れたり、吹雪の中で凍えそうになったりといった不幸を回避できるかもしれません。そんな今日の天気や気温を、目覚まし時計が毎朝教えてくれたら便利だと思いませんか? というわけで喋らせましょう。

天気は、livedoorが提供しているお天気情報のAPIから取得しています。cityパラメータに知りたい地域のコードを与えることで、その地域の天気、最高気温、最低気温などをJSONで受け取ることができます。このAPIのレスポンスのうち、非ASCII文字はUnicodeエスケープシーケンスで表されているため、これをUnicode数値文字参照に置換したうえで、nkfコマンドの--numchar-inputでUTF-8に変換しています。あとは前述の六曜同様、jqコマンドで必要な項目を取り出すだけです。この時「雪」「せつ」と読んでしまうようなケースがあるため、ひらがなに置換しています。また天気が「くもり」の場合は「曇り」と表記されるのですが、⁠くもりときどきゆき」の場合は「曇時々雪」と表記されるなど、⁠り」を送る場合と送らない場合があるため、これもアドホックに吸収しています。数値はそのままでも正しく読み上げてくれるのですが「-⁠マイナス⁠⁠」を発声できないため、ここも置換します。これは、最高気温が氷点下にならないような地域では不要な処理かもしれません。ちなみに筆者は毎朝、タイマーでリビングのヒーターをONにしているのですが、部屋が充分に暖まるまでは布団から出たくありません。そこでついでに、先週のレシピでも使ったUSB温度計を利用し、室温もしゃべらせています。これで布団の中にいながら、外の天気と今日の最高気温、リビングの室温がわかるようになりました[6]⁠。

tenki.sh
#!/bin/bash

. /home/mizuno/bin/yukkuri.sh

LOCATION=016010
CITY="さっぽろ"
TEMPER=/usr/local/bin/temper

# jqコマンドの存在確認
JQ=${JQ:-$(which jq)}
if [ ! -x "${JQ}" ]; then
    echo "jq not exist." 1>&2
    echo "Please install jq package." 1>&2
exit 1
fi

# UnicodeエスケープシーケンスをUnicode数値文字参照に置換した後nkfでUTF-8にする
TENKI=$(curl -s "http://weather.livedoor.com/forecast/webservice/json/v1?city=${LOCATION}" | sed -e 's/\\u\(....\)/\\1;/g' | nkf --numchar-input -w)

# 天気を取り出す
TELOP=$(echo ${TENKI} | ${JQ} '.forecasts[0].telop')
# 晴(はれ) 雪(ゆき)などを正しく訓読みで発音できないので置換する
TELOP=$(echo ${TELOP} | sed -e 's/雪/ゆき/g' -e 's/曇り*/くもり/g' -e 's/晴れ*/はれ/g' -e 's/雨/あめ/g' -e 's/時々/ときどき/g')

# 最高気温を取り出す
TEMP=$(echo ${TENKI} | ${JQ} '.forecasts[0].temperature.max.celsius')
# マイナスを発音できるように置換する
# 最高気温がマイナスにならない地域では不要かも
TEMP=$(echo ${TEMP} | sed -e 's/\-/マイナス/')

# 天気をしゃべる
if [ -n "${TELOP}" ]; then
    yukkuri "今日の${CITY}の天気は、${TELOP}です。"
fi

# 気温をしゃべる
# 夜間など、APIを叩く時間帯によっては最高気温がnullで返ってくるので回避対応
if [ "${TEMP}" != "null" ]; then
    yukkuri "最高気温は、${TEMP}度です。"
fi

# temperコマンドで室温が取れる場合は、室温もしゃべる
if [ -x "${TEMPER}" ]; then
    ROOMTEMP=$(${TEMPER} | cut -d',' -f2 | sed -e 's/\.[0-9]*//')
    yukkuri "現在の室内の温度は、${ROOMTEMP}度です。"
fi

ゴミ出しを忘れないために

朝、忘れてはならないことと言えば……そう、ゴミ出しです。たとえば燃やせないゴミの日は月に一度しかないため、もしも忘れてしまったら、また1ヵ月、使用済みのスプレー缶と同居しなければなりません。そんな悲劇を未然に防ぐため、ゴミの収集スケジュールも喋らせましょう。

最近は自治体のゴミカレンダーがデータとして提供されているケースも増えてきましたので、これを探して取り込むのはそんなに難しいことではありません。幸い筆者の住む札幌市の場合、iCal形式のデータが提供されていました。ただシェルスクリプトから利用する場合、iCalのままでは都合が悪いので、簡単なシェルスクリプトを使って「日付,収集内容」というCSVに変換しました。このCSVを今日の日付でgrepして、もしも該当する行があればcutで収集内容を取り出せば良いわけです。これで布団の中にいながら、出勤時に持ち出さなければいけないゴミの内容もわかるようになってしまいました!

iCal形式をCSVに変換
while read line
do
    case $line in
        BEGIN:VEVENT*)
            DATE=""
            SUMMARY=""
            ;;
        END:VEVENT*)
            echo "$DATE,$SUMMARY"
            ;;
        DTSTART*)
            DATE=$(echo $line | sed -e 's/DTSTART;VALUE=DATE:\([0-9]*\)/\1/')
            ;;
        SUMMARY:*)
            SUMMARY=$(echo $line | sed -e 's/SUMMARY:\(.*\)/\1/')
            ;;
    esac
done < (icalファイル)
gomi.sh
#!/bin/bash

. /home/mizuno/bin/yukkuri.sh

# 「YYYYMMDD,収集内容」となっているCSV形式のゴミカレンダー
GOMICAL=/home/mizuno/gomical/gomical.csv

# ゴミカレンダーから今日の収集内容を取得
if [ -r "${GOMICAL}" ]; then
    GOMI=$(grep $(date +%Y%m%d) ${GOMICAL} | cut -d',' -f2)
fi

# ゴミの収集予定をしゃべる
if [ -n "${GOMI}" ]; then
    yukkuri "今日は、${GOMI}、のゴミ収集があります。ゴミ出しを忘れないようにしましょう。"
else
    yukkuri "今日のゴミ収集はありません。"
fi

タスクリマインダーとして利用する

本連載357回で、柴田さんが「目標達成のために必要なのは「目標を忘れないこと」「⁠⁠目標を忘れないこと』を覚えておくこと⁠⁠」だと言っていました。ならば毎朝、残タスクの内容を読み上げさせれば、目標を忘れないでいられるのではないでしょうか。というわけでタスクも喋らせます。

Taskwarriorがインストールされている場合、taskコマンドを実行してタスクの一覧を得ることにします。ただし標準の状態では、タスクIDやヘッダ行などが出力されてしまうため、読み上げの邪魔になります。taskコマンドが出力する項目は~/.taskrcファイルに設定を書くことでカスタマイズできるのですが、descriptionだけを出力するという設定は音声出力のためだけにしか使わない設定ですので、恒久的な設定にしてしまうと、普段対話的にtaskコマンドを使う際に邪魔になってしまいます。taskコマンドは「rc.hoge」という引数で、設定ファイルの内容をコマンドラインから一時的に上書きできるため、これを利用しましょう。具体的には「verbose=nothing」でヘッダ行などの出力を抑制、⁠color=off」で色つけを抑制、⁠columns=description」でdescriptionのみを出力し、⁠filter=status:pending」でペンディング状態のタスクのみをフィルタしています。残タスクを毎朝読み上げてくれるので、これでタスクを忘れることもなくなりましたね![7]

task.sh
#!/bin/bash

. /home/mizuno/bin/yukkuri.sh

TASK=${TASK:-$(which task)}

# 発声のためタスクのdescriptionだけを抽出するオプション
TASK_OPTIONS="rc.verbose=nothing rc.color=off rc.report.yukkuri.columns=description rc.report.yukkuri.filter=status:pending yukkuri"

# TaskWarriorがインストールされている場合、現在Pending中のタスクをしゃべる
if [ -x "${TASK}" ]; then
    yukkuri "現在のタスクは"

    # 複数行は発声できないため、読み込んだタスクを一行にまとめる
    TASKS=$(${TASK} ${TASK_OPTIONS} | tr '\n' ' ')
    if [ -n "${TASKS}" ]; then
        yukkuri "${TASKS} です。"
    else
        yukkuri "ありません。"
    fi
fi

cronで定時実行

ここまでに作成したシェルスクリプトを連続して呼び出すスクリプトを作成し、cronに登録しましょう。cronは曜日を指定して設定が可能なので、平日と土日で実行時間をずらしています。これで様々な情報を読み上げてくれる、便利な目覚まし時計が完成しました。

$ cat /etc/cron.d/schedule 
45 8 * * 1,2,3,4,5 mizuno /home/mizuno/bin/alerm.sh
30 9 * * 6,7       mizuno /home/mizuno/bin/alerm.sh

さらなる活用

基本は「コマンドやAPIを叩く⁠⁠→⁠レスポンスを整形する⁠⁠→⁠発声させる」だけですので、シェルスクリプトで文字列の整形やパースさえできれば、さらに色々な応用が可能です。たとえばTinyTinyRSSにニュースのRSSフィードを登録しておき、APIを経由して今日のニュースヘッドラインを読み上げさせてみるのはどうでしょう? Googleカレンダーから予定を取得したり、未読メールのサブジェクトや、TwitterのMentionなどを読み上げさせてもおもしろそうです。若い女性は占いが大好きだと聞いたので、筆者はWeb ad Fortune APIを利用して、今日の運勢とラッキーアイテム、ラッキーカラーを喋らせてみました。ひょっとしたら、お昼休みの話題として役立つかもしれません[8]⁠。

筆者は実際にこのシステムをここ2週間ほど自宅で運用していますが、なかなか便利です。また上記で作成した各スクリプトは個別に実行することが可能なので、cronに以下のようなスケジュールを登録し、時報にしていたりもします[9]⁠。

0 10-19 * * *      mizuno /home/mizuno/bin/datetime.sh

皆さんも、自分だけの秘書をUbuntuで作ってみてはいかがでしょうか。

実際の動作状況

おすすめ記事

記事・ニュース一覧