himanago

C#とかAzureとかMS系の技術メモ中心に書きたいです。最近はLINE関連技術やVUIも多め。

C# によるスマートスピーカースキルのクロスプラットフォーム開発ライブラリ「XPlat.VUI」の紹介

本記事は、Qiita「スマートスピーカー Advent Calendar 2019」の3日目のエントリです。 qiita.com

この記事について

現在個人的に開発をして公開している、C#Googleアシスタント、Alexa、Clova に対応したクロスプラットフォームなスキルを効率よく開発できるようになるライブラリ「XPlat.VUI」を紹介します。

XPlat.VUI は、Googleアシスタント、Alexa、Clova に対応したスキル開発における処理の共通化が可能で、HTTPのリクエストをそのまま渡すだけで、各プラットフォームの起動およびインテントリクエストに対応したレスポンスを作って返せます。

背景

まずは簡単に、「XPlat.VUI」ライブラリを開発した背景について説明します。

C# で開発するメリット

C#スマートスピーカースキルを開発するメリットとして、

  • LINQ、async / await、非同期ストリームなど C# 言語そのものの強み
  • Visual StudioIDE)による強力な開発支援機能
  • 豊富な NuGet パッケージ
  • ASP.NET Core や Xamarin など C# を用いた他のプロダクトとのコード共有
  • Microsoft Azure との親和性

などが挙げられます。

特に Azure では、Azure Functions(Azure におけるサーバーレスなコード実行基盤サービス)を使用することが多く、スキルのバックエンドを効率よく実装できます。

また、スキル開発に C# / Azure Functions を使用する理由の一つに、Durable Functions があります。
Durable Functions は サーバーレスでありあがらステートフルな処理をコードのみで実現する Azure Functions の拡張機能です。
これを用いるとスマートスピーカースキルに多く存在する制約を超えていくことができます(今回は本題からずれるので詳細は割愛)。

クロスプラットフォーム対応の苦しみ

Alexa, Googleアシスタント, Clova では、それぞれカスタムスキル(アクション)を実行するときにやりとりされる JSONスキーマが異なります。
また、それぞれの SDK のしくみや使い方も違います。C# SDK においても、それは同様です。

そのため、3プラットフォームに同じ内容のスキルをリリースしたいと思った場合、ロジック部分はある程度共通化できても、それぞれで異なるライブラリを使用し異なるデータのやりとりを実装しなければなりません。

スマートスピーカースキルは使用感の軽いものが多く、スキル内容のコーディングそのものは簡単に済んでしまうケースが多いはずですが、3つのプラットフォームに対応させようとすると、その差異への対応のほうに多く時間を取られる結果となってしまいます。そのため、単一のプラットフォームでの提供のみで終わってしまうケースも多くあると思われます。

XPlat.VUI とは

そこで、XPlat.VUI を開発しました。

NuGet: www.nuget.org

ソースコードgithub.com

XPlat.VUIは、C# を使って Alexa, Googleアシスタント, Clova それぞれのプラットフォームに対応したスキルを共通の実装で開発できるようにしたライブラリです。
これを使えばそれぞれのプラットフォームでの差異を意識することなく、スキルの処理本体の実装に注力することができます。

基本的な使い方(Azure Functions の例)

Azure Functions でのスキル実装の例を見ていきます。

準備

HTTP トリガーの関数を作成

Visual Studio で Azure Functions のプロジェクトを作成し、HTTPトリガーの関数を作ります。言語は C# です(詳細は省略)。
NuGet より XPlat.VUI のパッケージをプロジェクトに追加しておきます。

AssistantBase 抽象クラスを継承したクラスを用意

以下のように、XPlat.VUI では AssistantBase を継承したクラスを作ります。
このクラス内で、スキルがいつ・どんな応答をするかを組み立てていきます。

public class MyAssistant : AssistantBase
{
}

