Custom Extensionを作成する

Custom Extensionとは、CLOVAが基本的に提供している機能やサービスではなく、開発者が任意に拡張した機能や、外部のサービスを提供するExtensionです。例えば、ウェブ検索やニュースクリッピングなどのサービスだけでなく、ユーザーアカウント認証の必要な音楽、ショッピング、金融などの外部サービスを提供することができます。Custom Extensionは、解析されたユーザーの発話情報をCEKから受け取り、その内容を処理して、サービスの処理結果を返す必要があります。

このドキュメントでは、Custom Extensionを作成する際の準備事項と、Custom ExtensionがCEKとどのようなメッセージのやり取りをして、どのように動作する必要があるかについて、次の内容を説明します。

  • 準備事項
  • Custom Extensionリクエストを処理する
    • LaunchRequestリクエストを処理する
    • IntentRequestリクエストを処理する
    • SessionEndedRequestリクエストを処理する
    • リクエストメッセージを検証する
  • Custom Extensionレスポンスを返す
  • マルチターン対話をする
  • オーディオコンテンツを提供する

準備事項

Custom Extensionの開発者は、次の項目を事前に準備する必要があります。

  • 対話モデル

    CLOVA Developer Centerに登録するCustom Extensionの対話モデルです。対話モデルは、ユーザーとCLOVAの対話シナリオのようなものです。ユーザーが発話しそうなフレーズを定義し、それぞれのフレーズがどんな意図を表し、どんな情報を持つかを設定します。

    対話モデルを定義する方法の詳細については、対話モデルの定義を参照してください。

  • Extensionサーバー

    CLOVA Developer Centerに登録するExtensionサーバーです。このサーバーは、CLOVAがユーザーの音声入力を解析した結果や、デフォルトで提供されるインテントを渡された際に、そのインテントを処理して適切な応答を返す必要があります。

  • 認可サーバー

    音楽、ショッピングなど、ユーザーアカウント認証の必要な外部のサービスを提供するCustom Extensionの場合、ユーザーアカウントを連携する必要があります。ユーザーアカウントを連携するには、必ず認可サーバーを構築する必要があります。詳細については、ユーザーアカウントを連携するを参照してください。

Custom Extensionリクエストを処理する

Custom ExtensionはCEKからCustom Extensionメッセージ形式のユーザーリクエストを受信します(HTTPSリクエスト)。Custom Extensionは通常、次のようにリクエストを処理し、レスポンスする必要があります。

このようなユーザーのリクエストは一度で終わるリクエストの場合もありますが、次のように文脈が維持される必要のあるマルチターン対話の場合もあります。

そのため、ユーザーのリクエストを3つのタイプに分類しています。Custom Extensionの開発者は、リクエストのタイプに応じて適切に処理する必要があります。 3つのリクエストタイプと、各リクエストタイプのユーザーの発話パターンは次のとおりです。

リクエストタイプ ユーザーの発話パターン サンプル発話
LaunchRequest [Extensionの呼び出し名] + 「を起動して/を開いて/に繋いで」 「ピザボットを起動して」
IntentRequest (LaunchRequestタイプのリクエストを受け付けた状態で) [Extensionごとに登録したコマンド] (ピザボット起動状態で)「注文を確認して」
SessionEndedRequest (LaunchRequestタイプのリクエストを受け付けた状態で)「終了して/終了/やめて」 「(ピザボットを)終了して」

メモ

EventRequestは、ユーザーの発話の有無に関わらず、デバイスの状態が変化したときにExtensionに送信されるメッセージです。これらのイベントは、デバイスの状態を取得したり、状態変化したことを検知することに利用できます。また、Extensionがオーディオコンテンツを提供する際にも使用されます。ここでは、オーディオコンテンツ再生時のEventRequestについては説明しません。

LaunchRequestの処理

LaunchRequestタイプのリクエストは、ユーザーが特定のExtensionを使用すると宣言したことを示す際に使用されます。例えば、ユーザーが「ピザボットを起動して」や「ピザボットを開いて」のように指示した場合、CEKはピザの宅配サービスを提供するExtensionにLaunchRequestタイプのリクエストを渡します。このタイプのリクエストを渡されたExtensionは、ユーザーの次のリクエストも受信できるように準備している必要があります。

LaunchRequestタイプのメッセージは、request.typeフィールドに"LaunchRequest"の値を持ち、requestフィールドにユーザーの発話の解析情報を含めていません。Extensionの開発者は、このメッセージを受信した場合、事前の準備事項を処理するか、ユーザーにサービスを提供する準備ができたというレスポンスメッセージを返します。

