Google Assistant SDK for Python(工事中)

AIシステム開発者向けページが提供するGoogle Assistant SDK for Pythonをご紹介します。

Google Assistant SDK for Pythonの概要

Google Assistant SDK for Pythonとは?

Google Assistant SDKはRaspberry Pi 3B用の「Google Assistant library for Python」とその他のプラットフォームで使用できるローレベルのAPIである「Google Assistant gRPC API」の2つで構成されています。

Google Assistant SDK for Pythonを使用すると、「OK Google」の代わりにボタンを押してGoogle Assistantを起動させたり、Google Assistantが音声を再生している間LEDランプを点滅させたり、ユーザーリクエストの音声をディスプレイにテキストとして表示させたりすることができるようです。

また、IFTTTなどと連携して、音声コマンドでいろいろ操作させることもできます。

IFTTTの枠組を下図に示す.


IFTTT

Google Assistant On RaspberryPi 3の要点

  • 現時点では日本語も対応である。
  • Google Cloud Platform(GCP)からAPI・サービスの設定を行う。
  • Googleのアカウント・諸々の許可が必要である(プライバシー大丈夫かな)
  • Google Assistant SDKが必要である。
  • Google Assistant for Pythonのレスポンスはgoogle.assistant.library.eventで返ってくる。

Google Assistant gRPC APIの要点

Google(Dialogflow)でGoogle Homeから電車の遅延情報を取得する

作成するアプリの説明

今回は「指定した路線の遅延有無を教えてくれるアプリ」を題材に説明します。以下のような会話が行えることを目標としましょう。

ユーザ:「OK Google, XX線の遅延情報を教えて」
GoogleHome:
(遅延が発生している場合)「XX線で遅延発生中です。」
(遅延が発生していない場合)「現在XX線では遅延は発生していません。」

このアプリの流れを図にすると以下のようになります。


処理の流れ

図内に登場する単語を簡単に説明します。

  • Action on Google:
    Google アシスタント対応のアプリ開発を行えるプラットフォームです。
    Action on Googleで作成するアプリの単位をプロジェクトと呼びます。
  • Dialogflow:
    自然言語対話のプラットフォームです。Dialogflowを使うことで、GUIで簡単に会話形アプリを作成することができます。
    Dialogflowで作成するアプリの単位をエージェントと呼びます。
  • 遅延情報取得プログラム:
    今回作成するスクリプトです。遅延情報の取得処理はこのスクリプトで行います。
    Dialogflow 上でFulfillmentとして設定することで Dialogflow と連携することができます。

以下から詳細な手順を説明します。

環境

  • Google Home
  • Raspberry Pi3 (Raspbian Lite 9.1)

Actions on Googleプロジェクトの作成

GoogleHomeと連携しているGoogleアカウントでAction on Googleのコンソールにアクセスし、Add/import project から新しいプロジェクト TrainDelayInfo を作成しましょう。

作成したプロジェクトをクリックし、ADD ACTIONSから Dialogflow のBUILDを選択します。

Dialogflow Agentの作成

今回 Dialogflow で設定することは大きく以下の3点です。 順番に設定していきましょう。

  • Entitiesの作成
  • Intentsの作成
  • Fulfillmentの実装

Dialogflowの設定項目

Dialogflowの画面左側に色々項目があります。

ここで必要な項目について簡単に説明しておきます。

Intents

ここで「○○って言ったら××をする」ってのを定義します。

Entities

ここは言葉の呼び方を定義します。

例えば「お腹空いた」という言葉は他にも色んな言い方があります。

「腹減った」とか「お腹ペコペコ」とか。

そういった色んな言い方をしても意味は「お腹空いた」だよっていうのを定義します。

様々な言い方を一つの言葉に集約させるんです。

Integrations

Dialogflowに設定したAgentを他のサービス(今回はGoogle Assistant)へ使えるよう反映します。

Agentを作ったり、更新したときに操作します。

Fulfillment

Webhookや「Cloud Functions for Firebase」のInline Editorで返答結果をがちゃがちゃするのに使います。

では以上を踏まえ、Agentを設定してみましょう。

Entityの作成

まずはEntityを2つ作ります。

「○○を××して」の「○○」に相当する「target」と、「××」に相当する「action」って名前のEntityです。

「target」は「テレビ」とか「電気」とかです。

「action」は「起動」とか「スタンバイ」とかになります。

左側のカラムに集約させたい言葉、右側のカラムに色んな言い方を思いつく限り入力していきます。

しかし、今回は、簡単のため「train-route」と言う名前のEntityを1つだけ作ります。

train-route
  • 山手線 山手線、やまのてせん
  • 中央線 中央線、ちゅうおうせん
  • 総武線 総武線、そうぶせん
  • 浅草線 浅草線、あさくさせん
  • 小田急線 小田急線、おだきゅうせん

Fulfillmentの設定

