himanago

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

CEK.CSharp がアップデートで便利になりました

はじめに

だいぶ前になりますが、@kenakamu108さんに誘われて、LINE Developer Community の C# SDK 開発にかかわらせてもらっています。
自分は、いつも使っている Clovaスキル開発用のSDKである CEK.CSharp のアップデートをやらせてもらえることになり、少し前にリニューアル版をリリースしました(0.2.1 まではちょっとバグがあるので、0.2.2 が出たらアップデート推奨です)。

www.nuget.org

CEK.CSharpのリニューアル

今回のリニューアル(0.2.x 以降)では、他のLINE APISDKと合わせて名前空間LineDC.XXX に統一されるようになったり、推奨の使い方が変化したりとわりかし大きな変更になっています。

アップデートは、4人いるC# SDKチームのメンバーで意見を出し合いながら作ってきました。
#便利だなーと思う部分はだいたいもらった意見がもとになっているので、自分の功績はいったいどこなのか…(笑)

これまでのCEK.CSharp では CEK のリクエスト・レスポンスの薄いラッパーというかんじで、そのままの構成でオブジェクトを作っていかないといけなかった(オリジナルのリクエスト、レスポンスのスキーマ構造を理解していないといけない)ので、それが結構面倒でした。

新しい CEK.CSharp ではそこを軽減し、開発者がやることを実装そのものにできるよう、考え方が変わっています。

以下、新しくなったSDKの使い方を簡単に紹介します。

使い方

抽象クラス ClovaBase を継承したクラスを作る

まず ClovaBase という抽象クラスを継承します。これがスキルのロジック部分本体になります。
いままでになかったものですが、これがいちばん重要かつ大きな変更点です。

public class MyClova : ClovaBase
{
}

何か外部から受け取ったりするための機能を持たせたいときは、(このあと出てくるDIのために)拡張用のインターフェースを作ります。

public interface ILoggableClova : IClova
{
    ILogger Logger { get; set; }
}

public class MyClova : ClovaBase, ILoggableClova
{
    public ILogger Logger { get; set; }
}

DIを使ってインスタンスを取得

HTTPリクエストをさばく部分(Function Appの関数メソッドだったり、ASP.NET Coreのアクションメソッドだったり)で ClovaBase 継承クラスを使うために、DI します。
DI用に AddClova という拡張メソッドを用意しています。
最近 Azure FunctionsでもDIできるようになったのでうれしいですね(以下は ASP.NET Core の例)。

public void ConfigureServices(IServiceCollection services)
{
    services.AddClova<IClova, MyClova>();
    services.AddMvc();
}

RespondAsyncメソッドの呼び出し

HTTPリクエストをさばく処理は以下のようになります。
clova はDIで受け取ったオブジェクトの RespondAsync を呼んでその戻り値をHTTPレスポンスに入れるだけ。

var response = await clova.RespondAsync(Request.Headers["SignatureCEK"], Request.Body);
return new OkObjectResult(response);

リクエストごとのオーバーライドメソッド

リニューアルの一番のポイントはここです。
従来、switch とかで LaunchRequest やら IntentRequest ごとに分岐を作らなきゃいけなかった部分ですが、分岐はSDK内に隠蔽して、事前に各リクエストに対応するメソッドが用意されていて、RespondAsync を呼べばそれぞれのタイミングで動くようになってます。
ちょうど、Bot Framework の OnTurnAsync みたいなイメージで、開発者は独自の処理を行いたいタイミングのメソッドをオーバーライドしてコードを書くことになります。

public class MyClova : ClovaBase
{
    protected override async Task OnLaunchRequestAsync(
        Session session, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

オーバーライドできるメソッドはこれだけあります。
イベントリクエストをイベントごとに分けて用意したのはちょっとしたこだわりポイントです。

メソッド 引数
OnLaunchRequestAsync Session session, CancellationToken cancellationToken
OnIntentRequestAsync Intent intent, Session session, CancellationToken cancellationToken
OnEventRequestAsync Event ev, Session session, CancellationToken cancellationToken
OnSkillEnabledEventAsync Event ev, Session session, CancellationToken cancellationToken
OnSkillDisabledEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayFinishedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayPausedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayResumedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayStartedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnPlayStoppedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnProgressReportDelayPassedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnProgressReportIntervalPassedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnProgressReportPositionPassedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnStreamRequestedEventAsync Event ev, Session session, CancellationToken cancellationToken
OnSessionEndedRequestAsync Session session, CancellationToken cancellationToken
OnUnrecognizedRequestAsync CEKRequest request, CancellationToken cancellationToken

レスポンスの作り方

各オーバーライドメソッド内でやる、レスポンス内容の作成はメソッドチェーンで書けます。
メソッドチェーンのアイディアはチームの打ち合わせの中で出たものなんですが、とてもすっきり書けるのでとても気に入っています。

Response
    .AddText("こんにちは!");
    .AddUrl("https://dummy.domain/myaudio.mp3");
    .AddText("Hi!", Lang.En);
    .AddUrl("https://dummy.domain/myaudio.mp3", Lang.En);

ShouldEndSession も隠蔽していて、KeepListening というメソッドとセッションが継続されます。

Response
    .AddText("What do you want?", Lang.En)
    .KeepListening();

AudioPlayer

個人的によく使う AudioPlayer もシンプルなメソッドを用意しました。

メソッド 引数
PlayAudio Source source, AudioItem audioItem, AudioPlayBehavior playBehavior
EnqueueAudio Source source, params AudioItem[] audioItems
PauseAudio -
ResumeAudio -
StopAudio -

サンプル

Before / After がわかりやすいように、旧SDKでできたものを新SDKに変えて見比べられるようにしてみました。 題材は、少し前の スマートスピーカーを遊びたおす会 で話題になったちょまどさんの滑舌スキルです。

FunctionsのDI

スキルの本体である KatsuzetsuClova をDIします。

using LineDC.CEK;
using Line.Messaging;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using KatsuzetsuApp.Settings;

[assembly: FunctionsStartup(typeof(KatsuzetsuApp.Startup))]
namespace KatsuzetsuApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services
                .AddClova<ILineMessageableClova, KatsuzetsuClova>()
                .AddSingleton<ILineMessagingClient, LineMessagingClient>(_ => new LineMessagingClient(Keys.token));
        }
    }
}