このメッセージを受信してからSessionEndedRequestタイプのリクエストメッセージを受信するまで、IntentRequestタイプのリクエストメッセージを受信し、session.sessionIdフィールドは前のメッセージと同じ値を持ちます。

次はLaunchRequestタイプのリクエストメッセージのサンプルです。

{
  "version": "1.0",
  "session": {
    "new": true,
    "sessionAttributes": {},
    "sessionId": "a29cfead-c5ba-474d-8745-6c1a6625f0c5",
    "user": {
      "userId": "U399a1e08a8d474521fc4bbd8c7b4148f",
      "accessToken": "XHapQasdfsdfFsdfasdflQQ7"
    }
  },
  "context": {
    "System": {
      "application": {
        "applicationId": "com.example.extension.pizzabot"
      },
      "user": {
        "userId": "U399a1e08a8d474521fc4bbd8c7b4148f",
        "accessToken": "XHapQasdfsdfFsdfasdflQQ7"
      },
      "device": {
        "deviceId": "096e6b27-1717-33e9-b0a7-510a48658a9b",
        "display": {
          "size": "l100",
          "orientation": "landscape",
          "dpi": 96,
          "contentLayer": {
            "width": 640,
            "height": 360
          }
        }
      }
    }
  },
  "request": {
    "type": "LaunchRequest"
  }
}

上記のサンプルで、各フィールドの意味は次のとおりです。

  • version:使用しているCustom Extensionメッセージフォーマットのバージョンです。現在のバージョンはv1.0です。
  • session:新しいセッションです。新しいセッションで使用されるセッションのIDとユーザーの情報(ID、アクセストークン)が含まれています。
  • context:クライアントデバイスの情報です。デバイスのIDとデフォルトユーザーの情報が含まれています。
  • request:LaunchRequestタイプのリクエストです。対象Extensionの使用を開始することを示します。ユーザーの発話の解析情報はありません。

IntentRequestの処理

IntentRequestタイプのリクエストは、あらかじめ定義した対話モデルに従って、CEKがExtensionにユーザー発話のリクエストを送信する際に使用されます。例えば、ユーザーが「ピザボットを起動して」と発話してサービスを開始した後に、「ピザを頼んで」と指示した時に、CEKはピザの宅配サービスを提供するExtensionにIntentRequestタイプのリクエストを渡します。

IntentRequestタイプのリクエストは、request.typeフィールドに"IntentRequest"の値を持ちます。呼び出されたインテントの名前と、解析されたユーザーの発話情報は、request.intentフィールドから確認できます。このフィールドを分析してユーザーのリクエストを処理してから、レスポンスメッセージを返します。

次はIntentRequestタイプのリクエストメッセージのサンプルです。

{
  "version": "1.0",
  "session": {
    "new": false,
    "sessionAttributes": {},
    "sessionId": "a29cfead-c5ba-474d-8745-6c1a6625f0c5",
    "user": {
      "userId": "U399a1e08a8d474521fc4bbd8c7b4148f",
      "accessToken": "XHapQasdfsdfFsdfasdflQQ7"
    }
  },
  "context": {
    "System": {
      "application": {
        "applicationId": "com.example.extension.pizzabot"
      },
      "user": {
        "userId": "U399a1e08a8d474521fc4bbd8c7b4148f",
        "accessToken": "XHapQasdfsdfFsdfasdflQQ7"
      },
      "device": {
        "deviceId": "096e6b27-1717-33e9-b0a7-510a48658a9b",
        "display": {
          "size": "l100",
          "orientation": "landscape",
          "dpi": 96,
          "contentLayer": {
            "width": 640,
            "height": 360
          }
        }
      }
    }
  },
  "request": {
    "type": "IntentRequest",
    "intent": {
      "name": "OrderPizza",
      "slots": {
        "pizzaType": {
          "name": "pizzaType",
          "value": "ペパロニ"
        }
      }
    }
  }
}

上記のサンプルで、各フィールドの意味は次のとおりです。

  • version:使用しているCustom Extensionメッセージフォーマットのバージョンです。現在のバージョンはv1.0です。
  • session: 既存のセッションに続くユーザーのリクエストです。既存セッションのIDとユーザーの情報(ID、アクセストークン)が含まれています。
  • context:クライアントデバイスの情報です。デバイスのIDとデフォルトユーザーの情報が含まれています。
  • request: IntentRequestタイプのリクエストです。"OrderPizza"という名前で登録されたインテントを呼び出しています。該当するインテントが必要とする情報として"pizzaType"というスロットが一緒に渡されます。そのスロットは"ペパロニ"という値を持っています。