Webhookを「ENABLED」へ切り替え、URLに「https://dummy/」みたいにダミーのURLを入力してSAVEして下さい。

こうしないとIntentの設定時にFulfillmentの項目が設定できないからです。

ここのURLは後ほどFirebaseでWEB APIを作成し、それに置き換えます。

image.png

Intentの作成

今回は、簡単のため「Default Welcome Intent」と言う名前のIntentを使用します。

Intentで設定すべき項目は5箇所です。

「1.Intent名」「2.User says」「3.Action」「4.Response」「5.Fulfillment」です。

1.Intent名

「Intent名」は適当になんか入れて下さい。

2.User says

「User says」は「○○って言ったら××をする」の「○○」の部分の定義です。

ここでは先ほど作ったEntityを使用します。

ここでのEntityの指定方法がちょっと一癖あります。

まず先ほど作成した「target」と入力します

次に入力した「train-root」の文字を全て選択してハイライトます(文字をダブルクリックしたりとか)

するとポップアップが出てきて@から始まる変数っぽいのの一覧が出ます

ここから「@train-root」を選択します

すると選択していた「target」の文字が黄色くハイライトされます

以上の方法で、もし他の「Entity」(「action」など)がある場合も指定してしてみて下さい。

Entityでは記述しきれなかった接続詞を含めたパターンをいくつか定義してみます。

とにかく、このIntentとして認識される「例文!!!」を入力します。

追加で「路線名は遅延している?」という呼びかけでも認識するようにしました。

黄色背景部分(路線名)はパラメータとなる部分です。

  • 中央線の遅延情報を教えて
  • 山手線は遅延してる?
3.Action

上のテキストボックスにはアクション名を指定します。後述のFulfillmentの実装で利用します。2つ目のテーブルではパラメータを指定します。

  • REQUIRED:
    必須有無を選択できます。必須の場合、このパラメータが入力されないと次の Intent には遷移しません。
  • PARAMETER NAME:
    パラメータ名です。Fulfillmentにてパラメータを識別するために利用します。
  • ENTITY:
    このパラメータのEntityです。今回は先ほど作成したEntityの@train-routeを指定しています。
  • IS LIST:
    複数の値を受け取る場合に指定します。
  • PROMPTS:
    パラメータの入力を促す文章を定義します。
    REQUIREDにチェックしたパラメータについて、ユーザからの入力が無かった場合やEntityに存在しない単語を入力された場合に使われます。
4.Response

このIntentでのユーザへの返答を定義します。今回はFulfillmentを使って返答するため、デフォルトで設定されているものは削除します。

5.Fullfillment

「Fullfillment」は「Use webhook」にチェックつけるだけです。

一通り設定したらSAVEします。

image.png

Firebase Funcitonsの作成

FulfillmentのWebhookに設定したURLへのアクセスはPOSTになります。

そうすると今までIFTTTでやっていたようにPUTでDatabaseを更新することができません。

じゃあどうするかというと「Firebase Functions」を使い、POSTされたデータをDatabaseへ書き込むWEB APIを作成します。

※DialogflowのInline Editorではありません!

今までの記事ではFirebaseのRealtime Databaseの機能のみを使用していました。

今回はプラスでFunctionsの機能も使用します。

Functionsを作成するためには「firebase-tools」というNode.jsモジュールの管理ツールを使用します。

Firebaseアプリの例

Firebaseアプリの例を以下に示します。

「app.tell()」関数で、音声応答を返しています。

'use strict';

process.env.DEBUG = 'actions-on-google:*';
const App = require('actions-on-google').DialogflowApp;
const functions = require('firebase-functions');

// Intentのアクション名を指定
const NAME_ACTION = 'confirm_delay';

// パラメータのPARAMETER NAMEを指定
const ROUTE_ARGUMENT = 'train-route';

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
    const app = new App({request, response});

    function confirmDelay (app) {
        // 路線名を取得する
        let route = app.getArgument(ROUTE_ARGUMENT);

        let isDelay = false;

        /* 指定した路線の電車遅延情報を取得する処理をここに書く */

        if (isDelay) {
            app.tell(route + 'で遅延発生中です。');
        } else {            
            app.tell('現在' + route + 'では遅延は発生していません。');
        }
    }
    let actionMap = new Map();
    actionMap.set(NAME_ACTION, confirmDelay);

    app.handleRequest(actionMap);
});

Actions on Googleの設定

Actions on Googleへ戻るとアプリケーションの情報入力を求められます。

Assistantアプリを呼び出す時の名称とか決めたかったら入力しましょう。

無視して次へ進めることもできます。

その場合アプリ名称は一時的に「テスト用アプリ」となります。

そして最後に「TEST DRAFT」をクリックしてみましょう。

Simulator画面へ進みます。

※ちなみに隣にある「SUBMIT DRAFT FOR REVIEW」を選択するとGoogleの審査の後作ったAssistantアプリをリリースすることができます