関数本体

KatsuzetsuClova に、同じくDI で受け取った LineMessagingClient を 引き渡します。
こうやって引き渡せるようインターフェースの拡張が必要というわけです。

using System;
using Line.Messaging;
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;

namespace KatsuzetsuApp
{
    public class ClovaEndpoint
    {
        private readonly ILineMessageableClova _clova;

        public ClovaEndpoint(ILineMessageableClova clova, ILineMessagingClient lineMessagingClient)
        {
            _clova = clova;
            _clova.LineMessagingClient = lineMessagingClient;
        }

        [FunctionName("clova")]
        public async Task<IActionResult> Clova(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            var response = await _clova.RespondAsync(req.Headers["SignatureCEK"], req.Body);
            return new OkObjectResult(response);
        }
    }
}

ロジック部分

スキルの本体。処理を行いたいリクエストのメソッドをオーバーライドして実装します。

using KatsuzetsuApp.Settings;
using LineDC.CEK;
using LineDC.CEK.Models;
using Line.Messaging;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace KatsuzetsuApp
{
    public class KatsuzetsuClova : ClovaBase, ILineMessageableClova
    {
        public ILineMessagingClient LineMessagingClient { get; set; }

        protected override Task OnLaunchRequestAsync(Session session, CancellationToken cancellationToken)
        {
            Response
                .AddText(Messages.WelcomeMessage)
                .KeepListening();

            return Task.CompletedTask;
        }

        protected override async Task OnIntentRequestAsync(Intent intent, Session session, CancellationToken cancellationToken)
        {
            switch (intent.Name)
            {
                case IntentNames.DefaultIntent:
                    {
                        // 滑舌オーケー
                        if (intent.Slots.TryGetValue(SlotNames.NamamugiNamagomeNamatamago, out var slot) ||
                            intent.Slots.TryGetValue(SlotNames.SyusyaSentaku, out slot))
                        {
                            // LINE messaging に投げる
                            await LineMessagingClient.PushMessageAsync(
                                to: session.User.UserId,
                                messages: new List<ISendMessage>
                                {
                                    new TextMessage(string.Format(Messages.LineCongratsMessage, slot.Value)),
                                }
                            );
                            // Clova に喋らせる
                            Response
                                .AddText(Messages.GenerateCongratsMessage(slot.Value))
                                .KeepListening();
                        }
                        // 滑舌ダメです
                        else
                        {
                            await LineMessagingClient.PushMessageAsync(
                                to: session.User.UserId,
                                messages: new List<ISendMessage>
                                {
                                    new TextMessage(Messages.LineWrongMessage),
                                }
                            );

                            Response
                                .AddText(Messages.WrongPronunciationMessage)
                                .KeepListening();
                        }

                        break;
                    }
                default:
                    // noop
                    break;
            }
        }

        protected override Task OnSessionEndedRequestAsync(Session session, CancellationToken cancellationToken)
        {
            Response.AddText(Messages.GoodbayMessage);
            return Task.CompletedTask;
        }
    }
}

ロジック部分(ClovaBase継承クラス)がシンプルにわかりやすく作れるところがポイントですが、これくらいの規模のスキルだと拡張したインターフェースを用意して関数にDIするところが少し面倒かも(テンプレート化したりすれば軽減できそうではある)。

もちろんDIなしでも使えるので、ClovaBase継承クラスだけ作って関数上でnewしてもOK。てっとりばやく作りたければこれでいいかもです。

おわりに

SDKOSS の開発をがっつりやったのは初めてだったのでとても、勉強になりました。
今後もメンテナンスなど継続していきます。

そして、この新SDKを使った話を入れた書籍を技術書典で頒布します。

自サークルの『AzureでつくるクロスプラットフォームAIアシスタントスキル』
techbookfest.org

と、お隣の

『LINE API HANDBOOK』
techbookfest.org

にも Clova+C# で参加してます!

それぞれの本に書いた内容、リンクというか、ちょっと似てるけど違う…という箇所があるのでぜひ見比べていただけると!
(手抜きやコピペとか言わないでくださいね…)

日付上はもう明日。
雨の予報ですが、【す22D】と【す23D】でお待ちしてますm(_ _)m

Googleアシスタントでも ShouldEndSession = true; したい

はじめに

ちょっと間が空いてしまいましたが、技術書典7に向けて執筆していた中で見つけたスマートスピーカースキル開発まわりの小ネタを書きます。
今回もGoogleアシスタントC#です。