メモ

IntentRequestタイプのリクエストは、LaunchRequestタイプのリクエストと関係なく、新しいセッションを開始してリクエストを処理することができます。

SessionEndedRequestの処理

SessionEndedRequestタイプのリクエストは、ユーザーが特定のモードやCustom Extensionの使用を中断すると宣言したことを示す際に使用されます。ユーザーが「終了して」「やめて」などのように指示した場合、クライアントはExtensionの使用を中断し、CEKは対話サービスを提供するExtensionにSessionEndedRequestタイプのリクエストを渡します。

SessionEndedRequestタイプのメッセージはrequest.typeフィールドに"SessionEndedRequest"の値を持ち、LaunchRequestタイプと同じくrequestフィールドにユーザーの発話の解析情報は含まれません。Extensionの開発者は、サービス終了時の後処理を実施してください。

次はSessionEndedRequestタイプのリクエストメッセージのサンプルです。

{
  "version": "1.0",
  "session": {
    "new": false,
    "sessionAttributes": {},
    "sessionId": "a29cfead-c5ba-474d-8745-6c1a6625f0c5",
    "user": {
      "userId": "U399a1e08a8d474521fc4bbd8c7b4148f",
      "accessToken": "XHapQasdfsdfFsdfasdflQQ7"
    }
  },
  "context": {
    "System": {
      "application": {
        "applicationId": "com.example.extension.pizzabot"
      },
      "user": {
        "userId": "U399a1e08a8d474521fc4bbd8c7b4148f",
        "accessToken": "XHapQasdfsdfFsdfasdflQQ7"
      },
      "device": {
        "deviceId": "096e6b27-1717-33e9-b0a7-510a48658a9b",
        "display": {
          "size": "l100",
          "orientation": "landscape",
          "dpi": 96,
          "contentLayer": {
            "width": 640,
            "height": 360
          }
        }
      }
    }
  },
  "request": {
    "type": "SessionEndedRequest"
  }
}

上記のサンプルで、各フィールドの意味は次のとおりです。

  • version:使用しているCustom Extensionメッセージフォーマットのバージョンです。現在のバージョンはv1.0です。
  • session: 既存のセッションに続くユーザーのリクエストです。既存セッションのIDとユーザーの情報(ID、アクセストークン)が含まれています。
  • context:クライアントデバイスの情報です。デバイスのIDとデフォルトユーザーの情報が含まれています。
  • request: SessionEndedRequestタイプのリクエストです。対象Extensionの使用を中断することを示します。ユーザーの発話の解析情報はありません。

注意

CEKがSessionEndedRequestタイプのリクエストをExtensionに送信した瞬間から、CEKはそのExtensionからの応答をすべて無視します。

リクエストメッセージを検証する

ExtensionがCEKからHTTPリクエストを受信するとき、そのリクエストが第三者ではなく、CLOVAから送信された信頼できるリクエストかどうかを検証する必要があります。HTTPヘッダーにあるSignatureCEKフィールドとRSA公開鍵を使用して、以下のようにリクエストメッセージを検証してください。

RSA公開鍵を用いてリクエストメッセージを検証する

  1. `context.System.application.applicationId`が設定済みの`ExtensionId`と同一であることを確認してください

  2. CLOVAの署名用RSA公開鍵を以下のURLからダウンロードしてください

    https://clova-cek-requests.line.me/.well-known/signature-public-key.pem

  3. SignatureCEKヘッダーの値を取得してください

    SignatureCEKヘッダーの値は、Base64エンコードされた、HTTP bodyのRSA PKCS #1 v1.5署名値です。

  4. ステップ1でダウンロードしたRSA公開鍵を用いてステップ2で取得したSignatureCEK ヘッダーを以下のように検証(verify)してください
String signatureStr = req.getHeader("SignatureCEK");
byte[] body = getBody(req);
String publicKeyStr = downloadPublicKey();
publicKeyStr = publicKeyStr.replaceAll("\\n", "")
    .replaceAll("-----BEGIN PUBLIC KEY-----", "")
    .replaceAll("-----END PUBLIC KEY-----", "");
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(pubKeySpec);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(pubKey);
sig.update(body);
byte[] signature = Base64.getDecoder().decode(signatureStr);
boolean valid = sig.verify(signature);