Actions on Google_20171103_004756.png

ここまで来ればGoogle Homeの実機や、スマホのGoogle Assistantから作成したActions on Googleアプリが使えます。

もちろんSimulatorからテストもできるので、思い思いの方法でテストしてみましょう。

ちなみにアプリを終了するときは「停止」と言うと終了されます。

また、終了用のIntentの作成もできます。

Google Assistant SDK for Pythonの例用例

Google Assistant SDK for Pythonの例用例を説明します。

発話APIの使用例(google_assistant.py)

pushtotalk.pyは、ボタンを押すことで会話を開始します。

#!/usr/bin/env python
 
# Copyright (C) 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
 
from __future__ import print_function
 
import argparse
import os.path
import json
import google.oauth2.credentials
 
from google.assistant.library import Assistant
from google.assistant.library.event import EventType
from google.assistant.library.file_helpers import existing_file
 
def process_event(event):
    """Pretty prints events.
    Prints all events that occur with two spaces between each new
    conversation and a single space between turns of a conversation.
    Args:
        event(event.Event): The current event to process.
    """
    if event.type == EventType.ON_CONVERSATION_TURN_STARTED:
        print()
 
    print(event)
 
    if (event.type == EventType.ON_CONVERSATION_TURN_FINISHED and
            event.args and not event.args['with_follow_on_turn']):
        print()
 
def main():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('--credentials', type=existing_file,
                        metavar='OAUTH2_CREDENTIALS_FILE',
                        default=os.path.join(
                            os.path.expanduser('~/.config'),
                            'google-oauthlib-tool',
                            'credentials.json'
                        ),
                        help='Path to store and read OAuth2 credentials')
    args = parser.parse_args()
    with open(args.credentials, 'r') as f:
        credentials = google.oauth2.credentials.Credentials(token=None,
                                                            **json.load(f))
 
    with Assistant(credentials) as assistant:
        for event in assistant.start():
            process_event(event)
 
if __name__ == '__main__':
    main()

Google Assistant gRPC APIの使用例

発話APIの使用例(pushtotalk.py)

pushtotalk.pyは、ボタンを押すことで会話を開始します。

# Copyright (C) 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Sample that implements gRPC client for Google Assistant API."""

import json
import logging
import os.path

import click
import grpc
import google.auth.transport.grpc
import google.auth.transport.requests
import google.oauth2.credentials

from google.assistant.embedded.v1alpha1 import embedded_assistant_pb2
from google.rpc import code_pb2
from tenacity import retry, stop_after_attempt, retry_if_exception

try:
    from . import (
        assistant_helpers,
        audio_helpers
    )
except SystemError:
    import assistant_helpers
    import audio_helpers


ASSISTANT_API_ENDPOINT = 'embeddedassistant.googleapis.com'
END_OF_UTTERANCE = embedded_assistant_pb2.ConverseResponse.END_OF_UTTERANCE
DIALOG_FOLLOW_ON = embedded_assistant_pb2.ConverseResult.DIALOG_FOLLOW_ON
CLOSE_MICROPHONE = embedded_assistant_pb2.ConverseResult.CLOSE_MICROPHONE
DEFAULT_GRPC_DEADLINE = 60 * 3 + 5


