🗣️

mikanのmy単語帳の音声は、どのように作られているか

2022/12/05に公開約5,200字

はじめに

株式会社mikan バックエンドエンジニアの@hytkgamiです。
mikan Advent Calendar 2022 5日目の記事を担当します。

4日目は @gumioji による Androidエンジニアとしてmikanに入社し、あっという間に2年が過ぎちゃいました。 でした。
彼がmikanに入社してから2年間の、mikan Androidの改善の歴史がまとめられています。興味があれば、ぜひそちらもご覧ください!

この記事では、mikanアプリのmy単語帳で利用している音声がどのように作られているかについて、振り返りを兼ねて設計観点で紐解いていきます。

mikanとは

英語アプリmikanは累計650万ダウンロードを超えた、たのしく英語を学ぶことができる英語アプリです。
iOS版とAndroid版で若干の差異はありますが、継続的に対応教材数を拡大[1]しています。
また、リスニングに適したAudio Playerや、書籍のPDFをアプリで確認できるBook Readerなど、英語の学習体験を豊かにする機能も続々とリリースしています。

my単語帳とは

そんなmikanアプリの機能の一つに、my単語帳があります。
my単語帳とは、教材に掲載されている単語を登録して、まとめることができる機能です。
単語を自分で直接入力することも可能です。

my単語帳に登録した単語は、4択やカードめくりといった学習ができる他、発音を確認することもできます。

音声合成には、iOSではAVSpeechSynthesizer、AndroidではTextToSpeechを利用しています。
これらのAPIで音声を作ったとき、単語によっては聞き取りにくい音声になることがあり、音声の品質改善が課題でした。

以降は、この課題に対応するために構築したシステムについて触れていきます。

全体構成

現在運用している、my単語帳の音声生成システムの全体構成はこちらです。
mikanの単語帳データはFirestoreを利用しているため、Cloud Functions for Firebaseを利用した設計にしました。

各ステップを1つずつ解説していきます。

1. my単語帳への登録or更新をトリガーに関数を実行

図の①にあたる部分です。

my単語帳の単語データはFirestoreの/users/:user_id/generated_books/:book_id/generated_chapters/:chapter_id/generated_words/というコレクションに保存されているため、そのパスに対するイベントを拾うことで関数を呼び出します。

これにはCloud Functions for FirebaseのonCreateonUpdateトリガー[2]を利用しています。

2. キャッシュデータの確認

図の②にあたる部分です。

キャッシュ用のコレクションgenerated_word_audio_cachesを作っており、ドキュメントのモデルは以下のようになっています。(TypeScriptの例です)

type PartOfSpeech = 'noun' | 'adjective' | 'adverb' | 'verb' | 'idiom' | 'preposition' | 'conjunction';
type FirestoreWordAudioCache = {
  document_id: string;
  english_text: string;
  part_of_speech: PartOfSpeech;
  audio_filename: string;
  created_at: Date;
  updated_at: Date;
};

英単語を示すenglish_textと品詞を示すpart_of_speechを元にクエリを実行し、データの有無を確認します。
このときデータが見つかれば、そのデータのaudio_filenameを元にURLを生成し、ユーザのmy単語帳データのaudio_urlに値をセットします。(図の⑥)

3. 音声生成のリクエスト

図の③にあたる部分です。

音声の生成にはAmazon Pollyを利用しています。Amazon Pollyを採用する前にGoogle CloudのText-to-Speechも候補に上がっていましたが、Text-to-SpeechはSSMLタグで品詞の指定ができなかった[3]ため、採用を見送りました[4]

②でキャッシュが見つからなかった場合、Amazon Pollyにリクエストを行います。

APIはSynthesizeSpeechを利用しています。 似たAPIにStartSpeechSynthesisTaskがあり、こちらは非同期に音声合成を行った上でデータをS3バケットに保存してくれるリッチな機能ですが、mikanではGoogle Cloud Storageを利用しているため、前者を採用しました。

APIにはそれぞれ制限があります[5]が、SynthesizeSpeech APIを利用していても今のところmikanでは問題になっていません。