メモ

検証に失敗した場合にはそのリクエストは破棄してください。

注意

HTTP headerのフィールドは大文字/小文字を区別しないcase-insensitiveです。Section 4.2, "Message Headers"

実装時にはフィールド名の大文字/小文字問わず、値が取得できるようにしてください。

Custom Extensionレスポンスを返す

リクエストメッセージを処理すると、CEKにレスポンスメッセージを返す必要があります(HTTPレスポンス)。リクエストメッセージのタイプによって異なる内容を返すこともありますが、レスポンスメッセージの構造に大差はありません。以下は、LaunchRequestタイプのリクエスト(「ピザボットを起動して」というユーザーリクエスト)を処理した後に返したレスポンスメッセージです。

{
  "version": "1.0",
  "sessionAttributes": {},
  "response": {
    "outputSpeech": {
      "type": "SimpleSpeech",
      "values": {
          "type": "PlainText",
          "lang": "ja",
          "value": "こんにちは。ピザボットです。どういったご用件ですか"
      }
    },
    "card": {},
    "directives": [],
    "shouldEndSession": false
  }
}

各フィールドは、次のような意味を持ちます。

  • version:使用しているCustom Extensionメッセージフォーマットのバージョンです。現在のバージョンはv1.0です。
  • response.outputSpeech:ユーザーが日本語で「こんにちは。ピザボットです。どういったご用件ですか」の文章を話すように設定します。
  • response.card:クライアントの画面に表示するデータがありません。コンテンツテンプレート形式のデータで、クライアントの画面に表示するコンテンツをこのフィールドで渡すことができます。
  • response.shouldEndSession:セッションを終了せず、引き続きユーザーの入力を受け付けるかを管理します。このフィールドの値がtrueの場合、SessionEndedRequestリクエストを受け取る前に、Extensionからセッションを終了できます。

メモ

  • response.directivesフィールドはExtensionがCEKに渡すディレクティブです。response.directivesフィールドで使用するディレクティブは、現在、主にオーディオコンテンツを提供するために使用されます。
  • Extensionサーバーのレスポンスは8秒以内に返却するよう実装してください。時間内にCEKにレスポンスメッセージが渡されない場合は対話が終了します。対話の終了後に返却されたレスポンスは実行されません。

次のように、場合によって複数の文章を出力するようにレスポンスメッセージを作成することができます。あるいは、インターネット上のオーディオファイルを再生するようにレスポンスメッセージを作成することもできます。

{
  "version": "1.0",
  "sessionAttributes": {},
  "response": {
    "outputSpeech": {
      "type": "SpeechList",
      "values": [
        {
          "type": "PlainText",
          "lang": "ja",
          "value": "歌を歌ってみます。"
        },
        {
          "type": "URL",
          "lang": "" ,
          "value": "https://example.com/song.mp3"
        }
      ]
    },
    "card": {},
    "directives": [],
    "shouldEndSession": true
  }
}

各response.outputSpeechフィールドの説明は、次のとおりです。

  • response.outputSpeech.type:複文タイプ(SpeechList)の音声情報です。
  • response.outputSpeech.values[0]:テキスト形式の音声情報です。日本語で「歌を歌ってみます」と発話するように設定されています。
  • response.outputSpeech.values[1]:URL形式の音声情報です。valueフィールドに入力されたURLのファイルを再生するように設定されています。

メモ

単文や複文タイプの音声情報の他に、画面を持たないデバイスのような詳しい内容をGUIで表現できないクライアントデバイスのために、複合タイプ(SpeechSet)の音声情報もサポートしています。詳細については、Custom Extensionメッセージのレスポンスメッセージを参照してください。

音声出力だけでなく、クライアントデバイスの画面やクライアントアプリの画面にデータを出力する必要がある場合、次のようにresponse.cardフィールドにコンテンツテンプレートに合わせて表示するコンテンツを設定します。