class SampleAssistant(object):
    """Sample Assistant that supports follow-on conversations.

    Args:
      conversation_stream(ConversationStream): audio stream
        for recording query and playing back assistant answer.
      channel: authorized gRPC channel for connection to the
        Google Assistant API.
      deadline_sec: gRPC deadline in seconds for Google Assistant API call.
    """

    def __init__(self, conversation_stream, channel, deadline_sec):
        self.conversation_stream = conversation_stream

        # Opaque blob provided in ConverseResponse that,
        # when provided in a follow-up ConverseRequest,
        # gives the Assistant a context marker within the current state
        # of the multi-Converse()-RPC "conversation".
        # This value, along with MicrophoneMode, supports a more natural
        # "conversation" with the Assistant.
        self.conversation_state = None

        # Create Google Assistant API gRPC client.
        self.assistant = embedded_assistant_pb2.EmbeddedAssistantStub(channel)
        self.deadline = deadline_sec

    def __enter__(self):
        return self

    def __exit__(self, etype, e, traceback):
        if e:
            return False
        self.conversation_stream.close()

    def is_grpc_error_unavailable(e):
        is_grpc_error = isinstance(e, grpc.RpcError)
        if is_grpc_error and (e.code() == grpc.StatusCode.UNAVAILABLE):
            logging.error('grpc unavailable error: %s', e)
            return True
        return False

    @retry(reraise=True, stop=stop_after_attempt(3),
           retry=retry_if_exception(is_grpc_error_unavailable))
    def converse(self):
        """Send a voice request to the Assistant and playback the response.

        Returns: True if conversation should continue.
        """
        continue_conversation = False

        self.conversation_stream.start_recording()
        #logging.info('Recording audio request.')
        logging.info('お話の記録を開始しています・・')

        def iter_converse_requests():
            for c in self.gen_converse_requests():
                assistant_helpers.log_converse_request_without_audio(c)
                yield c
            self.conversation_stream.start_playback()

        # This generator yields ConverseResponse proto messages
        # received from the gRPC Google Assistant API.
        for resp in self.assistant.Converse(iter_converse_requests(),
                                            self.deadline):
            assistant_helpers.log_converse_response_without_audio(resp)
            if resp.error.code != code_pb2.OK:
                logging.error('server error: %s', resp.error.message)
                break
            if resp.event_type == END_OF_UTTERANCE:
                #logging.info('End of audio request detected')
                logging.info('お話の終了を検出しました。')
                self.conversation_stream.stop_recording()

            if resp.result.spoken_request_text:
                #logging.info('Transcript of user request: "%s".', resp.result.spoken_request_text)
                logging.info('お話の内容: "%s"です。', resp.result.spoken_request_text)
                #logging.info('Playing assistant response.')
                logging.info('「お話AIちゃん」が返事をします。')

            if len(resp.audio_out.audio_data) > 0:
                self.conversation_stream.write(resp.audio_out.audio_data)

            if resp.result.spoken_response_text:
                logging.info('Transcript of TTS response '
                    '(only populated from IFTTT): "%s".',
                    resp.result.spoken_response_text)

            if resp.result.conversation_state:
                self.conversation_state = resp.result.conversation_state

            if resp.result.volume_percentage != 0:
                self.conversation_stream.volume_percentage = (
                    resp.result.volume_percentage
                )

            if resp.result.microphone_mode == DIALOG_FOLLOW_ON:
                continue_conversation = True
                logging.info('Expecting follow-on query from user.')

            elif resp.result.microphone_mode == CLOSE_MICROPHONE:
                continue_conversation = False

        #logging.info('Finished playing assistant response.')
        logging.info('「お話AIちゃん」の返事が終わりました。')
        self.conversation_stream.stop_playback()
        return continue_conversation

    def gen_converse_requests(self):
        """Yields: ConverseRequest messages to send to the API."""

        converse_state = None
        if self.conversation_state:
            logging.debug('Sending converse_state: %s',
                          self.conversation_state)
            converse_state = embedded_assistant_pb2.ConverseState(
                conversation_state=self.conversation_state,
            )
        config = embedded_assistant_pb2.ConverseConfig(
            audio_in_config=embedded_assistant_pb2.AudioInConfig(
                encoding='LINEAR16',
                sample_rate_hertz=self.conversation_stream.sample_rate,
            ),
            audio_out_config=embedded_assistant_pb2.AudioOutConfig(
                encoding='LINEAR16',
                sample_rate_hertz=self.conversation_stream.sample_rate,
                volume_percentage=self.conversation_stream.volume_percentage,
            ),
            converse_state=converse_state
        )
        # The first ConverseRequest must contain the ConverseConfig
        # and no audio data.
        yield embedded_assistant_pb2.ConverseRequest(config=config)
        for data in self.conversation_stream:
            # Subsequent requests need audio data, but not config.
            yield embedded_assistant_pb2.ConverseRequest(audio_in=data)


@click.command()
@click.option('--api-endpoint', default=ASSISTANT_API_ENDPOINT,
              metavar='<api endpoint>', show_default=True,
              help='Address of Google Assistant API service.')
@click.option('--credentials',
              metavar='<credentials>', show_default=True,
              default=os.path.join(click.get_app_dir('google-oauthlib-tool'),
                                   'credentials.json'),
              help='Path to read OAuth2 credentials.')
@click.option('--verbose', '-v', is_flag=True, default=False,
              help='Verbose logging.')
@click.option('--input-audio-file', '-i',
              metavar='<input file>',
              help='Path to input audio file. '
              'If missing, uses audio capture')
@click.option('--output-audio-file', '-o',
              metavar='<output file>',
              help='Path to output audio file. '
              'If missing, uses audio playback')
@click.option('--audio-sample-rate',
              default=audio_helpers.DEFAULT_AUDIO_SAMPLE_RATE,
              metavar='<audio sample rate>', show_default=True,
              help='Audio sample rate in hertz.')
@click.option('--audio-sample-width',
              default=audio_helpers.DEFAULT_AUDIO_SAMPLE_WIDTH,
              metavar='<audio sample width>', show_default=True,
              help='Audio sample width in bytes.')
@click.option('--audio-iter-size',
              default=audio_helpers.DEFAULT_AUDIO_ITER_SIZE,
              metavar='<audio iter size>show_default=True,
              help='Size of each read during audio stream iteration in bytes.')