4. 音声データの保存

図の④にあたる部分です。

③からは音声ストリームが返ってくるため、Cloud Storageのストリーミング アップロードを利用しています。

5. キャッシュの作成

図の⑤にあたる部分です。

一度音声データを作成してCloud Storageに保存したら、以降同じ品詞と英単語の組み合わせでキャッシュが利用できるように、generated_word_audio_cachesにデータを書き込みます。

6. ユーザのmy単語帳の音声URLを更新

図の⑥にあたる部分です。

音声データのaudio_filenameを元にURLを生成し、ユーザのmy単語帳データのaudio_urlに値をセットします。
アプリはaudio_urlを元に音声データを取得し、アプリ内で音声を再生します。

本システムの設計当初、audio_urlへの書き込みではなく、キャッシュへのリファレンスを保持することでキャッシュの更新を容易にできないかと考えました。
しかし、その場合はFirestoreのドキュメントに新たにフィールドを追加することになります。

そうなると、当然ながらアプリをアップデートしないユーザには届けられない機能となってしまいます。

その時点で既にmy単語帳は多くのユーザに使われていたため後方互換性を優先し、既に存在するaudio_urlを利用する方法を採用しました。

課題

1. 一度キャッシュされたデータを差し替えにくい

作成した音声データを差し替えたい場合、Cloud Storage上のファイルを直接更新するしかありません。

前述したようにgenerated_word_audio_cachesへの参照を持てば、キャッシュデータの更新に伴って再生される音声も変更されるようになりますが、現在の設計では手動対応が必要となってしまいます。

また、Cloud StorageのオブジェクトにもCache-Controlでキャッシュが設定されているため、同じファイル名で音声を差し替えてもすぐには反映されない場合があります。

2. 不正なキャッシュデータが残ってしまうことがある

ほとんど発生していませんが、Cloud Storageには音声ファイルが存在しないがgenerated_word_audio_cachesにはデータが存在するケースで起こります。

原因としては、システムのバグやAPIのエラー、本システムとは別の経路でCloud Storageからファイルを削除した場合などが考えられます。

この場合アプリではHTTP Status 404 Not foundを受け取ることになりますが、これを適切に処理しないと体験が悪くなってしまいます。

3. Amazon Pollyの制限を受ける

Amazon Pollyに限らずですが、利用しているサービスの制限を受けてしまいます。

現在利用しているSynthesizeSpeech APIでは、下記のような制限があります。

  • APIに一度に送ることができる文字数はSSMLタグを含めて6000文字まで
  • APIで一度に適用できるレキシコン[6]の数は5つまで
  • 音声ストリームは10分間で、それを超過すると音声は切断される

これらの制限がmikanで問題になるようであれば、音声合成APIの再検討をすることになると思います。

まとめ

mikanのmy単語帳の音声がどのように作られているか、そのシステム構成や課題などについて解説しました。
ネイティブアプリにつきまとう後方互換性というテーマに向き合いながら、シンプルな構成に着地することができたと思います。

仮にAmazon Polly以外のサービスを利用したり、Cloud Storage以外のサービスを利用したりすることになったとしても、差し替えやすいような作りになっています[7]

今回は設計部分にフォーカスを当て、実装部分はほとんど省略してしまいました。
また別の機会に、実装に関する記事を書いてみたいと思います。

脚注
  1. 株式会社mikanのニュースリリース https://mikan.link/news ↩︎

  2. https://firebase.google.com/docs/functions/firestore-events ↩︎

  3. wタグがサポートされていませんでした ↩︎

  4. Amazon PollyがサポートしているSSMLタグはこちらです。 ↩︎

  5. https://docs.aws.amazon.com/polly/latest/dg/limits.html ↩︎

  6. https://docs.aws.amazon.com/polly/latest/dg/managing-lexicons.html ↩︎

  7. ただし、Cloud Firestoreから乗り換える場合は、多くの改修が必要になります。 ↩︎

Discussion

ログインするとコメントできます