ShouldEndSession

タイトルに書いた通りなんですが、Alexa と Clova はレスポンスで ShouldEndSession = true; としてあげると、その返答でスキルのセッションを終了させることができます。

Googleアシスタントでは、会話の終了は Dialogflow のインテントの設定で行うのが一般的なようで、バックエンド側の指示でセッションを終了させるようなプロパティは(C#の)SDKでは見当たりませんでした。

f:id:himanago:20190919233530p:plain

3プラットフォームでなるべく同じような開発方法でできたらな、とあきらめきれず探していたら、以下のように書けば Dialogflow 側で設定しなくてもバックエンドの指示だけでセッションを終了できるということがわかりました。

var webhookResponse = new WebhookResponse
{                
    FulfillmentText = "もうこの話は終わりね。",
    Payload = new Struct
    {
        Fields =
        {
            {
                "google", Value.ForStruct(new Struct
                {
                    Fields =
                    {
                        { "expectUserResponse", Value.ForBool(false) },
                    }
                })
            },
        }
    }
};

Payload の中で、expectUserResponsefalse にすればOK。
これで3プラットフォームで同じような作り方ができます!

目的は共通化

実はこれを調べていた目的は、3プラットフォームの「共通化」でした。

3つのプラットフォームに向けたスキル開発をする際、コードをいかに共通化できるかは、設定できる項目をどれだけ共通化できるかがカギです。
3プラットフォームの最大公約数だけでスキル開発が完結できるのであれば、共通化ライブラリが作れるからです。
(なので ShouldEndSession 的な機能をGoogleアシスタントで見つけることは非常に大きな意義があった)

おわりに

そんなこんなで、技術書典で頒布する本、入稿できました(ほんとぎりぎりでどうなることかと…)。

実はこの本の中で、C#クロスプラットフォーム開発できるライブラリを作る話を紹介しています。
そして、実はそのライブラリを、すでに NuGet で公開していたりします。
…ただ、ちょっとまだ粗削りな部分があるので、もう少し詰めたらここにも記事を書いて、少しずつアピールしていきたいな、と。。

目指すのはスマートスピーカースキルにおける Xamarin.Forms みたいなもの。
最大公約数な機能を共通のコードで実現しつつ、必要に応じてプラットフォーム固有の実装ができるようなものにしたいと思っています。

ということで技術書典も今週末…。。
『AzureでつくるクロスプラットフォームAIアシスタントスキル』、ぜひいらしてください!m(_ _)m

techbookfest.org

Googleアシスタント対応バックエンドを C#+Azure Functions で「簡単に」作る(ProtcolBufJsonResult不要論)

はじめに

今日もスマートスピーカースキル開発まわりの小ネタを。
今日はGoogleアシスタントでいきます。

ProtcolBufJsonResult 的なクラス

Googleアシスタント用のバックエンドを C# + Azure Functionsで作るときの話です。

Azure Functionsでバックエンドを作るのはとてもお手軽なんですが、毎回こんなクラスを用意していました(地味に面倒)。

JSONシリアライズをするための、IActionResult の実装です。

public class ProtcolBufJsonResult : IActionResult
{
    private readonly object _obj;
    private readonly JsonFormatter _formatter;

    public ProtcolBufJsonResult(object obj, JsonFormatter formatter)
    {
        _obj = obj;
        _formatter = formatter;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        context.HttpContext.Response.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json; charset=utf-8"));
        var stringWriter = new StringWriter();
        _formatter.WriteValue(stringWriter, _obj);
        await context.HttpContext.Response.WriteAsync(stringWriter.ToString());
    }
}

C# で開発をされている諸先輩方が使っていたので必須なのかなぁとまねしてたのですが…。

実はいらなかった

作らなくてもよかったみたいです。

return new OkObjectResult(webhookResponse.ToString());

というようにレスポンスのオブジェクトを ToString() するだけでシンプルに返却できました。

関数コード全体だとこんなかんじです。

※もはや会話でもなんでもないですが…

using Google.Cloud.Dialogflow.V2;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
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;

namespace Sample
{
    public static class GoogleEndpoint
    {
        [FunctionName(nameof(GoogleEndpoint))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            var parser = new JsonParser(JsonParser.Settings.Default.WithIgnoreUnknownFields(true));
            var webhookRequest = parser.Parse<WebhookRequest>(await req.ReadAsStringAsync());
            var webhookResponse = new WebhookResponse();

            switch (webhookRequest.QueryResult.Intent.DisplayName)
            {
                case "Default Welcome Intent":
                    webhookResponse.FulfillmentText = "こんにちは。";
                    break;

                case "CustomIntent":
                    webhookResponse.FulfillmentText = "そうですね。";
                    break;

                case "Default Fallback Intent":
                default:
                    webhookResponse.FulfillmentText = "特に何もしません。";
                    break;
            }

            return new OkObjectResult(webhookResponse.ToString());
        }
    }
}

おわりに

意外と簡単にできるなぁという気づきでした。

技術書典で頒布する本では上記の書き方で書いてます。

頒布するのは Azure && (C# || ノンコーディング) でクロスプラットフォームスマートスピーカースキル開発する本で、順番にやっていくとAzureのPaaSとスマートスピーカースキル開発両方をマスターできるように構成してます!

『AzureでつくるクロスプラットフォームAIアシスタントスキル』、興味のある方はぜひm(_ _)m

サークルチェックもお願いします!

techbookfest.org

Alexa.NET を使うなら ResponseBuilder がおすすめ

前置き

技術書典7 で本を出します!
Azure && (C# || ノンコーディング) でクロスプラットフォームスマートスピーカースキル開発する本です。

techbookfest.org

いままで試してきたことをまとめたいなぁと思って書いていたのですが、書いているといろいろ気づくこととか新たに知ることも多かったりします。

そこで、技術書典までにちょこちょこそういった小ネタを少しずつブログのほうにも載せていこうと思います。

C# で Alexa開発

今回は Alexaスキルの開発の話です。
普段 Alexa.NET という NuGetパッケージを使ってC#+Azure Functions でスキル開発しています。

www.nuget.org

で、このSDKなんですが、よくよく見てみると便利な機能がありました。

今回紹介するのは、そのひとつ ResponseBuilder です。

ResponseBuilder とは

Alexa に返すレスポンスオブジェクトを簡単に作れる便利クラスです。

代表的な機能は TellAsk の2つです。

このメソッドの戻り値をそのままHTTPレスポンスで返してしまえばOKという、非常にシンプルで使い勝手のいい機能です。

Alexa のレスポンス

Alexa のレスポンスは shouldEndSessiontrue にするか false にするかで対話を継続するかどうかを切り替えることができます。

shouldEndSessionfalse にして返すとAlexa がしゃべったあとユーザーの返答を待つためにリスニング状態でスキルのセッションが維持されます。

ResponseBuilder.Tell

Tell メソッドは、単純なテキストのメッセージの読み上げをセッションを継続せずに行います(shouldEndSession: trueでレスポンスを作ってくれる)。

Tell は「伝える」という意味であり、質問ではありません。なのでユーザーの返答が不要なときに使うことになります。

ResponseBuilder.Ask

対して Ask メソッドは、単純なテキストのメッセージの読み上げをセッションを継続して行います(shouldEndSession: falseでレスポンスを作ってくれる)。

Ask は「尋ねる」という意味で、質問です。だからユーザーの返答が必要。わかりやすい!

中学生でも知っているような明確な単語で動きをシンプルに定義していてとてもよいです。こういうSDKの作り方、見習いたい。

サンプルコード

Azure Functions でのコードです(なんてことない「はい」「いいえ」だけの会話)。

いちいち response.ShouldEndSession = false; とか書かなくていいのですっきりです。

あと地味にビルトインインテントを定数で用意してくれてるのもポイント高い。

using Alexa.NET;
using Alexa.NET.Request;
using Alexa.NET.Request.Type;
using Alexa.NET.Response;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Threading.Tasks;

namespace Sample
{
    public static class AlexaEndpoint
    {
        [FunctionName(nameof(AlexaEndpoint))]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            var request = JsonConvert.DeserializeObject<SkillRequest>(await req.ReadAsStringAsync());

            // レスポンス用の変数を宣言
            SkillResponse response = null;

            switch (request.Request)
            {
                case LaunchRequest lr:
                    response = ResponseBuilder.Ask(
                        "こんにちは。お元気ですか?", new Reprompt("元気ですか?"));
                    break;

                case IntentRequest ir:
                    switch (ir.Intent.Name)
                    {
                        case BuiltInIntent.Yes:    // 「はい」と答えたとき
                            response = ResponseBuilder.Ask(
                                "それはよかったです。まだお話ししますか?", new Reprompt("まだお話しますか?"));
                            break;

                        case BuiltInIntent.No:     // 「いいえ」と答えたとき
                            response = ResponseBuilder.Tell("そうですか。それではまた元気なときにお話ししましょう。");
                            break;

                        default:
                            response = ResponseBuilder.Tell("またお話ししましょう。");
                            break;
                    }
                    break;
            }

            return new OkObjectResult(response);
        }
    }
}

C# 8.0 の正式リリースが来たら、switch式でもっとすっきりしそうな可能性を秘めてる気がする。

おわりに

ということで、あまり記事として紹介されていない話かなと思ったので紹介してみました(GitHubのREADMEにはちゃんと書いてある)。

技術書典で頒布する本にはこういった話もたくさん入れつつ体系的にまとめようとしています(まだできてないw)。

『AzureでつくるクロスプラットフォームAIアシスタントスキル』、興味のある方はぜひm(_ _)m

サークルチェックもお願いします! techbookfest.org

CEK裏技「無音無限ループ」スキルをストア公開するためには?

はじめに

前回こんな記事を書きました。 himanago.hatenablog.com

今回はその続編として、裏技である「無音無限ループ」を使用した腹話術スキルをストア公開した際の話を書きます。

特に無音無限ループは使い方によっては非常に危険なので、安全性が要求されるストア公開では細心の注意が必要です。
そんな話を下記「第4回ボット自慢/苦労自慢 LT 大会」でも話したので、そのときのスライドを交えつつ、その後の改善についても併せてまとめます。

linedevelopercommunity.connpass.com

公開したスキル

「テキスト腹話術」という、LINE Bot経由で好きなことをClovaにしゃべらせるスキルです。
clova.line.me

アーキテクチャ等は前回記事を見てください。

Azure Functionsなどで構築しています。
そして肝になるのがCEK裏技の「無音無限ループ」(勝手に命名)です。

無音無限ループとは

f:id:himanago:20190728014222p:plain

CEKの AudioPlayer 機能で無音mp3の再生をさせ、その再生終了時の AudioPlayer.PlayFinished イベントで再度無音mp3を再生させることで無限ループを作り、スキルを終了せず無限のセッションを得る裏技です。

f:id:himanago:20190728014726p:plain

テキスト腹話術スキルでは、「LINEからのメッセージ送信」で無限ループをbreakし、その内容をしゃべるよう実装しています(ここでDurable Functionsを組み合わせています。詳細は前回記事)。

ストア公開のきっかけ

もともとこのスキルはDurable Functionsを活かしたサンプル(というかネタスキル)として開発したものだったので、一般公開を目指してデザインされたものではありませんでした。

実は以前、Clova Skill Awards を狙って新しいスキルを開発していたのですが、作っていたものとほぼ同じギミックのスキルが先に公開されてしまいました。

後発ではパクリになってしまうため(特にアワード期間中は)出すことができなくなり、アワードに応募するために秘蔵の腹話術スキルを公開に向けて再構築することにしました(今回のアワードはストア公開が前提)。

アワードは無事入賞できてよかったのですが、ギミックがかぶったスキルが最優秀賞(!)だったので、やっぱりちょっと悔しかったですね…。

という感じで急遽のストア公開でしたが、裏技を使っているスキルということでいくつかのハードルがありました。

無音無限ループの危険性

そもそも無音無限ループ自体が、使い方次第で非常に危険だということが挙げられます。

f:id:himanago:20190728015639p:plain

無音無限ループは、短い無音mp3を繰り返し再生させることで無限のセッションを得る裏技ですが、それが動作している間はClovaへの音声指示が効かなくなります。

例えば1秒のmp3をループ再生している場合、1秒間隔でClovaは新しい処理を行うことになるので、ユーザーの音声を聞き取る余裕がありません。

テキスト腹話術の場合、LINE Botのリッチメニューにより終了操作が行えますが、手元にLINEがない場合(特に連携アカウントの持ち主が外出中でほかの人がスキル起動した場合など)やそもそもBotを友だち追加していない場合、詰みます。

こうなると、デバイスを再起動するしか止める方法がないのです…。

ストア申請した結果

アワード狙いのストア申請とはいえ、無音無限ループの手法自体は「これを世に出さないのはもったいない!」と思っていたので、申請時のコメントで「裏技っぽいけどスキルの幅が広がる思うのでどうか通して…!」的なことを書いて泣きつきながら申請しました。

…いま思うとちょっと恥ずかしいですが。

で、さすがに一発通過とはならずリジェクトされました。

細かい指摘はいくつかありましたが、大きかったのは以下2点。

友だち追加していない場合には先に進めないようにしてほしい

友だち追加していない状態だと、スキル使えないですからね。

これは当然。

ちなみに、Botとの連携が必須のスキルの場合は必ずチェックされるポイントのようで、 説明文への明記も必須みたいです。

また、Botのアカウント名もスキルと同じ名前にするなど、わかりやすくすることが求められます。
※アカウント名の変更には制限(1度変えると7日間変更できなくなる!)があるので要注意

ちなみに友だち追加してるかどうかのチェックは(急いでたし)こんなふうに「プロフィール取得に失敗したら友だち追加していないとみなす 」って感じで実装したんですが、これでよかったんだろうか…

// 友だち追加チェック
try
{
    await messagingClient.GetUserProfileAsync(userId);
}
catch
{
    cekResponse.AddText("連携するLINEアカウントが友だち追加されていません。" +
        "Clovaアプリの本スキルのページから、連携するLINEアカウントを友だち追加してください。");
    break;
}

音声でスキルを終了できるようにすることは必須

まぁ、やはりそうですよね。友だち追加しててもBot操作ができないケースでは変わらず詰みますからね。

やはり音声によるコントロールを行えるようにすることはマストなようでした。

無音無限ループ中の挙動を検証

音声で終了操作をするために、無音無限ループの動作を検証してみました。

すると、いくつかの事実が判明しました。

無音無限ループ中、

  • shouldEndSessionをfalseにした状態で
  • Clovaに何らかの発話をさせると
  • その後の一定時間(無音mp3再生終了まで)は

Clovaがウェイクワードなしで指示を聞き取る状態になっていたのです。

これまで無音mp3を1秒にしていた(+ウェイクワード認識時の効果音をオフにしていた)ため気づかなかったのですが、mp3の長さを長めにしたら、腹話術による発話後にClovaが指示待ち状態になることに気づきました。

といっても、shouldEndSession = falseでしゃべらせてるんですから当たり前ですね。
これは普通のスキルにおいて、Clovaの発話後にユーザーの返答待ちにするやりかたです。

なので、「止めて」をインテントで登録しておけばスキルが終了することがわかりました。

また、mp3再生中、ウェイクワード「ねぇClova」で指示を聞いてくれることも判明。

これもmp3を長めにしたら気づいたことですが、よくよく考えてみれば、これはAudioPlayerの機能として当然の動きで、音楽再生のスキルを作った場合、「止めて」とか「次」とか言えるようにするのが普通ですよね。

ということで、音声でスキルを終了させる隙が2か所存在したのです。

f:id:himanago:20190728023335p:plain

無音無限ループ “安全版”の誕生 ⇒ 無事公開!

こうして、音声でコントロールできることがわかりました。

公開用に、無音mp3の長さを「5秒」にしました。

これは反応速度の許容範囲と音声指示に必要な長さのぎりぎりの妥協点です。これより短いと音声コントロールの失敗率が高くなり、長いと腹話術の反応速度が非常に鈍く感じるようになります(5秒でもサーバー側のタイミング次第で10秒ほど待つこともありますが)。

なんとか公開までたどり着くことができました。

細かい点含め、申請とリジェクトを何往復もしてようやく公開になりました。

最後まで対応いただいたストアの審査担当のみなさんには本当に感謝です。ありがとうございました。

その後のアップデート

先日アップデートを行い、shouldEndSession = falseでしゃべらせて「止めて」待ちにするのはやめました。

理由としては、スタンバイ効果音をオンにしていると、Clovaがしゃべるたびにポンという音が出て腹話術中にかなり邪魔になるのと、Clovaへの指示ではない会話も聞き取ってしまい、誤動作の原因になってしまうからです。
利用例で挙げている「ごっこ遊び」とかでは致命的。

また、対象デバイスからXperia Ear Duoを外しました。
こちらはストアの担当の方から正常動作しないという報告を受けたためです。

AudioPlayer利用のスキルに対応したとのことだったのでいけると思っていたのですが、実機を持っていないので検証できず泣く泣く対象外ということで…。

LT資料

スライドはこちらに置いてあります。

www.slideshare.net

Durable FunctionsとCEK裏技「無音無限ループ」で「テキスト腹話術」を開発しました

はじめに

先日書いた下記記事の、詳細解説その1です。
まずは開発したきっかけとメインの機能の解説です(公開の話はまた次回)。 himanago.hatenablog.com

この記事では、

LINE Developer Community : 第 2 回 ボット自慢 LT 大会でLT登壇した資料

www.slideshare.net

および

[Cogbot 勉強会 Special ★ LINE から話せる楽しいチャットボットを作ろう!] (https://cogbot.connpass.com/event/124711/)でLT登壇した資料

www.slideshare.net

をベースに書いていきます。上記2つの資料はほとんどいっしょです。

開発のきっかけ

Azureを使うとすごいBotやVUIスキルが作れるよ!ということを示したかったのがきっかけです。
結果的にVUIの常識を覆すような面白いものができたのでよかったです。

ちなみにBOOT AWARDSでファイナル進出した「絵本読み聞かせ」でも使ったテクニックの流用で、これのリニューアル中に思いついたネタでもあります。

f:id:himanago:20190714095212p:plain

(でも、この「絵本読み聞かせ」はあてにしていたText-to-speechのAPIが廃止されちゃったため公開を断念…ゴメンナサイ。。)

f:id:himanago:20190714095216p:plain

Demo

デモ動画です。LT登壇したときはClova Friendsでやりましたが、自宅で撮ったこの動画はClova Deskです。
※ちなみにこれは開発時のものなので、現在のバージョンと少し動作が異なります

f:id:himanago:20190714100143p:plain

こんなふうに、スキル起動中にClovaが待機状態になり、その間にLINEでメッセージを送るとその内容をしゃべってくれる、というスキルです。

通常、決まったことしか話してくれないClovaに好きなセリフを言わせ、はげましてもらったり、ごっこ遊びなんかに使ったりできます。
f:id:himanago:20190714100253p:plain

Botには、事前に使いたいセリフをテンプレートとして登録しておく機能も備えています。

f:id:himanago:20190714100437p:plain

すごいところ

f:id:himanago:20190714100519p:plain

対話が原則のClovaスキルの常識を覆す!

まずポイントは、ユーザーが話しかけなくてもClovaだけがしゃべりだす点です。

VUIは基本的にユーザー側から問いかけて使うものなので、スマートスピーカーからしゃべってくる、というのは新しい可能性につながりそうな予感がします。

その場で何でもしゃべってくれる!

LINEで入力した内容をそのまましゃべらせる、文字通りの「腹話術」ができます。

Clovaに言ってほしいと思う自由な言葉を言わせることができることができます。

いつまでも続くスキルのセッション!

一番の目玉が、セッションが勝手に切れず長時間遊べる点です。
スマートスピーカースキルを使ったことがある方なら、スキル起動後はユーザーと対話を続けないとセッションが切れてスキルが終了してしまいます。

また、スキルを開発したことがある方なら、サーバー側でもタイムアウトが厳しく、スキル起動中に長時間待たせることができない点に苦労することが多いと思います。

しかし、このスキルではユーザーからの操作を、スキルを起動したまま「無限に」待ち受けることができます。

これを実現しているのが「無音無限ループ」と「Durable Functions」のコンボです。

アーキテクチャ

f:id:himanago:20190714101237p:plain

アーキテクチャはこれだけです。

基本的にバックエンドはAzure Functionsのみで実装しています。

f:id:himanago:20190714101308p:plain

は?

f:id:himanago:20190714101345p:plain

長時間起動したり、テンプレート作成のように会話の流れを覚えてステートフルに処理する機能を持っていますが、本来単純な関数を作る機能であるFaaS(Function as a Service)のAzure Functionsだけで、このような処理を実現できるとは思えません。

f:id:himanago:20190714101535p:plain
でも、できちゃうのがAzure Functionsなんです。Durable Functionsという拡張機能を使うことにより、関数がステートフルに動くようになるのです。

Durable Functions

Durable Functionsでできること

f:id:himanago:20190714101637p:plain
図は公式のドキュメントからとってきたものです。
Durable Functionsは、状態を維持して行うこれらのような一連のワークフローをシンプルな関数・コードのみで実現できる拡張機能です。

Durable Functionsのしくみ

f:id:himanago:20190714101856p:plain

Durable Functionsでは3種類の関数を組み合わせてステートフルな処理を実現します(v2.0からはさらにEntity Functionというものが追加され、さらに幅が広がります)。
関数の実行状況など、ステートフルに情報を保持しながらいいかんじに動いてくれるものですが、履歴などの実行情報をストレージに書き込んで勝手に管理してくれます。

OrchestrationClient(Starter関数)

外部から呼び出し/実行される関数本体で、CEKやMessaging APIからのHTTPリクエストをで呼び出されるものです。
今回使ったものではHTTPトリガーの関数です。Orchestratorを起動する役割を持ちます。

Orchestrator関数

次に挙げるActivity(実処理を担当する関数)を呼び出して状態を管理し、関数のオーケストレーションを担当します。
制約として、オーケストレーターではランダム値やI/O処理、非同期APIの呼び出しを直接行うことは禁止です(Activityにやらせる必要がある)。

Activity関数

Orchestratorからの実行指示で起動する関数で、アプリケーションの機能を担当します。

Durable Functionsではこれらの組み合わせで複雑なフローをシンプルに実現していきます。

Durable Functionsの関数で使用する代表的なメソッド

f:id:himanago:20190714102510p:plain
関数同士を連携させて"Durable"(持続的)な処理を作る部品が揃っています。ここに挙げたのは一部ですが、全体でもそこまで数が多いわけではないので、それらを一度押さえてしまえば、組み合わせでいろんなことが実現できます。

公式ドキュメントに載っている実装例(監視とか人による操作とか)がピンと来なくても、それ以外にも意外と使える場面は多いのではないか?と思うので、いろいろ試してみたいと思っています。

腹話術スキルでの利用例

Durable Functionsは、まずBotのテンプレート作成で使っています。

f:id:himanago:20190714102947p:plain

リッチメニューから「テンプレート作成」を押してオーケストレーターを起動し、外部イベントを待機する状態にします。

その間、LINEからメッセージを送られた際にそれを外部イベントとして RaiseEventAsync することで、待機していたオーケストレーターが動きセリフリストに追加する、という具合です。

コード

f:id:himanago:20190714103231p:plain

こんなふうに、本当にシンプルなコードだけで複数リクエストの連携が実装できるというのは手軽で素晴らしいです。

ストレージや複数の連携などを意識せず実装できるという点で、より「サーバーレス」になるものだということもできるのですが、そもそもステートレスでない点で「サーバーレスを逸脱する」と言う人もいそうで、なかなか説明が難しいと最近は感じています。

無音無限ループ

さらにこのスキルでもう一つ肝になっているのが、「無音無限ループ」というCEK(Clova Extensions Kit)の裏技です。
これ自体は「絵本読み聞かせ」の機能を実装しているときに見つけたものです。

無音無限ループとは

f:id:himanago:20190714103555p:plain

CEKには、 AudioPlayer という機能があり、mp3再生をClovaにさせることができます。
この機能でmp3を再生すると、再生終了時、AudioPlayer.PlayFinished というイベントが発行され、サーバーに通知されます。

ごく短い長さの「無音のmp3ファイル」を用意し、これをAudioPlayerで再生させ、PlayFinished イベントが来たら再び同じ無音mp3を再生させるということをすれば、スキルを維持したままClovaスキルを待機状態にし、理論上無限のセッションを得ることができるのです。

もちろん、そのまま無限に繰り返すだけだと何もできないので、何らかのフラグを持たせ、特定の条件でループをbreakするようにしておけば、さまざまなことが実現できます。

そのbreakする部分にDurable Functionsのイベント待機が相性がよいです。

腹話術スキルでの利用例

今回は、こんなかんじで「LINEからの入力」で無限ループを脱出するように実装しています。
f:id:himanago:20190714103734p:plain

LINEからの入力を待機するオーケストレーターを作っておき、その実行状態を監視、無限ループの継続条件にします。

外部イベントが発生すると無限ループが終了、Clovaに入力内容をしゃべらせる処理を実行するという流れです。

コード

f:id:himanago:20190714104615p:plain
Durable Functionsではオーケストレーターの実行状態を確認するメソッド(外部イベントの待機が継続しているか確認することができる)があるので、無限ループの継続条件にそのまま使えばOK。

超お手軽です!

苦労した点

f:id:himanago:20190714104855p:plain
複数のイベントをお互いに監視しあうようなロジックになっているので、タイミングを合わせるのが結構大変です。

微妙に隙が生まれて無反応になってしまうことがあったので、そのあたりをできる限り調整した部分がここのコードです。

Durable Functionsは便利ですが、完全リアルタイムで同期されるような処理を作るのには、実はあんまり向いていないなと実感した部分です。

ここまでやっても、タイムラグはどうしても生まれてしまいます(腹話術スキルでは無音mp3の秒数を短くして反応をよくすることもできます)。

イベント監視の反応をよくする方法

Functionsの host.json の設定ファイルで、 maxPollingInterval を設定することで反応をよくすることができるようです。

{
  "version": "2.0",
  "extensions": {
    "queues": {
      "maxPollingInterval": "00:00:05"
    }
  }
}

ポーリングの間隔はデフォルト30秒なようなので、リアルタイム性を求めるのであれば、ここを設定することが必要です。

おまけ:翻訳機能

Cogbot勉強会でのLT登壇をきっかけに、Cognitive Servicesを使った翻訳機能を追加しました。
勉強会の時間内では間に合わなかったものの、あとから完成させました。

Clovaがしゃべれる英語と韓国語に対応しています。
f:id:himanago:20190714105605p:plain

おわりに

今回はDurable Functionsの機能を最大限に活用するサンプルとして実装し、LTで紹介しましたが、このあとこのスキルはストア公開につながりました。

その話はまた別の機会に書いていこうと思います。

公開に伴い何か所か修正したので、その前のプロトタイプ版(翻訳追加時点)のコードへのリンクを置いておきます。

github.com

Clovaスキル「テキスト腹話術」の開発とそれにまつわる登壇、ストア公開までの道のり

はじめに

平成最後の月である2019年4月に、スマートスピーカースキルの開発と、それにあわせた登壇をいくつか行いました。

本当は、登壇後すぐにブログを書いておきたかったのですが、4月の終わりごろに自宅に住めなくなり、引っ越し先として契約した家もまたすぐに住めなくなるというけっこう深刻かつレアな状況に陥ったため、なかなかブログ執筆などのためのまとまった時間がとれませんでした。

今月に入り、無事に新しい家へ引っ越してこれたので、心機一転、書いていきたいと思います。

きっかけ

LINE Developer Communityの第1回ボット自慢大会(自分は参加していなかった)で、こんなツイートをみたのがきっかけ。

Azureが好きで、LINE関連の開発もしている身としては、AzureがLINE開発との相性がいいことも知っていて、なんだか悔しく思ったので、こうリプライ。

こんな流れで、第2回でAzureを使ったLINE Bot(+Clovaスキル)を披露することになりました。

第2回自慢大会

そして迎えた自慢大会。 linedevelopercommunity.connpass.com

Azureの中でも特徴的なDurable Functionsを使ったClovaスキルで「Azureはいいぞ」をしようと思い、サンプルスキルを開発してLT登壇しました。
LINE Botとも連携し、Bot・スキル双方でDurable Functionsを使用しました。

また、「無音無限ループ」というClovaスキル開発での自分のとっておきの技を使ったので、けっこう大きなインパクトを与えることができました。

Cogbot勉強会

つづいて、こちらのイベントでもほぼ同じ内容でLT登壇しました。
cogbot.connpass.com

Bot Framework (Azure Bot Service) と Cognitive ServicesでLINE Botを作るハンズオンが中心の勉強会だったので同じ内容はどうかな…と思いながらの参加でしたが、冒頭のセッションではClovaへの言及もあり、ミニハッカソンの成果LTとしてわりと自然な流れでお話をさせていただくことができました。

Global Azure Boot Camp 2019

そして最後に、大変ありがたいことに、Azureの大きなイベントで登壇機会を得られました。 jazug.connpass.com

こちらは、公募セッションとして45分お話しできる機会をいただきました。

自慢大会、Cogbot勉強会で紹介したスキルのほか、Qiitaに書いたLogic Appsでのクロスプラットフォームスキル開発の話や、Functionsのコールドスタート対策の話も紹介し、それまでにやってきたAzureでのスマートスピーカースキル開発の総決算としてお話をしました。

新しく生まれたスキル

自慢大会をきっかけに生まれたのは、LINE Botを使ってClovaで腹話術ができるスキル、「テキスト腹話術」です。

Durable Functionsで楽に複雑なことができるよ!というサンプルスキルのつもりで作ったものでしたが、ストアで公開いただくことができました。

clova.line.me

そしてなんと、LINE Clova Skill Awards の「LINE(Messaging API、LIFF、LINE Pay等のAPI)との連携がうまく活用されているスキル」部門で入賞することもできました(下記リンクの下のほうに載ってます)。

engineering.linecorp.com

5月中旬が締め切りだったこのミニアワードですが、実は、これに向けて別のスキルを作っていました。

が、開発に苦戦&アイディアが他の方のスキルと被ってしまったため、締め切り間際に急遽サンプルスキルとして作っていた腹話術を提出…という経緯でした。

「無音無限ループ」がかなり裏技的なハックだったため、ストア公開は厳しいと思っていたので一種の賭けでしたが、なんとか審査を通過。
サンプルスキルを公開用にアレンジするのも結構大変でしたが、そのぶんかなり勉強にもなりました。

登壇とストア公開の話を書いていきます

さて、ざっと4月以降の話を書きましたが、それぞれ細かい部分で詳細にアウトプットしておきたいことがあります。

  • 登壇したそれぞれの発表内容に関する細かい解説的な話
  • 「テキスト腹話術」スキルのしくみ
  • スキルのストア公開時の注意することと「無音無限ループ」安全版
  • Clova Skill Awards 受賞と今後の展望

こんな内容を、これからいくつかの記事に分けて書いていこうかなと思っています。

今回はあまりまとまりのない雑多な投稿になってしまいましたが、上記について、なるべく間をあけずに書いていきたいなと思います。