@click.option('--audio-block-size',
              default=audio_helpers.DEFAULT_AUDIO_DEVICE_BLOCK_SIZE,
              metavar='<audio block size>', show_default=True,
              help=('Block size in bytes for each audio device '
                    'read and write operation..'))
@click.option('--audio-flush-size',
              default=audio_helpers.DEFAULT_AUDIO_DEVICE_FLUSH_SIZE,
              metavar='<audio flush size>', show_default=True,
              help=('Size of silence data in bytes written '
                    'during flush operation'))
@click.option('--grpc-deadline', default=DEFAULT_GRPC_DEADLINE,
              metavar='<grpc deadline>', show_default=True,
              help='gRPC deadline in seconds')
@click.option('--once', default=False, is_flag=True,
              help='Force termination after a single conversation.')

def main(api_endpoint, credentials, verbose,
         input_audio_file, output_audio_file,
         audio_sample_rate, audio_sample_width,
         audio_iter_size, audio_block_size, audio_flush_size,
         grpc_deadline, once, *args, **kwargs):
    """Samples for the Google Assistant API.

    Examples:
      Run the sample with microphone input and speaker output:

        $ python -m googlesamples.assistant

      Run the sample with file input and speaker output:

        $ python -m googlesamples.assistant -i <input file>

      Run the sample with file input and output:

        $ python -m googlesamples.assistant -i <input file> -o <output file>
    """
    # Setup logging.
    logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO)

    logging.info('main() 1.api_endpoint = ' + str(api_endpoint))
    logging.info('main() 2.credentials = ' + str(credentials))
    logging.info('main() 3.verbose = ' + str(verbose))
    logging.info('main() 4.input_audio_file = ' + str(input_audio_file))
    logging.info('main() 5.output_audio_file = ' + str(output_audio_file))
    logging.info('main() 6.audio_sample_rate = ' + str(audio_sample_rate))
    logging.info('main() 7.audio_sample_width = ' + str(audio_sample_width))
    logging.info('main() 8.audio_iter_size = ' + str(audio_iter_size))
    logging.info('main() 9.audio_block_size = ' + str(audio_block_size))
    logging.info('main() 10.audio_flush_size = ' + str(audio_flush_size))
    logging.info('main() 11.grpc_deadline = ' + str(grpc_deadline))
    logging.info('main() 12.once = ' + str(once))
    logging.info('main() 13.*args = ' + str(*args))
    logging.info('main() 14.**kwargs = ' + str(**kwargs))
    
    #return

    # Load OAuth 2.0 credentials.
    try:
        with open(credentials, 'r') as f:
            credentials = google.oauth2.credentials.Credentials(token=None,
                                                                **json.load(f))
            http_request = google.auth.transport.requests.Request()
            credentials.refresh(http_request)
    except Exception as e:
        logging.error('Error loading credentials: %s', e)
        logging.error('Run google-oauthlib-tool to initialize '
                      'new OAuth 2.0 credentials.')
        return

    # Create an authorized gRPC channel.
    grpc_channel = google.auth.transport.grpc.secure_authorized_channel(
        credentials, http_request, api_endpoint)
    #logging.info('Connecting to %s', api_endpoint)
    logging.info('「%s」に接続しています。', api_endpoint)

    # Configure audio source and sink.
    audio_device = None
    if input_audio_file:
        audio_source = audio_helpers.WaveSource(
            open(input_audio_file, 'rb'),
            sample_rate=audio_sample_rate,
            sample_width=audio_sample_width
        )
    else:
        audio_source = audio_device = (
            audio_device or audio_helpers.SoundDeviceStream(
                sample_rate=audio_sample_rate,
                sample_width=audio_sample_width,
                block_size=audio_block_size,
                flush_size=audio_flush_size
            )
        )
    if output_audio_file:
        audio_sink = audio_helpers.WaveSink(
            open(output_audio_file, 'wb'),
            sample_rate=audio_sample_rate,
            sample_width=audio_sample_width
        )
    else:
        audio_sink = audio_device = (
            audio_device or audio_helpers.SoundDeviceStream(
                sample_rate=audio_sample_rate,
                sample_width=audio_sample_width,
                block_size=audio_block_size,
                flush_size=audio_flush_size
            )
        )
    # Create conversation stream with the given audio source and sink.
    conversation_stream = audio_helpers.ConversationStream(
        source=audio_source,
        sink=audio_sink,
        iter_size=audio_iter_size,
        sample_width=audio_sample_width,
    )

    with SampleAssistant(conversation_stream,
                         grpc_channel, grpc_deadline) as assistant:
        # If file arguments are supplied:
        # exit after the first turn of the conversation.
        if input_audio_file or output_audio_file:
            assistant.converse()
            return

        # If no file arguments supplied:
        # keep recording voice requests using the microphone
        # and playing back assistant response using the speaker.
        # When the once flag is set, don't wait for a trigger. Otherwise, wait.
        wait_for_user_trigger = not once
        while True:
            if wait_for_user_trigger:
                #click.pause(info='Press Enter to send a new request...')
                click.pause(info='何かキーを押して、お話ください...')

            #u 音声入力を開始する
            continue_conversation = assistant.converse()

            # wait for user trigger if there is no follow-up turn in
            # the conversation.
            wait_for_user_trigger = not continue_conversation

            # If we only want one conversation, break.
            logging.info('「お話AIちゃん」の判定。once = ' + str(once) + ', continue_conversation = ' + str(continue_conversation))
            if once and (not continue_conversation):
                break