{
  "version": "1.0",
  "sessionAttributes": {},
  "response": {
    "outputSpeech": {
      "type": "SimpleSpeech",
      "values": {
          "type": "PlainText",
          "lang": "ja",
          "value": "ブラウンの画像です"
      }
    },
    "card": {
      "type": "ImageText",
      "imageUrl": {
        "type": "url",
        "value": ""
      },
      "mainText": {
        "type": "string",
        "value": "LINE Friendsのブラウンです"
      },
      "referenceText": {
        "type": "string",
        "value": "CLOVA検索結果"
      },
      "referenceUrl": {
        "type": "url",
        "value": "DUMMY_REFERENCE_URL"
      },
      "subTextList": [
        {
          "type": "string",
          "value": "LINE Friends"
        }
      ],
      "thumbImageType": {
        "type": "string",
        "value": "キャラクター"
      },
      "thumbImageUrl": {
        "type": "url",
        "value": "DUMMY_IMAGE_URL"
      }
    },
    "directives": [],
    "shouldEndSession": true
  }
}

マルチターン対話をする

Custom Extensionがサービスを提供したり、または動作するために必要な情報が、CEKから渡されたユーザーのリクエスト(IntentRequest)にすべて含まれていないことがあります。また、シングルターンの対話では、1回の発話でユーザーのリクエストを受け付けることが難しい場合もあります。その場合、Custom Extensionはユーザーから足りない情報を聞き出すために、マルチターンの対話を行うことができます。

例えば、ユーザーが「ペパロニピザを頼んで」と発話したと仮定します。それを受け、CEKは次のようなリクエストメッセージを送信します。

{
  "version": "1.0",
  "session": {
    "new": false,
    "sessionAttributes": {},
    "sessionId": "a29cfead-c5ba-474d-8745-6c1a6625f0c5",
    "user": {
      "userId": "U399a1e08a8d474521fc4bbd8c7b4148f",
      "accessToken": "XHapQasdfsdfFsdfasdflQQ7"
    }
  },
  "context": {
    ...
  },
  "request": {
    "type": "IntentRequest",
    "intent": {
      "name": "OrderPizza",
      "slots": {
        "pizzaType": {
          "name": "pizzaType",
          "value": "ペパロニ"
        }
      }
    }
  }
}

Custom Extensionがピザの種類だけでなく、注文する数量に関する情報を必要とすることがあります。その際、レスポンスメッセージのresponse.shouldEndSessionフィールドをfalseに設定すると、マルチターン対話で足りない情報を確認することができます。また、ユーザーが先に入力した情報をsessionAttributesフィールドにキー(key)-値(value)の形で保存することもできます。

以下のようにレスポンスを返すことで、ユーザーがすでにリクエストしたintentフィールドとpizzaTypeの情報を保存するようにCLOVAにリクエストすることができます。また、ユーザーに数量に関する追加の情報を求めることができます。

{
  "version": "1.0",
  "sessionAttributes": {
    "intent": "OrderPizza",
    "pizzaType": "ペパロニ"
  },
  "response": {
    "outputSpeech": {
      "type": "SimpleSpeech",
      "values": {
          "type": "PlainText",
          "lang": "ja",
          "value": "何枚注文しますか?"
      }
    },
    "card": {},
    "directives": [],
    "shouldEndSession": false
  }
}

ユーザーの応答サンプルリクエストは次のようになります。マルチターン対話において追加で送信されたメッセージのsession.newはfalseになり、前のメッセージと同じsession.sessionId値を持ちます。 また、解析した結果と共に、前回のレスポンスに含まれていたsessionAttributesオブジェクトの情報をリクエストメッセージのsession.sessionAttributesフィールドに含めて再送信します。Custom Extensionはこれらの受信した追加の情報を使用して次の動作を行うことができます。

{
  "version": "1.0",
  "session": {
    "new": false,
    "sessionAttributes": {
        "intent": "OrderPizza",
        "pizzaType": "ペパロニ"
    },
    "sessionId": "a29cfead-c5ba-474d-8745-6c1a6625f0c5",
    "user": {
      "userId": "U399a1e08a8d474521fc4bbd8c7b4148f",
      "accessToken": "XHapQasdfsdfFsdfasdflQQ7"
    }
  },
  "context": {
    ...
  },
  "request": {
    "type": "IntentRequest",
    "intent": {
      "name": "AddInfo",
      "slots": {
        "pizzaAmount": {
          "name": "pizzaAmount",
          "value": "2"
        }
      }
    }
  }
}

注意

ExtensionがSessionEndedRequestタイプのリクエストを受信すると、マルチターンの対話は終了します。SessionEndedRequestタイプのリクエストを受信してからは、Extensionからどんな応答(使用終了のあいさつなど)が返されても、CEKで無視されます。

注意

