Python 3 から Google Home に喋らせる(低遅延)


はじめに

Google Home にプッシュで喋らせる場合、google-home-notifier がよく使われると思います。
しかし、なんか大げさな気がする・・・

軽い方法として、Python で pychromecast を使ってしゃべらせる方法があります。

しかし使ってみたら、スクリプトを起動してから喋るまで10秒近くかかっちゃいます。

そこで、できるだけ早く喋り始めさせる方法を模索しました。

pychromecast のキホン

pychromecast は、音声データのある適当なURLをぶち込むと、それを再生してくれます。(それ以外にもいろいろできますが、ここでの使い方はそれを想定しています)

音声データはどこかのWebサーバに置いておく必要があります。任意の言葉をしゃべらせる場合は、テキスト→音声変換が別途必要になりますね。

喋らせる内容が固定であれば、固定の音声ファイルをWebサーバに置いておけばよいです。

ここでは、 http://192.168.0.100/test.mp3 に音声ファイルがある前提で進めます。(Google Home から見える場所であれば、ローカルネットワーク内でもインターネット上でもどちらでもいいです。)

基本的な使い方は以下のようになります。

ghspeech1.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pychromecast

mp3url = 'http://192.168.0.100/test.mp3';

#Chromecastデバイス(Google Homeも)を探す
chromecasts = pychromecast.get_chromecasts()

if len(chromecasts) == 0:
    print("Google Homeが見つかりませんω")
    exit()

#固定で1個目を使う
googleHome = chromecasts[0]

if not googleHome.is_idle:
    print("Killing current running app")
    googleHome.quit_app()
    time.sleep(5)

#喋らせる
googleHome.wait()
googleHome.media_controller.play_media(mp3url, 'audio/mp3')
googleHome.media_controller.block_until_active()

Google Home が複数台ある場合はどうなるでしょうか?

たまたま最初に見つかったGoogle Homeがしゃべります。
仮にGoogle Homeが1台だけでも、Chromecastが存在する場合はそっちが選ばれてしまう可能性があります。

ほとんどのご家庭では実用になりませんね!!!

で、次。名前でGoogle Homeを特定する方法。

ghspeech2.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pychromecast

mp3url = 'http://192.168.0.100/test.mp3';

#Chromecastデバイス(Google Homeも)を探す
chromecasts = pychromecast.get_chromecasts()

if len(chromecasts) == 0:
    print("Google Homeが見つかりませんω")
    exit()

#名前で探す
googleHome = next(cc for cc in chromecasts if cc.device.friendly_name == 'リビング')

if not googleHome.is_idle:
    print("Killing current running app")
    googleHome.quit_app()
    time.sleep(5)

#喋らせる
googleHome.wait()
googleHome.media_controller.play_media(mp3url, 'audio/mp3')
googleHome.media_controller.block_until_active()

違いは

#固定で1個目を使う
googleHome = chromecasts[0]

#名前で探す
googleHome = next(cc for cc in chromecasts if cc.device.friendly_name == 'リビング')

になっただけです。
簡単ですね。

ちなみに、モデル名を使って

#モデル名で探す
googleHome = next(cc for cc in chromecasts if cc.device.model_name == 'Google Home Mini')

のように探すこともできます。

ただこれらの方法、しゃべり始めるまでに10秒ぐらいかかります。

どこで時間がかかっているかというと、pychromecast.get_chromecasts() のところです。つまり、デバイス一覧を作るところです。pychromecast のソースを見てみたところ、一覧作成時に5秒の待ちがあります。

一覧の作成は飛ばしたい。

IPアドレス指定でやる

IPアドレス指定でイケる方法が絶対あるはずだ。そう思って調べた結果が以下のソースになります。

ghspeech4.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pychromecast

mp3url = 'http://192.168.0.100/test.mp3';

#IPアドレスで特定する
googleHome = pychromecast.Chromecast('192.168.0.22')

if not googleHome.is_idle:
    print("Killing current running app")
    googleHome.quit_app()
    time.sleep(5)

#喋らせる
googleHome.wait()
googleHome.media_controller.play_media(mp3url, 'audio/mp3')
googleHome.media_controller.block_until_active()

これだと割とすぐ喋ってくれます。

ちなみに、ubuntu 18.04 で apt install python3-pychromecast でインスコした pychromecast ではエラーが出てしまいます。pip で最新をインスコしましょう。

これで低遅延で喋らせることができましたが、IPアドレスを直接指定するのは嫌だ! やっぱり名前で指定したい!!

一覧の作成はせず、名前で指定する

名前指定で探して、見つかったら可及的速やかにそいつに喋らせる。不可能ではないはずだ。そう思って5時間ぐらいかけて作ったコードが以下です。

ghspeech5.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pychromecast
import time
from threading import Event
from zeroconf import ServiceBrowser, Zeroconf

googleHomeName = 'リビング'
mp3url = 'http://192.168.0.100/test.mp3'

class MyListener(object):
    def add_service(self, zeroconf, type, name):
        global ip
        info = zeroconf.get_service_info(type, name)
        if info.properties[b'fn'].decode() == googleHomeName :
            #見つかった機器に割り振られているIPアドレスの最初のIPを採用(IPv6の場合もあるかも)
            ip = info.parsed_addresses()[0]
            #見つかったことを通知
            discover_complete.set() 

discover_complete = Event() #待ち合わせ用
zeroconf = Zeroconf()
listener = MyListener()

# GoogleHomeを探す(非同期実行)
browser = ServiceBrowser(zeroconf, "_googlecast._tcp.local.", listener)

# 見つかるかタイムアウトするまで待ち
discover_complete.wait(5)

googleHome = pychromecast.Chromecast(ip)

if not googleHome.is_idle:
    print("Killing current running app")
    googleHome.quit_app()
    time.sleep(5)

#喋らせる
googleHome.wait()
googleHome.media_controller.play_media(mp3url, 'audio/mp3')
googleHome.media_controller.block_until_active()

zeroconfを使って探索を開始し、指定された名前のが見つかったらすぐにそいつに喋らせます。

探索は非同期実行なので、Event の set() wait() を使っているところがミソです。

まとめ

  • pychromecast を普通に使うとデバイス一覧作成で時間がかかる
  • IPアドレス直指定だと低遅延で喋らせることができる
  • さらに、zeroconfと組み合わせて、名前指定でも低遅延で喋らせることができる!