def start(_utility):
    main()

if __name__ == '__main__':
    main()

assistant_helpers.py

# Copyright (C) 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Helper functions for the Google Assistant API."""

import logging

from google.assistant.embedded.v1alpha1 import embedded_assistant_pb2


END_OF_UTTERANCE = embedded_assistant_pb2.ConverseResponse.END_OF_UTTERANCE


def log_converse_request_without_audio(converse_request):
    """Log ConverseRequest fields without audio data."""
    if logging.getLogger().isEnabledFor(logging.DEBUG):
        resp_copy = embedded_assistant_pb2.ConverseRequest()
        resp_copy.CopyFrom(converse_request)
        if len(resp_copy.audio_in) > 0:
            size = len(resp_copy.audio_in)
            resp_copy.ClearField('audio_in')
            logging.debug('ConverseRequest: audio_in (%d bytes)',
                          size)
            return
        logging.debug('ConverseRequest: %s', resp_copy)


def log_converse_response_without_audio(converse_response):
    """Log ConverseResponse fields without audio data."""
    if logging.getLogger().isEnabledFor(logging.DEBUG):
        resp_copy = embedded_assistant_pb2.ConverseResponse()
        resp_copy.CopyFrom(converse_response)
        has_audio_data = (resp_copy.HasField('audio_out') and
                          len(resp_copy.audio_out.audio_data) > 0)
        if has_audio_data:
            size = len(resp_copy.audio_out.audio_data)
            resp_copy.audio_out.ClearField('audio_data')
            if resp_copy.audio_out.ListFields():
                logging.debug('ConverseResponse: %s audio_data (%d bytes)',
                              resp_copy,
                              size)
            else:
                logging.debug('ConverseResponse: audio_data (%d bytes)',
                              size)
            return
        logging.debug('ConverseResponse: %s', resp_copy)

audio_helpers.py

# Copyright (C) 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Helper functions for audio streams."""

import logging
import threading
import time
import wave
import math
import array

import click
import sounddevice as sd


DEFAULT_AUDIO_SAMPLE_RATE = 16000
DEFAULT_AUDIO_SAMPLE_WIDTH = 2
DEFAULT_AUDIO_ITER_SIZE = 3200
DEFAULT_AUDIO_DEVICE_BLOCK_SIZE = 6400
DEFAULT_AUDIO_DEVICE_FLUSH_SIZE = 25600


def normalize_audio_buffer(buf, volume_percentage, sample_width=2):
    """Adjusts the loudness of the audio data in the given buffer.

    Volume normalization is done by scaling the amplitude of the audio
    in the buffer by a scale factor of 2^(volume_percentage/100)-1.
    For example, 50% volume scales the amplitude by a factor of 0.414,
    and 75% volume scales the amplitude by a factor of 0.681.
    For now we only sample_width 2.

    Args:
      buf: byte string containing audio data to normalize.
      volume_percentage: volume setting as an integer percentage (1-100).
      sample_width: size of a single sample in bytes.
    """
    if sample_width != 2:
        raise Exception('unsupported sample width:', sample_width)
    scale = math.pow(2, 1.0*volume_percentage/100)-1
    # Construct array from bytes based on sample_width, multiply by scale
    # and convert it back to bytes
    arr = array.array('h', buf)
    for idx in range(0, len(arr)):
        arr[idx] = int(arr[idx]*scale)
    buf = arr.tostring()
    return buf


def align_buf(buf, sample_width):
    """In case of buffer size not aligned to sample_width pad it with 0s"""
    remainder = len(buf) % sample_width
    if remainder != 0:
        buf += b'\0' * (sample_width - remainder)
    return buf