cardは、Custom Extensionには対応しておりません。

オーディオコンテンツを提供する

Custom Extensionで、ユーザーに音楽やポッドキャストなどのオーディオコンテンツを提供することができます。そのためには、Custom ExtensionメッセージのEventRequestタイプのリクエストメッセージとレスポンスメッセージの仕様のうち、オーディオコンテンツ再生関連のCIC APIを使用する必要があります。ユーザーにオーディオコンテンツを提供するには、次の内容をExtensionに実装する必要があります。

  • 必須

    • オーディオコンテンツの再生を指示する
    • オーディオコンテンツの再生をコントロールする
  • 任意

    • 再生状態の変更および進行状況のレポートを収集する
    • セキュリティのためにオーディオコンテンツのURLを更新する

メモ

音声出力タイプ、オーディオコンテンツの再生タイプ共に、音源(.mp3)のリソースはHTTPSのみ許可されます。

オーディオコンテンツの再生を指示する

ユーザーから音楽や、または音楽のような形でオーディオコンテンツの再生をリクエストされたとき、そのオーディオコンテンツの情報を渡す必要があります。ユーザーからのオーディオコンテンツ再生のリクエストがIntentRequestタイプのリクエストでCustom Extensionに渡され、Custom ExtensionはそのIntentRequestタイプのリクエストメッセージに対するレスポンスメッセージを返す必要があります。そのとき、そのメッセージにクライアントがオーディオコンテンツを再生するように指示するAudioPlayer.Playディレクティブを含めます。

注意

オーディオコンテンツを提供するCustom Extensionでは、コンテンツをコントロールする処理は必須の実装項目です。

以下は、AudioPlayer.PlayディレクティブをCustom Extensionのレスポンスメッセージに含めたサンプルです。

{
  "version": "1.0",
  "sessionAttributes": {},
  "response": {
    "card": {},
    "directives": [
      {
        "header": {
          "namespace": "AudioPlayer",
          "name": "Play"
        },
        "payload": {
          "audioItem": {
            "audioItemId": "90b77646-93ab-444f-acd9-60f9f278ca38",
            "episodeId": 22346122,
            "stream": {
              "beginAtInMilliseconds": 0,
              "episodeId": 22346122,
              "playType": "NONE",
              "podcastId": 12548,
              "progressReport": {
                "progressReportDelayInMilliseconds": null,
                "progressReportIntervalInMilliseconds": 60000,
                "progressReportPositionInMilliseconds": null
              },
              "url": "https://streaming.example.com/1212334548/2231122",
              "urlPlayable": true
            },
            "type": "podcast"
          },
          "source": {
            "name": "Potbbang",
            "logoUrl": "https://example.com/logo_180125.png"
          },
          "playBehavior": "REPLACE_ALL"
        }
      }
    ],
    "outputSpeech": {},
    "shouldEndSession": true
  }
}

メモ

音楽を再生するレスポンスメッセージには、response.outputSpeechフィールドを追加することもできます。例えば、ユーザーに対して、「リクエストしたオーディオコンテンツを再生します」という音声出力(TTS)を再生し、その後にオーディオコンテンツの再生を開始することができます。

注意

cardは、Custom Extensionには対応しておりません。

オーディオコンテンツの再生をコントロールする

クライアントがオーディオを再生しているときに、ユーザーが「前」「次」などのように再生のコントロールに関連する発話を発した場合、ユーザーのリクエストはIntentRequestタイプのリクエストメッセージでCustom Extensionに渡されます。現在、CEKはCustom Extensionで再生のコントロールに関連するユーザーのインテントを、以下のようなビルトインインテントとして渡すようになっています。

  • Clova.NextIntent
  • Clova.PreviousIntent

ユーザーが「前」「次」に該当する発話をして、Clova.NextIntentまたはClova.PreviousIntentビルトインインテントをIntentReqeustタイプのリクエストメッセージで受け取ると、レスポンスメッセージでユーザーが前に聞いた、または次に聞くはずのオーディオコンテンツを再生するように指示する(AudioPlayer.Play)必要があります。

メモ

前または次に該当するオーディオコンテンツがなかったり、有効ではない場合、「再生する前または次の曲がありません」などの音声出力をレスポンスメッセージで返す必要があります。

再生状態の変更および進行状況のレポートを収集する

