GoogleAssistant
GoogleHome
actionsongoogle
スマートスピーカー
SSML

SSMLを利用してGoogle Home向けの百人一首で遊ぶアプリを作ってみた

この記事は、 BRIGHT VIE Advent Calendar 2017 - Qiita 20日目の記事になります。

はじめに

弊社では音声入力を活用したアプリケーションの開発に取り組んでいるのですが、
その際にSSML関連の調査をしているときにふと思いついた「百人一首を遊べるアプリ」をSSMLの勉強がてら作ってみることにしました。

なおGoogle Home向けアプリの開発、申請周りについては、
個人的に取り組んでいた下記にて知見をためることが出来たこともあるため、
今回はSSMLに特化して記載しようと思います。

Google Home向け子供の育児日記アプリを作ってみた〜申請とリジェクトから得られた知見〜

SSMLとは

Speech Synthesis Markup Languageの略で、XMLでテキストの読み上げ内容を詳細に定義できる音声合成のためのマークアップ言語です。
Actions on Googleだけでなく、Amazon Alexaも採用されており、音声合成のためのレスポンスをSSMLで定義することができます。

また、Watson日本語APIの「Text to Speech(音声合成)」でもSSMLに対応しているようですね。

どんなことが出来るの?

単純にテキストデータを再生するだけだと単調な読み上げになってしまい、意図が伝わりづらかったりしてしまいます。
そこで、SSMLを利用することで、「単語と単語の間の一時停止」や「テキストのピッチやスピーチレート、音量のカスタマイズ」、また「記録済みの音声データを再生する」などを行うことが可能となり、
より柔軟に音声合成による発音が可能となります。

詳細は、公式のドキュメントや下記参考記事を御覧ください。

SSMLを利用した百人一首読み上げアプリ

さて、このSSMLを勉強するにあたって何か作れないかなと思っていたところ、テレビで百人一首の話がしており、
「そういえば百人一首って正月親戚が集まってしていたときも誰か一人は読み手やったなぁ」、
「これ、スマートスピーカーがやってくれたら便利じゃね」と思ったので
勉強がてら早速作ってみました。

SSML確認ツール

まずは作成するにあたって、SSMLを設定して発音を確認できるようなサイトやツールはないかなと思っていたところ、
AWSが提供している文章を音声に変換する「Amazon Polly」を利用すれば簡単に試せると思いこちらを利用しました。

なおこの「Amazon Polly」、少し前に話題になったエフエム和歌山のAIアナウンサー「ナナコ」にも利用されており、
自動音声でニュースや天気予報を読み上げることに活用されているみたいですね。すごい。。。

なお、後から気がついたのですが、Action on GoogleのSimulatorの「Audio」の欄を利用すれば
SSMLを生成して再生の検証をしたり、作成した音声合成データをDLしたり出来ました。

Action on GoogleでSSMLを試してみる.png

1. テキストデータから音声を生成してみる

では早速、今季の朝ドラを見ている人ならピンとくる歌で。
「瀬を早み 岩にせかるる 滝川の われても末に 逢はむとぞ思ふ」

<speak>
 <prosody rate="65%"  pitch="medium">
   <p>
      <prosody volume="loud">瀬を</prosody>早み<break time="0.4s"/>
      岩にせかるる<break time="0.3s"/>
      滝川の
   </p>
   <break time="3.0s"/>
   <p>
      <prosody volume="loud">われても</prosody>末に<break time="0.2s"/>
      逢はんとぞ思ふ
   </p>
   <break time="2.0s"/>
   <p>
      <prosody volume="loud">われても</prosody>末に<break time="0.2s"/>
      逢はんとぞ思ふ
   </p>
 </prosody>
</speak>

ない知識を振り絞ってマークアップしてみました。

全体的にゆっくり読まれているため通常の65%程度のスピードで記載。
そして、上の句下の句ともに最初の文字は強めの発音と感じたので、
「<prosody volume="loud">」で音量を大きくすることで他よりも強めに表現。
それぞれの段落のまは、breakで現状は停止させているのですが、
百人一首独特の音を伸ばすみたいなことって流石にSSMLだと出来ないのかなぁ。。。