class WaveSource(object):
    """Audio source that reads audio data from a WAV file.

    Reads are throttled to emulate the given sample rate and silence
    is returned when the end of the file is reached.

    Args:
      fp: file-like stream object to read from.
      sample_rate: sample rate in hertz.
      sample_width: size of a single sample in bytes.
    """
    def __init__(self, fp, sample_rate, sample_width):
        self._fp = fp
        try:
            self._wavep = wave.open(self._fp, 'r')
        except wave.Error as e:
            logging.warning('error opening WAV file: %s, '
                            'falling back to RAW format', e)
            self._fp.seek(0)
            self._wavep = None
        self._sample_rate = sample_rate
        self._sample_width = sample_width
        self._sleep_until = 0

    def read(self, size):
        """Read bytes from the stream and block until sample rate is achieved.

        Args:
          size: number of bytes to read from the stream.
        """
        now = time.time()
        missing_dt = self._sleep_until - now
        if missing_dt > 0:
            time.sleep(missing_dt)
        self._sleep_until = time.time() + self._sleep_time(size)
        data = (self._wavep.readframes(size)
                if self._wavep
                else self._fp.read(size))
        #  When reach end of audio stream, pad remainder with silence (zeros).
        if not data:
            return b'\x00' * size
        return data

    def close(self):
        """Close the underlying stream."""
        if self._wavep:
            self._wavep.close()
        self._fp.close()

    def _sleep_time(self, size):
        sample_count = size / float(self._sample_width)
        sample_rate_dt = sample_count / float(self._sample_rate)
        return sample_rate_dt

    def start(self):
        pass

    def stop(self):
        pass

    @property
    def sample_rate(self):
        return self._sample_rate


class WaveSink(object):
    """Audio sink that writes audio data to a WAV file.

    Args:
      fp: file-like stream object to write data to.
      sample_rate: sample rate in hertz.
      sample_width: size of a single sample in bytes.
    """
    def __init__(self, fp, sample_rate, sample_width):
        self._fp = fp
        self._wavep = wave.open(self._fp, 'wb')
        self._wavep.setsampwidth(sample_width)
        self._wavep.setnchannels(1)
        self._wavep.setframerate(sample_rate)

    def write(self, data):
        """Write bytes to the stream.

        Args:
          data: frame data to write.
        """
        self._wavep.writeframes(data)

    def close(self):
        """Close the underlying stream."""
        self._wavep.close()
        self._fp.close()

    def start(self):
        pass

    def stop(self):
        pass


class SoundDeviceStream(object):
    """Audio stream based on an underlying sound device.

    It can be used as an audio source (read) and a audio sink (write).

    Args:
      sample_rate: sample rate in hertz.
      sample_width: size of a single sample in bytes.
      block_size: size in bytes of each read and write operation.
      flush_size: size in bytes of silence data written during flush operation.
    """
    def __init__(self, sample_rate, sample_width, block_size, flush_size):
        if sample_width == 2:
            audio_format = 'int16'
        else:
            raise Exception('unsupported sample width:', sample_width)
        self._audio_stream = sd.RawStream(
            samplerate=sample_rate, dtype=audio_format, channels=1,
            blocksize=int(block_size/2),  # blocksize is in number of frames.
        )
        self._block_size = block_size
        self._flush_size = flush_size
        self._sample_rate = sample_rate

    def read(self, size):
        """Read bytes from the stream."""
        buf, overflow = self._audio_stream.read(size)
        if overflow:
            logging.warning('SoundDeviceStream read overflow (%d, %d)',
                            size, len(buf))
        return bytes(buf)

    def write(self, buf):
        """Write bytes to the stream."""
        underflow = self._audio_stream.write(buf)
        if underflow:
            logging.warning('SoundDeviceStream write underflow (size: %d)',
                            len(buf))
        return len(buf)

    def flush(self):
        if self._flush_size > 0:
            self._audio_stream.write(b'\x00' * self._flush_size)

    def start(self):
        """Start the underlying stream."""
        if not self._audio_stream.active:
            self._audio_stream.start()

    def stop(self):
        """Stop the underlying stream."""
        if self._audio_stream.active:
            self.flush()
            self._audio_stream.stop()

    def close(self):
        """Close the underlying stream and audio interface."""
        if self._audio_stream:
            self.stop()
            self._audio_stream.close()
            self._audio_stream = None

    @property
    def sample_rate(self):
        return self._sample_rate