AudioPlayer.Playディレクティブでオーディオを再生するクライアントは、再生が開始、一時停止、再開、終了するタイミングで、AudioPlayer.PlayStarted、AudioPlayer.PlayPaused、AudioPlayer.PlayResumed、AudioPlayer.PlayStopped、AudioPlayer.PlayFinishedのようなイベントをCLOVAに送信します。そのとき、CLOVAはそのイベントの内容をEventRequestタイプのリクエストメッセージでCustom Extensionに送信します。

また、クライアントはオーディオコンテンツを再生するように指示(AudioPlayer.Play)を受けた後、AudioPlayer.PlayディレクティブのprogressReportフィールドに定義されている設定に従って再生の進行状況をレポートします。その内容もまた、EventRequestタイプのリクエストメッセージでCustom Extensionに送信されます。クライアントは、進行状況をレポートするために、以下のイベントを送信します。

  • AudioPlayer.ProgressReportDelayPassedイベント:再生が開始してから特定の時間が経過した後、再生の進行状況をレポートする
  • AudioPlayer.ProgressReportPositionPassedイベント:オーディオコンテンツの特定の位置(オフセット)を再生するときに、進行状況をレポートする
  • AudioPlayer.ProgressReportIntervalPassedイベント:再生中の場合、特定の間隔で繰り返し進行状況をレポートする

以下は、EventRequestタイプのリクエストメッセージで送信されたレポートのサンプルです。


{
  "context": {
    "AudioPlayer": {
      "offsetInMilliseconds": 60000,
      "playerActivity": "STOPPED",
      "stream": {
        "token": "TR-NM-17413540",
        "url": "https://music.serviceprovider.net/content?id=17413540",
        "urlPlayable": true
      },
      "totalInmillisecodns": 300000
    },
    "System": "{ ...}"
  },
  "request": {
    "type": "EventRequest",
    "requestId": "e5464288-50ff-4e99-928d-4a301e083d41",
    "timestamp": "2017-09-05T05:41:21Z",
    "event": {
      "namespace": "AudioPlayer",
      "name": "PlayStopped",
      "payload": {}
    }
  },
  "session": {
    "new": true,
    "sessionAttributes": {},
    "sessionId": "69b20cc1-9166-41f3-a2dd-85b70f8e0bf5"
  },
  "version": "1.0"
}

上記のEventRequestタイプのリクエストメッセージは、クライアントが全部で5分のオーディオコンテンツで、1分になるタイミングで再生を中断したことをレポートしています。Custom Extensionは、このようにして、クライアントの再生状態の変化を追跡することができます。例えば、AudioPlayer.PlayStoppedとAudioPlayer.PlayFinishedイベントが含まれたEventRequestタイプのリクエストメッセージを収集して、オーディオを最後まで聞いたり、または最後まで聞かないユーザーを区別し、それを統計データにすることができます。

また、AudioPlayer.ProgressReportIntervalPassedイベントが含まれたEventRequestタイプのリクエストメッセージを使って、おおよそユーザーがオーディオコンテンツをどの位置まで聞いたかを把握することができます。ユーザーが次に同じオーディオコンテンツの再生をリクエストする場合、そのデータに基づいて、最後に聞いた位置から再生することができます。

注意

再生の進行状況のレポートに関するEventRequestタイプのリクエストメッセージのうち、AudioPlayer.PlayFinishedイベントが含まれたメッセージを受け取った場合、Custom Extensionは、再生完了に対するクライアントの次のアクションをレスポンスメッセージで返す必要があります。そのアクションとして、次のオーディオコンテンツの再生を指示することもできますし、再生停止などの再生コントロールを指示することもできます。

ちなみに、このセクションで扱っているAudioPlayer名前欄イベントには、AudioPlayer.PlaybackStateコンテキストが含まれます。その情報もまた、EventRequestタイプのリクエストメッセージが送信されるときに含まれるので、Custom Extensionは含まれたAudioPlayer.PlaybackStateコンテキストからオーディオコンテンツのID、再生状態、オーディオコンテンツの再生位置などを把握できます。

以下は、AudioPlayer.PlaybackStateコンテキストが送信されたサンプルです。

{
  "offsetInMilliseconds": 5077,
  "playerActivity": "PLAYING",
  "stream": {
    "token": "TR-NM-17413540",
    "url": "https://music.serviceprovider.net/content?id=17413540",
    "urlPlayable": true
  },
  "totalInMilliseconds": 195265
}

セキュリティのためにオーディオコンテンツのURLを更新する

