音声合成(TTS)で LINE Clova に世界のことばをしゃべらせてみよう

本記事は　[IBM Cloud Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ibmcloud) 12/20の担当分です。内容的には [LINEBot&Clova Advent Calendar 2018](https://qiita.com/advent-calendar/2018/linebot) にもエントリしたかった。

## はじめに

昨年の [IBM Cloud Advent Calendar 2017](http://qiita.com/advent-calendar/2017/bluemix) に投稿した[記事](https://qiita.com/midheartty/items/895b43b76b9db0b2ed15
)が、その後プレゼント企画で [LINE賞](https://blog.qiita.com/171043969974-2/) に選出されて、副賞のスマートスピーカー LINE Clova をいただきました。

今回は、その　LINE Clova (以下単に Clova とします）を使って、 IBM Cloud 上で Watson　の機能を利用して日本語や英語以外の言語をしゃべらせてみよう、という内容です。

## おおまかなフロー

Clova の開発キット ([CEK](https://clova-developers.line.biz/cek/)) では、ユーザーの発話が解析されて「インテント」としてデータ化されたあとにバックエンドのサービスに伝わります。（本記事公開時点では発話された内容そのものはバックエンドには送信されません）

したがって、今回は「ユーザが自由に発話した内容をいろんな言語に翻訳して音声合成する」という仕組みはあきらめます。その代わりに、複数の例文を用意して、例文の番号と翻訳したい言語を Clova に伝えると、その言語で翻訳された音声が Clova から流れてくる、という仕組みにしたいと思います。

#### 会話例

ユーザー 「Clova、例文翻訳を起動して」 （スキル名：例文翻訳）
Clova 「例文翻訳をします。例文の番号と翻訳したい言語をお話しください」（プロンプト）
ユーザー 「6番の例文を英語に翻訳して」
Clova 「翻訳結果です。」
Clova 「"Tomorrow's weather appears to be rainy."」
ユーザー 「4番の例文をフランス語に翻訳して」
Clova 「翻訳結果です。」
Clova 「"Bienvenue chez vous, monsieur."」
ユーザー 「終了」


#### 【注意点】

今回の内容は、実際に公開するための審査を経たものではありません。本記事を参考にスキルを開発して審査に通らなくても、問い合わせ等には応じられませんので、ご了承ください。

## 必要なもの

###  アカウントなど

- [IBM Cloud](https://console.bluemix.net/) アカウント (今回の例は無料の Lite アカウントでも構築できます)
- LINE アカウント ([Clova Development Center](https://clova-developers.line.biz/) へログインしてスキル作成に使用します)
- LINE Clova スピーカー (エミュレーターはないようです)

### IBM Cloud で使用するリソース

| リソース名(リンク)      | プラン | 使用方法 |
|:-----------------|:-------------|:-----------------|
| [Node.js Web App with Express.js](https://console.bluemix.net/developer/appservice/starter-kits/nodejs-web-app-with-expressjs) | (256MB) | サーバー本体のためのスターターキット。 SDK for Node.js でもOK |
| [Cloudant](https://console.bluemix.net/catalog/services/cloudant) | Lite | 生成した音声データを保存する場所 |
| [Text to Speech](https://console.bluemix.net/catalog/services/text-to-speech) | Lite | 音声合成のための Watson サービス |
| [Language Translator](https://console.bluemix.net/catalog/services/language-translator) | Lite | テキストを翻訳するための Watson サービス |


## Clova スキルの設定

[Clova Development Center](https://clova-developers.line.biz/) へ LINE アカウントでログインします。

スキルを新規作成します。今回の例ではスキル名を「例文翻訳」とします。サーバー設定の ExntetionサーバーのURL に作成する Node.js アプリケーション上のエンドポイントを指定します。例えば、アプリ名は `reibunhonyaku` 、エンドポイントが `/callback` であればURLは次のようになります。

```
https://reibunhonyaku.mybluemix.net/callback
```

### 対話モデルの作成

今回は `TranslationIntent` というカスタムインテントと、`CLOVA.NUMBER` というビルトインスロットタイプおよび `LanguageName` というカスタムスロットタイプを作成します。

`CLOVA.NUMBER` は 「X番の例文」の X を拾うために使用します。

`LanguageName` は翻訳したい言語を認識するのに使用します。音声合成に対応している「イタリア語」「ポルトガル語」「日本語」「ドイツ語」「スペイン語」「フランス語」「英語」を辞書として追加します。

カスタムインテント `TranslationIntent` には、サンプル発話として「5番の例文をフランス語に翻訳して」を追加し、以下のように設定します。「5」と「フランス語」をマウス等で選択すると設定できます。

| スロット名      | スロットタイプ | スロット値 |
|:-----------------|:----------|:-----------------|
| example_index | CLOVA.NUMBER | 5 |
| target_language | LanguageName | フランス語 |

保存したあと、ビルドします。

## IBM Cloud と Watson のコーディングのポイント

使用した npm モジュールは以下の3つになります。コード内での初期化方法などについては、それぞれのドキュメントを参照ください。

- Clova CEK (`@line/clova-cek-sdk-nodejs`)
- nano for Cloudant (`nano`)
- Watson SDK (`watson-developer-cloud`)

CEK for Node.js の  [Example](https://github.com/line/clova-cek-sdk-nodejs#example) を参考に、エンドポイントで受け取ったリクエストの処理コードを書きます。また、[こちらの記事](https://qiita.com/miso_develop/items/6b256c1e8757c0ace4a9)も参考になりました。

今回はカスタムインテント `TranslationIntent` を追加したので、`onIntentRequest` の switch ブロックに追加するコード例を示します。

```javascript:callback.js
      case 'TranslationIntent':
        var slots = responseHelper.requestObject.request.intent.slots;
        var source_text = "きょうの東京の天気はどうですか？"; // デフォルトの例文
        var target_language = "en";
        var source_language = "ja";
        if (slots!=null) {
          // example_index スロットから値を取得
          if (slots.example_index) {
            console.log(slots.example_index.value);
            if (slots.example_index.value<=exampleSourceList.length) {
              var example = exampleSourceList[slots.example_index.value-1];
              source_language = example.language;
              source_text = example.text;  
            }
          }
          // target_language スロットから値を取得
          if (slots.target_language) {
            target_language = langId[slots.target_language.value];
          }
        }
        console.log("source_text="+source_text);
        console.log("source_language="+source_language);
        console.log("target_language="+target_language);

        // まず英語に翻訳
        const english_text = await doTranslation(source_text, source_language, "en");
        // 次に英語から対象言語に翻訳
        const target_text = await doTranslation(english_text, "en", target_language);
        // 音声合成を実行して Cloudnt 上の URL を取得
        const voice_url = await doSynthesize(source_text, target_text, target_language);
        console.log(voice_url);

        // Clova から出力するメッセージ
        const res0 = clova.SpeechBuilder.createSpeechText("翻訳結果です。","ja");
        const res1 = clova.SpeechBuilder.createSpeechUrl(voice_url);
        responseHelper.setSpeechList([res0,res1]);
        break;
```

今回は例文は以下のようなリストで持っています。Cloudant があるので、別途データベースをつくって、そこからスキル起動時`onLaunchRequest` にロードする形にすると例文が柔軟に追加できると思います。

```javascript:exampleSourceList
const exampleSourceList = [
{ language: "ja",  text: "百聞は一見にしかず。"
},
{ language: "ja",  text: "あけましておめでとうございます。"
},
{ language: "ja",  text: "千里の道も一歩から。"
},
{ language: "ja",  text: "ようこそおいでくださいました。"
},
{ language: "ja",  text: "きょうの調子はいかがですか？"
},
{ language: "ja",  text: "明日の天気は雨のようです。"
},
{ language: "ja",  text: "カバンをお持ちしましょうか？"
}
];
```

`doTranslation` は以下のような Language Translator サービスを呼び出すコードです。`srclang` と `targetlang` が同一の場合は、翻訳せずに `text` をそのまま返しています。

```javascript:doTranslation
function doTranslation(text, srclang, targetlang) {
  return new Promise(function (resolve, reject) {
    if (srclang !== targetlang) {
      var param = {
        text: text,
        model_id: srclang + "-" + targetlang
      };
      translator.translate(param, function(err, response) {
        if (err) reject(err);
        resolve(response.translations[0].translation);
      });    
    } else {
      resolve(text);
    }
  });
}
```

`doSynthesize` は　音声合成を実行して、Cloudant に音声のバイナリデータ(MP3)を保存しつつ、その URL を返します。Cloudant ドキュメントの Attachment として音声データを追記するので、通常の `insert` 処理と `insertAsStream` 処理の2回に分けています。ここで stream として受け口を準備できるので、 Text-to-Speech サービスのAPI `synthesize` の `pipe` の引数に指定することができます。

また、Clova から音声データのMP3ファイルにインターネット経由でアクセス可能にするために、今回は Cloudant の Attachment の URL (`voice_url`) を使用しますが、その中に Cloudant の　`username` と `password` を含めたURL (`cloudant_url`) を指定する必要があります。管理者用のアカウントとは別に　Clova からアクセスするためのアカウントを別途作成したほうがよいかもしれません。

```javascript:doSynthesize
const dbName = "reibunhonyaku";
const mydb = nano.db.use(dbName);

function doSynthesize(srctext, targettext, lang) {
  return new Promise(function (resolve, reject) {
    // Cloudant に保存する通常のデータ
    var docdata = {
      language: lang,
      source_text: srctext,
      target_text: targettext
    };

    const synthesizeParams = {
      text: targettext,
      accept: 'audio/mp3',
      voice: voiceActor[lang]
    };        

    mydb.insert(docdata, async function(err,body){
      if (!err) {
        // Cloudant に　attachment として追記するためのデータ
        var docid = body.id;
        var docrev  = body.rev;
        var atttime = new Date().getTime();
        var attname = "voice_"+ atttime +".mp3";
        var atttype = "audio/mp3";
        const attis = mydb.attachment.insertAsStream(docid, attname, null, atttype,{ rev: docrev });
        attis.on('end', () => {
          var voice_url = cloudant_url+"/"+dbName+"/"+docid+"/"+attname;
          resolve(voice_url);
        });
        const syn = textToSpeech.synthesize(synthesizeParams);
        syn.on('error', (error) => {
          console.log(error);
        });
        syn.pipe(attis);
      }
    });
  });
}
```

コードの中に出てくる `langId` と `voiceActor` についても、それぞれ以下のように対応づけをハードコードしています。

```javascript:langId
const langId = {
  "英語": "en",
  "スペイン語": "es",
  "ドイツ語": "de",
  "フランス語": "fr",
  "イタリア語": "it",
  "日本語": "ja",
  "ポルトガル語": "pt"
};
```

```javascript:voiceActor
const voiceActor = {
  en: "en-US_AllisonVoice",
  es: "es-ES_LauraVoice",
  de: "de-DE_DieterVoice",
  fr: "fr-FR_ReneeVoice",
  it: "it-IT_FrancescaVoice",
  ja: "ja-JP_EmiVoice",
  pt: "pt-BR_IsabelaVoice"
};
```


## おわりに

本記事では、音声合成の出力先をクラウド上のデータベースに格納してその URL を Clova に伝えることにより、Clova がサポートしている言語以外の音声を出力する例を示しました。

現状では、ユーザーの自由な発話の全文を捕捉できるわけではありませんが、例文を完全な固定文ではなくスロットを追加して部分的に入力を受け付けるようにするなど、工夫の余地はまだまだあるように思います。

今回の例は　Clova に限らず、いろいろなスマートスピーカーにも応用できる形になっていると思いますので、お好きな音声合成サービス（ストリーム出力が望ましい）と組み合わせて、ちょっとしたサービスを作ることができますね。