ちなみに作成したAmazon Pollyだとこんな感じ。

AWS Amazon Polly作成音声

そして実際にこれをGoogle Assistant上で発音させてみるとこんな感じです。

Amazon Pollyで作成した音声のほうがまだ聞き取りやすい感じはしますが、
実際の百人一首の読み上げに比べると全然ですね...(まぁそりゃそうか)

小倉百人一首読み上げサンプル

2. 音声ファイルを再生させてみる

やはり本物感を出すのであれば音声ファイルを再生するのが一番のようです。
下記のようにaudioタグを利用することで外部ファイルを再生することが出来ます。

<speak>
  <audio src=“音声ファイルのURL”>
    // 音声ファイルが再生されなかったときの処理をここに記載
  </audio>
</speak> 

※ srcに指定するURLは、HTTPSの必要があります。

手元の環境でやってみましたが、やはり先程の音声合成とは比べ物にならないくらい
百人一首っぽさが出ており、Google Homeから聞こえてくることでより本物感は感じられたなと思いました。

ただ、音声データの場合、著作権の扱いがどうなのかよくわからなかったので、
組み込みについては今回は全てテキストデータにSSMLでマークアップする形で実施してみました。

SSMLファイルの作成

百人一首の一覧についてはネット上でかなりまとめられているため、
そこから「通常の表記」と「カナ表記」のデータを取得し、下記のようなjson形式にまとめています。

poem_card_77.json
{
    "no": 77, 
    "ssml": "<prosody rate=\"80%\" pitch=\"medium\"><break time=\"1.5s\"/><p>せをはやみ<break time=\"0.4s\"/>いはにせかるる<break time=\"0.3s\"/>たきがはの</p><break time=\"3.0s\"/><p>われてもすゑに<break time=\"0.2s\"/>あはむとぞおもふ</p><break time=\"2.0s\"/><p>われてもすゑに<break time=\"0.2s\"/>あはむとぞおもふ</p></prosody>", 
    "text": "瀬をはやみ 岩にせかるゝ 滝川の われてもすゑに あはむとぞおもふ"
}

なお予め分かっていましたが、5句ほど微調整を終えたあたりでしんどくなってきたので、
今回肝になる部分のssmlですが、一括置換で、「5 7 5 7 7」のそれぞれの間に
「<p>」や「<break time="0.4s"/>」や「<break time="2.0s"/>」を入れることにしました。

これを100句分作成し、1ファイルにまとめて利用できるようにしました。

百人一首アプリのGoogle Assistantの設定

構築手順など特に難しいことはしていないのでさくっと記載しちゃいます。

Dialogflowの設定

簡単に設定だけ記載してみます。

インテントは3つ(Default Fallback Intentは除く)

  • 起動時のインテント
    • 「百人一首で遊ぶにつないで」「百人一首で遊ぶ」で起動
  • 歌を詠み上げるインテント
    • 「お願いします」と言うとランダムで1句読み始める
  • 終了のインテント
    • 「ありがとうございました」と言うと終了する

WebHook側の処理

こちらも単純にNodejsで記載。

工夫した点は、百人一首の既読管理はリクエストの中で送られてきているセッションIDを利用しているところくらいでしょうか。
このセッションIDは、会話が終了すると変更になるので、
会話が継続している間だけ同一ユーザ同一セッションからのリクエストであることが分かります。