Custom Extensionがクライアントにオーディオコンテンツの再生を指示するとき、レスポンスメッセージにAudioPlayer.Playディレクティブを含める必要があります。そのとき、AudioPlayer.PlayディレクティブのaudioItem.stream.urlフィールドにオーディオコンテンツを再生できるURLを設定して送信します。

ただし、サービスの提供元によっては、セキュリティ上の問題により、永久に有効なURLを含めることができないことがあります。例えば、そのURLがさらされた場合、コンテンツを盗み取るための攻撃が発生する可能性がある場合などが考えられます。そのため、大抵の場合、比較的短い有効期限を持つインスタンスURLを使用します。また、クライアントがAudioPlayer.Playディレクティブを受信していても、より優先順位の高いタスクや、先に開始したタスク、またはネットワークの状況によって、オーディオコンテンツの再生開始が遅延することがあります。その場合、URLの有効期限が切れ、オーディオコンテンツを正常に再生できない可能性があります。

そのため、CLOVAはクライアントがオーディオコンテンツを再生できるURLを、再生の直前に取得する方法を提供しています。最初に、以下のようにAudioPlayer.PlayディレクティブのurlPlayableフィールドをfalseに指定し、urlフィールドにURLではない、他の形式の値を設定します。

{
  "audioItem": {
    "audioItemId": "9CPWU-c82302b2-ea29-4f6c-ba6e-20fd268d8c3b-c1570067",
    "title": "The theme song for LINE Friends",
    "artist": "Unknown",
    "stream": {
      "beginAtInMilliseconds": 0,
      "progressReport": {
        "progressReportDelayInMilliseconds": null,
        "progressReportIntervalInMilliseconds": null,
        "progressReportPositionInMilliseconds": 60000
      },
      "token": "TR-NM-17413540",
      "url": "clova:TR-NM-17413540",
      "urlPlayable": false
    }
  },
  "playBehavior": "REPLACE_ALL"
}

後にクライアントがAudioPlayer.Playディレクティブを処理するとき、urlPlayableフィールドがfalseに指定されていると、有効なオーディオコンテンツのURLを取得するためにAudioPlayer.StreamRequestedイベントをCLOVAに送信します。そのとき、イベントの内容はEventRequestタイプのリクエストメッセージで、以下のように送信されます。

{
  "context": {
    ...
  },
  "request": {
    "type": "EventRequest",
    "requestId": "e5464288-50ff-4e99-928d-4a301e083d41",
    "timestamp": "2017-09-05T05:41:21Z",
    "event": {
      "namespace": "AudioPlayer",
      "name": "StreamRequested",
      "payload": {
        "audioItemId": "9CPWU-c82302b2-ea29-4f6c-ba6e-20fd268d8c3b-c1570067",
        "title": "The theme song for LINE Friends",
        "artist": "Unknown",
        "audioStream": {
          "beginAtInMilliseconds": 0,
          "progressReport": {
            "progressReportDelayInMilliseconds": null,
            "progressReportIntervalInMilliseconds": null,
            "progressReportPositionInMilliseconds": 60000
          },
          "token": "TR-NM-17413540",
          "url": "clova:TR-NM-17413540",
          "urlPlayable": false
        }
      }
    }
  },
  "session": {
    "new": true,
    "sessionAttributes": {},
    "sessionId": "69b20cc1-9166-41f3-a2dd-85b70f8e0bf5"
  },
  "version": "1.0"
}

Custom Extensionは、そのタイミングで、再生できるオーディオコンテンツのURLをレスポンスメッセージで返す必要があります。そのために、AudioPlayer.StreamDeliverディレクティブをレスポンスメッセージに含める必要があります。クライアントは、以下のようなAudioPlayer.StreamDeliverディレクティブのボディを用いて、AudioPlayer.Playディレクティブを引き続き処理することができます。

{
  "version": "1.0",
  "sessionAttributes": {},
  "response": {
    "card": {},
    "directives": [
      {
        "header": {
          "namespace": "AudioPlayer",
          "name": "StreamDeliver"
        },
        "payload": {
          "audioItemId": "5313c879-25bb-461c-93fc-f85d95edf2a0",
          "audioStream": {
            "token": "b767313e-6790-4c28-ac18-5d9f8e432248",
            "url": "https://sample.musicservice.net/b767313e.mp3"
          }
        }
      }
    ],
    "outputSpeech": {},
    "shouldEndSession": true
  }
}

注意

cardは、Custom Extensionには対応しておりません。