class ConversationStream(object):
    """Audio stream that supports half-duplex conversation.

    A conversation is the alternance of:
    - a recording operation
    - a playback operation

    Excepted usage:

      For each conversation:
      - start_recording()
      - read() or iter()
      - stop_recording()
      - start_playback()
      - write()
      - stop_playback()

      When conversations are finished:
      - close()

    Args:
      source: file-like stream object to read input audio bytes from.
      sink: file-like stream object to write output audio bytes to.
      iter_size: read size in bytes for each iteration.
      sample_width: size of a single sample in bytes.
    """
    def __init__(self, source, sink, iter_size, sample_width):
        self._source = source
        self._sink = sink
        self._iter_size = iter_size
        self._sample_width = sample_width
        self._stop_recording = threading.Event()
        self._start_playback = threading.Event()
        self._volume_percentage = 50

    def start_recording(self):
        """Start recording from the audio source."""
        self._stop_recording.clear()
        self._source.start()
        self._sink.start()

    def stop_recording(self):
        """Stop recording from the audio source."""
        self._stop_recording.set()

    def start_playback(self):
        """Start playback to the audio sink."""
        self._start_playback.set()

    def stop_playback(self):
        """Stop playback from the audio sink."""
        self._start_playback.clear()
        self._source.stop()
        self._sink.stop()

    @property
    def volume_percentage(self):
        """The current volume setting as an integer percentage (1-100)."""
        return self._volume_percentage

    @volume_percentage.setter
    def volume_percentage(self, new_volume_percentage):
        logging.info('Volume set to %s%%', new_volume_percentage)
        self._volume_percentage = new_volume_percentage

    def read(self, size):
        """Read bytes from the source (if currently recording).

        Will returns an empty byte string, if stop_recording() was called.
        """
        if self._stop_recording.is_set():
            return b''
        return self._source.read(size)

    def write(self, buf):
        """Write bytes to the sink (if currently playing).

        Will block until start_playback() is called.
        """
        self._start_playback.wait()
        buf = align_buf(buf, self._sample_width)
        buf = normalize_audio_buffer(buf, self.volume_percentage)
        return self._sink.write(buf)

    def close(self):
        """Close source and sink."""
        self._source.close()
        self._sink.close()

    def __iter__(self):
        """Returns a generator reading data from the stream."""
        return iter(lambda: self.read(self._iter_size), b'')

    @property
    def sample_rate(self):
        return self._source._sample_rate


@click.command()
@click.option('--record-time', default=5,
              metavar='', show_default=True,
              help='Record time in secs')
@click.option('--audio-sample-rate',
              default=DEFAULT_AUDIO_SAMPLE_RATE,
              metavar='

IFTTTとは何か

IFTTTとは

TwitterやFacebook、Gmail、Instagram、Evernote、Dropboxなど、数え上げたらキリがないほど、現在では実に多彩なWebサービスが提供されています。 こうしたWebサービスは単体でも非常に便利な機能を提供していますが、複数のWebサービスが連携できれば、新しいWebサービスとしてさらに多くのことを実現できるでしょう。 ただ、Webサービスの連携には、多くの場合プログラミングが必須であり、実現するにはハードルが高いと思われがちです。

しかし、こうしたWebサービスを連携させる「IFTTT(イフト:IF This Then That)」というサービスを利用すれば、 あるWebサービスと別のWebサービスを簡単に連携させて、新しいサービスにすることができます。 プログラミングは不要で、すでにIFTTTで提供されている既存の連携サービス(IFTTTでは「レシピ」と呼ぶ)を使えば、 アクセス許可やフィルターの条件などを記述するだけで済むのです。

IFTTTの名前の由来である、「もし(IF)『This(入力)』ならば(Then)『That(出力)』する」の「This」と「That」を対応サービスから選択すれば、 新しいサービスが作成できるのなのです。


IFTTTを使って新しいサービスを作成する

IFTTTの利用例

例えば、スマートフォンの位置情報サービス(ロケーションサービス)とメールを組み合わせれば、乗換駅に着いたら、 自動的に家族にメールを送るといったことが簡単に実現できます。 同様に、クラウドサービスからの障害メールが届いたら、SMSやSlackで知らせるといったことも可能です。 特定のハッシュタグを付けたTwitterのつぶやきをFacebookに自動投稿するとか、Instagramの写真をDropboxにバックアップするなど、 Webサービス同士を組み合わせることもできます。

IFTTT自体は、Linden Tibbets(リンデン・チベット)氏が2011年にサービスの提供を開始しており、決して新しいものではありません。 しかし、最近盛り上がりつつある「Google Home(Google Assistant)」や「Amazon Echo(Amazon Alexa)」などのスマートスピーカー(AIスピーカー)がIFTTTに対応しており、 他のWebサービスなどと連携して利便性が増すことから、急速に注目が高まっているのです。

もちろんIFTTTは、こうしたAIスピーカーだけでなく、iPhoneやAndroidといったスマートフォンのIFTTTアプリ、 マグネット式電子工作キット「littleBits」や小型コンピュータ「Raspberry Pi」、ソニーのブロック形状の電子タグ「MESH」といったデバイスにも対応しています。

IFTTTは、基本無料で利用可能です。ただし自作したアプレットを自社サービスや製品に組み込んだり、複数の出力を可能にしたりする場合は、 月額199ドルの「Partner」や月額499ドルの「Partner Plus」の契約が必要になります(開発者向けの「Maker」に登録することで、 無料で「1入力多出力」のレシピを作成することも可能)。

原稿執筆時点でIFTTTが対応しているサービスは、This(入力)で450以上、That(出力)で約400種類あります(日本で使用できないものも含む)。 IFTTTで利用可能なサービスは「See all services - IFTTT」を参照のこと。

IFTTTと同様のサービスとして、Yahoo Japanの「myThings」やMicrosoftの「Microsoft Flow」、Zapierの「Zapier」、Integromatの「Integromat」などがありますが、 対応するサービスの種類ではIFTTTが最も多いです。



森の小径3