ロガーなど処理の中で使用したいものをプロパティとして持たせたい場合は、IAssistant インターフェースを使って拡張します。

public interface ILoggableAssistant : IAssistant
{
    ILogger Logger { get; set; }
}

public class MyAssistant : AssistantBase, ILoggableAssistant
{
    public ILogger Logger { get; set; }
}

DI の設定

AssistantBase を継承したクラスをスキルのエンドポイントで使用するのですが、単純に

var Assistant = new MyAssistant();

とするよりも DI(Dependency Injection)によって DI コンテナからもらってくるのがおすすめです。
スキルの実装がエンドポイントと依存せず、実装・テストがしやすくなり、他のサービスクラスとの連携もしやすくなります。

なお Azure Functions で DI するには

が必要なので NuGet で追加・更新しておきます。

Azure Functions での DI は、以下のような Startup クラスを定義します。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using XPlat.VUI;

[assembly: FunctionsStartup(typeof(Sample.Startup))]
namespace Sample
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddAssistant<ILoggableAssistant, MyAssistant>();
        }
    }
}

エンドポイント関数の実装

以下のようにエンドポイントとなる関数を実装します。
Alexa, Googleアシスタント, Clova それぞれで異なるエンドポイントを用意するため、関数メソッドは3つ用意しています。
また、DI を使用するので クラス、メソッド双方から static を外し、コンストラクタで AssistantBase 継承クラスを受け取っています。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using XPlat.VUI;
using XPlat.VUI.Models;

namespace Sample
{
    public class Endpoints
    {
        private ILoggableAssistant Assistant { get; }

        public Endpoints(ILoggableAssistant assistant)
        {
            Assistant = assistant;
        }

        [FunctionName(nameof(GoogleEndpoint))]
        public async Task<IActionResult> GoogleEndpoint(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            Assistant.Logger = log;
            var response = await Assistant.RespondAsync(req, Platform.GoogleAssistant);
            return new OkObjectResult(response.ToGoogleAssistantResponse());
        }

        [FunctionName(nameof(AlexaEndpoint))]
        public async Task<IActionResult> AlexaEndpoint(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            Assistant.Logger = log;
            var response = await Assistant.RespondAsync(req, Platform.Alexa);
            return new OkObjectResult(response.ToAlexaResponse());
        }

        [FunctionName(nameof(ClovaEndpoint))]
        public async Task<IActionResult> ClovaEndpoint(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            Assistant.Logger = log;
            var response = await Assistant.RespondAsync(req, Platform.Clova);
            return new OkObjectResult(response.ToClovaResponse());
        }

    }
}

それぞれのエンドポイントで、

  • ロガーの受け渡し(任意)
  • RespondAsync メソッドのコール(プラットフォーム種別を指定)
  • 各プラットフォームに合わせたレスポンス変換・返却

を行っています。この部分は基本的に定型なので、毎回同じようなコードになるはずです。

スキル処理本体の実装

続いて、スキルの応答の中身を作っていきます。この部分がスキル構築の核になります。

リクエスト/イベントベースの処理

AssistantBase 継承クラスでのスキル実装は、起動やインテントリクエストなど、タイミングごとに用意された仮想メソッド(RespondAsync 内で実行される)をオーバーライドして行います。

例えば、起動時とインテントリクエストで何か処理を行いたい場合は以下のように OnLaunchRequestAsyncOnIntentRequestAsync をオーバーライドし、それぞれ処理を実装します。