app.post('/webhook', function(request, response, next){

  // アクセスログを出力
  console.log('Incoming post request...');
  console.log('Request headers: ' + JSON.stringify(request.headers));
  console.log('Request body: ' + JSON.stringify(request.body));

  const app = new DialogflowApp({request: request, response: response});


  let version, userId, action, parameters, session;

  // Action on Googleからのリクエストがv1なのかv2なのかをチェックする
  version = (request.body.originalDetectIntentRequest)? request.body.originalDetectIntentRequest.version : "1";
  console.log("version: " + version);


  userId = 'default';
  if (request.body.originalRequest && request.body.originalRequest.data && request.body.originalRequest.data.user) {
    userId = request.body.originalRequest.data.user.userId;
  }
  console.log('userId:' + userId);

  action = app.getIntent_();
  console.log('action: ' + action);

  parameters = request.body.result.parameters; 
  console.log(parameters);

  // このセッションIDを利用
  sessionId = (request.body.sessionId) ? request.body.sessionId : undefined;
  console.log(sessionId);

  // リクエスト情報をセットし、Actionに応じて処理を呼び出す
  const helper = getActionHelper(app, userId, sessionId, action, parameters);
  return helper.exec();
}

下記が実際に百人一首の抽選やレスポンスの定義を行う部分。
(プログラムで記載すると長くなるので日本語ベースで記載しています)

// 1句ランダムに百人一首の歌を詠み上げる処理

// 全ての句を読み終えておりカードがない場合。
if (全てのカードを読み終えたかどうか) {
  return this.app.tell('カードが無くなったので百人一首を終了します。ありがとうございました。');
}

// ...以下省略...
// 未読の歌の中から1句を選択する
// 選択した歌を既読リストに追加する
// 選択した歌のテキスト情報を取得する(先程のpoem_card_77.jsonの形式のもの)

// 画面があるかないかでレスポンス形式を変えることが望ましい
const screenOutput = this.app.hasSurfaceCapability(this.app.SurfaceCapabilities.SCREEN_OUTPUT);

// レスポンスの際には、ssml形式のnextSpeachと画面描画用のnextTextの両方の形式を準備する
let nextSpeach, nextSpeach;


// 未読リストが残り1件だけだった場合
// - この歌を読み上げるとゲームは終わりのため、変更する文言を
if (未読リストが残り1件だけだった) {

   if (screenOutput) {

      nextText =  + '\nカードが無くなりました。これにて終了です。\nお疲れ様でした。';

     // 1句画面に表示させた後に終了を宣言する
     var richResponse = this.app.buildRichResponse().addSimpleResponse(displayText + nextText);
     return this.app.tell(richResponse);

   } else {

      nextSpeach = '<break time="1.0s"/><prosody rate="100%" pitch="medium">カードが無くなりました。これにて終了です。<break time="1.3s"/>お疲れ様でした。</prosody>';
      // 1句読み上げた後に終了を宣言する
      return this.app.tell('<speak>' + poemCard.ssml + nextSpeach + '</speak>');
   }

} else {

   if (screenOutput) {

     nextText = '「お願いします」。と言うと、次の句を詠みます。';

      // 1句読み上げた後、次の句の読み上げ依頼を待つ
     var richResponse = this.app.buildRichResponse().addSimpleResponse(displayText + nextText);
     return this.app.tell(richResponse);

   } else {

      nextSpeach = '<break time="2.0s"/><prosody rate="100%" pitch="medium">「お願いします」。と言うと、次の句を詠みます。</prosody>';

      // 1句読み上げた後、次の句の読み上げ依頼を待つ
      return this.app.tell('<speak>' + poemCard.ssml + nextSpeach + '</speak>');
   }
}

実際にやってみると

テレビやアニメで見るような音声ではないので、ちょっと?(いや、かなり)違和感はありますが、
(実際に流れる音声は、こちらです。)
まぁ無事に?百人一首を読み上げてくれることは出来ているようです。

ただ、テープなどと違って読み終えた後にこちらから次を読み上げてほしいがために、
「お願いします」と一声言わなきゃいけないのがちょっとつらい部分ではある感じですね...

(ラズパイなどから喋らすことは出来ますが、Google Assistantを経由してサーバ側をトリガーに喋らすことが出来たら良いのになぁーWebSocket的なイメージで...)

まとめ

やってみて音声合成を人が発音する雰囲気に合わせるのはやっぱ難しいと感じましたが、
ピッチや発音のテンション、一時停止処理などスマートスピーカーでの対話を行う際の印象も大きく変わってくるので、
是非今後は取り入れていきたいなと思いました。

また、今回AmazonのAmazon Pollyで合成された音声は比較的自然体だなと感じ、
Googleの音声はなぜあんなにも機械的なんだろうというのは凄く感じました。

ただ、ssmlを利用して音声データを流すことが出来るので、今の状態だったら
実際にサービスに導入する際には予め録音した音声データを再生するような仕組みで組み込むかなぁ...