public class MyAssistant : AssistantBase
{
    protected override Task OnLaunchRequestAsync(
        Dictionary<string, object> session, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    protected override Task OnIntentRequestAsync(
        string intent, Dictionary<string, object> slots, Dictionary<string, object> session,
        CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

OnIntentRequestAsync では インテント名やスロット(エンティティ)の値が引数から受け取れるので、処理の中で使用することができます。

XPlat.VUI では、3つのプラットフォームそれぞれで異なるリクエスト種別、イベントのタイミングをなるべく共通化して差異を吸収し、以下のメソッドを用意しています。

メソッド タイミング 備考
OnLaunchRequestAsync 起動時
OnIntentRequestAsync インテントリクエス Googleアシスタントは起動時以外
OnAudioPlayStartedAsync AudioPlayer での再生開始時 Alexa, Clova のみ
OnAudioPlayPausedOrStoppedAsync AudioPlayer での一時停止または停止時 Alexa, Clova のみ
OnAudioPlayNearlyFinishedAsync AudioPlayer での再生終了直前 Alexa のみ
OnAudioPlayFinishedAsync AudioPlayer での再生終了時 Alexa, Clova のみ

レスポンスの実装

オーバーライドメソッドの中では、スキルの応答内容を定義します。応答内容は Response プロパティに対して追加していきます。応答内容の追加に使用できるメソッドには、以下のものがあります。

Speak

スマートスピーカーが読み上げる、通常のテキストのレスポンスを定義します。

Response.Speak("こんにちは");
Play

mp3 オーディオファイルを再生します。効果音などでの使用を想定しています。

Response.Play("https://dummy.domain/myaudio.mp3");
Break

レスポンスの読み上げ・再生の間の休止(無音の空白)を「秒」で指定します。

Response
    .Speak("こんにちは")
    .Break(3)
    .Speak("サンプルスキルへようこそ");

Speak, Play, Break は、Clova を除き内部的に SSML に変換してそれぞれのレスポンスを作っていますが、Clova は SSML に対応していません。

そのため、特に break については標準機能に存在しないため、Clova の場合は指定秒数無音 mp3 を流すレスポンスを返すことで疑似的に SSML の break と同様の機能を実現しています。
ここで使用している無音 mp3 ファイルは silent-mp3リポジトリGitHub Pages として公開したものを使用しているので、スキルのコードやバックエンドサーバーに配置する必要がありません。

KeepListening

スキルの応答後、さらにユーザーによる返答を待つように指定します。
Alexa, Clova では ShouldEndSession の切り替えをし、Googleアシスタントでもレスポンスの expectUserResponse を切り替えています。

引数で Reprompt のメッセージを指定することができます。

Response
    .Speak("お元気ですか?")
    .KeepListening("元気かどうか教えてください。");
メソッドチェーン

これらは以下のようにメソッドチェーンを用いてワンライナーで書くことができます。

Response
    .Speak("お元気ですか?")                     // Speek simple text
    .Break(3)                                   // Pause for 3 seconds.
    .Play("https://dummy.domain/myaudio.mp3");  // Play mp3 audio.

ユーザーID/デバイスID

Request.UserId でユーザーID を、Request.DeviceId でデバイスID を取得できます。

Googleアシスタントでは、ユーザーID は XPlat.VUI で生成した文字列を疑似的なユーザーID として使用しています。また、Googleアシスタントでは現在デバイスID の取得には対応していません。

応用的な機能

そのほか、スキル開発の幅を広げる応用的な機能も持ちます。

プラットフォームごとに異なる処理を行う

3プラットフォームでコードを共有し処理を共通化しても、どうしても各プラットフォームで異なることをしたいことがあります。

たとえば、プラットフォームごとに異なるレスポンスを返したいことがあります。しゃべらせたいセリフを変えたり、流す効果音を変えたりすることが考えられます。
サウンドライブラリーなどはプラットフォームごとに異なるものが用意されていますし、しゃべり方もプラットフォームごとに違うため、言い回しを変えたりすることが考えられます。

そこで、Speak, Play, Break, PlayWithAudioPlayer では引数に Platform.GoogleAssistant などを渡すことで、指定したプラットフォームのみに含まれるレスポンスを定義することができます。
これも通常のレスポンス同様メソッドチェーンで書くことができるので、プラットフォームで異なるレスポンスを直観的に定義できます。

Response
    .Speak("私はグーグルです。", Platform.GoogleAssistant)
    .Speak("私はアレクサです。", Platform.Alexa)
    .Speak("私はクローバです。", Platform.Clova);

また、それ以外にロジックそのものを動作しているプラットフォームで切り替えたい場合は、Request.CurrentPlatform で分岐させることができます。
以下は、Clova の場合だけ LINE Bot(Messaging API)で Push メッセージを送信する処理を実装する方法です。

if (Request.CurrentPlatform == Platform.Clova)
{
    await lineMessagingClient.PushMessageAsync(Request.UserId, "Hello!");        
}

オーディオ再生(AudioPlayer / Media responses)

PlayWithAudioPlayer を使うと、AudioPlayer(Alexa, Clova)や Media responses(Googleアシスタント)を使ってオーディオコンテンツの再生が可能です。
通常レスポンスの Play でも mp3 再生は可能ですが、通常レスポンスでは再生時間の制限があるため専用のオーディオ再生機能がそれぞれのプラットフォームで用意されています。この機能はそれを共通化したものです(Googleアシスタントの Media responses でのオーディオ再生レスポンスの実装が特に辛かった…)。

Response.PlayWithAudioPlayer(
    "sample-id",                          // audio item id
    "https://dummy.domain/myaudio.mp3",   // audio url
    "Sample Title", "Sample Subtitle").   // タイトルなど

この部分は特に3プラットフォームでの差異が激しいので、複雑なイベント制御によるスキル構築をコード共有することは現状難しいです。
そもそも Googleアシスタントでは再生イベントのハンドリングは XPlat.VUI でも対応していないので、個別にインテントを作って処理する必要があります。

今後の機能追加予定

エンドポイントの共通化

現在3プラットフォームごとにエンドポイントとなる関数を別々に定義する必要がありますが、これを共通して1つで済ませられるようにできたらと考えています。

使用イメージとしては以下のようになります(RespondAsync内で送信元のプラットフォームを判別し、適切なレスポンスを返却する)。

namespace Sample
{
    public class Endpoints
    {
        private ILoggableAssistant Assistant { get; }

        public Endpoints(ILoggableAssistant assistant)
        {
            Assistant = assistant;
        }

        [FunctionName(nameof(CommonEndpoint))]
        public async Task<IActionResult> CommonEndpoint(
            [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req, ILogger log)
        {
            Assistant.Logger = log;
            return await Assistant.RespondAsync(req);
        }
    }
}

プロジェクトテンプレート

DI 部分とエンドポイントの実装をあらかじめ済ませた状態のテンプレートを作っておくことで、スキル開発者が 100% スキルの処理の実装に集中できるようにしたいと考えています。
まずは Azure Functions のテンプレートから作ろうと思いますが、折を見て ASP.NET Core や、AWS Lambda 用のものなども作れたらいいなと思います。

画面対応

将来的には Echo Show や Nest Hub, Clova Desk などのスマートディスプレイ対応も共通化できたらおもしろいなと思っています。Clova はまだ SDK が公開されていなかったり、どのような実装になるかは全く見当もつきませんが、いつかやってみたいと考えています。

まとめ

XPlat.VUI を使えば、簡単に3プラットフォーム対応のスキルを開発できます。

また、C# と Azure を用いることで、言語やクラウドの便利なところはもちろん、Visual Studio による恩恵によってもとても快適な開発が可能です(もちろん VS Code でもOK)。Durable Functions 等による応用的なスキルも開発できます。

スキル開発者は、作りたいものを形にすることに注力し、3プラットフォームに展開することでより多くのユーザーにスキルを使ってもらうことができます。

ぜひ XPlat.VUI を用いて様々なスキル開発に挑戦してほしいなと思います。不具合や要望、プルリクなども受け付けておりますので、ぜひ C# でのスキル開発に興味がある方は使ってみていただけるとうれしいです。