Voicy Tech Blog

Voicy公式Techブログ

Voice Techを支える音声処理APIをPython+Falcon+Docker+ECSで開発した話 〜技術選定の苦労話を添えて〜

こんにちは! 11月からVoicyでエンジニアとして働き始めたぱんでぃーです。

前回のエントリーからしばらく間が空きましたが、 ここ数ヶ月でVoicyを取り巻く環境も大きく変わってきました。

具体的には

www.wantedly.com

などがありましたが、今回のエントリーは2つ目のGoogle Homeにも関連するテーマで書きたいと思います。

今回のプロジェクトの背景

現在、Voicyでは下記のように1回の配信の中に複数のチャプターが含まれています。

f:id:voice-tech:20171212203554p:plain

実際の音声ファイルもチャプターごとに存在するのですが、Google HomeやAlexaなどのデバイスから音声コンテンツを配信する際にはひとまとまりのファイルに収まっていた方が都合が良い事があります。 そこで、今後Voicyが様々な声に関するサービスを展開していくための処理基板として、音声処理用のRESTful API(以下、AudioProcessor)を開発する事になりました。

今回はそのプロジェクトの中での技術選定の理由や全体のシステム構成などについて、下記の流れでご紹介していきたいと思います。

主要な技術スタック

まずは主要な技術スタックとその選定理由を紹介します。

音声ファイルの処理エンジン

FFmpeg 音声だけでなく動画ファイルにも対応した、マルチメディアファイル用のエンコーダです。 音楽業界のエンジニアさんも使用している、音声処理ソフトウェアのスタンダードなライブラリの一つです。

FFmpeg(エフエフエムペグ)は動画と音声を記録・変換・再生するためのフリーソフトウェアである[3]。Unixオペレーティングシステム (OS) 生まれであるが現在ではクロスプラットフォームであり、libavcodec(動画/音声のコーデックライブラリ)、libavformat(動画/音声のコンテナライブラリ)、libswscale(色空間・サイズ変換ライブラリ)、libavfilter(動画のフィルタリングライブラリ)などを含む。ライセンスはコンパイル時のオプションによりLGPLGPLに決定される。コマンドラインから使用することができる。対応コーデックが多く、多彩なオプションを使用可能なため、幅広く利用されている。 https://ja.wikipedia.org/wiki/FFmpeg

クロスプラットフォームの音声処理用のソフトウェアとしてはPortAudioも対抗馬として挙がっていましたが、FFmpegは「動画ファイルから音声ファイルだけを抽出する」といったこともできるため、今後のサービス展開も考慮してFFmpegを採用しました。

開発言語・フレームワーク

  • Python (ver. 3.6.3)
  • Pydub Python用に開発された、FFmpegの音声処理機能のみを簡単に扱うためのWrapperライブラリ。(ちなみに上述したPortAudio用のWrapperライブラリとしては、PyAudioがあります。)
  • Falcon RESTful APIに特化した高速で軽量なフレームワーク

VoicyではAPIなどのサーバーサイドの言語はGOに寄せていこうという流れがありますが、GO用の音声処理ライブラリで良さそうなものが見つからなかったため、第二の選択肢としてPythonで探した結果、Pydubというライブラリが良さそうだったため採用に至りました。

社内にはFFmpegに精通したエンジニアがいなかったため、公式ドキュメントの充実さは選定基準として重きを置いていましたが、その点、Pydubは公式のAPIドキュメントが非常に充実しており、FFmpegを使用したことが無い場合でも容易に扱うことができました。

GO用のライブラリとしては、gmfが良さそうでしたが、Beta版である事とドキュメントが少ない点がネックとなりました。

また、API開発用のFalconについては下記の記事などが参考になります。

Pythonフレームワークとしては、DjangoやFlask、Pyramidに比べると新しいですが、公式ドキュメントGithubのWikiが充実しており、RESTful APIに必要なRoutingやValidationなどのロジックをシンプルに書けるので、新規でAPIを開発するのであればFalcon一択と言っていいのではないでしょうか。

今回のプロジェクトとは直接的には関係ありませんが、VoicyではまだPythonで書かれたプロダクトがありませんでした。ただ、今後Voicyがユーザのデモグラ情報やビヘイビア情報を活用したサービスを展開していく上で、Pythonを使ったデータ分析は避けては通れない道となるのでこの機会にPython資産を作りたかったという背景もありました。(自分の"好みの声だけ"に包まれた生活とか、想像しただけでステキじゃないですか!?)

APIの実行環境

DockerとAlpine Linuxについて

まずは何と言ってもDocker。これの採用は外せませんでした。 Pythonと言えば、初学者が環境開発(特に仮想環境の構築)で悩むことで有名ですね。

qiita.com

また、FFmpegも複雑・・・という程ではないですが、インストールにひとクセあるソフトウェアですので、Dockerを使ってImageをPullするだけでサクッと同一の環境を作れるのは非常にありがたいです。

DockerのOSは軽量でDocker Imageには最適な、Alpine Linuxを選んでいます。 実際に採用しているのは公式のAlpine Imageではなく、下記のImageですが、

https://hub.docker.com/r/gliderlabs/alpine/

両者の違いやgliderlabs/alpineを選ぶ理由については下記のサイトで詳しく述べられているため、このエントリーでは割愛します。

また、APIの実行環境を構成するためのDockerfileはこちらです。

# Use Alpine Linux as a parent image
FROM gliderlabs/alpine:3.6

MAINTAINER voicy.jp

# change system timezone to Asia/Tokyo
RUN apk update && \
    apk-install tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

# setup pyton runtime
RUN apk-install \
        'python3<3.7' \
        'python3-dev<3.7' \
        build-base && \
    echo "Dockerfile"        >> /etc/buidfiles && \
    echo ".onbuild"          >> /etc/buidfiles && \
    echo "requirements.txt"  >> /etc/buidfiles

RUN apk-install 'ffmpeg<3.5'

WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Install any needed packages specified in requirements.txt
RUN pip3 install --trusted-host pypi.python.org -r requirements.txt -c constraints.txt

# Make port 8000 available to the world outside this container
EXPOSE 8000

RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]

いくつか解説しますと、

# change system timezone to Asia/Tokyo
RUN apk update && \
    apk-install tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

Alpineのパッケージ管理ツールである、apkコマンド(Ubuntuで言うところのaptコマンド)の更新を行った後、Dockerコンテナ内のOSのタイムゾーンAsia/Tokyoに設定しています。

# Install any needed packages specified in requirements.txt
RUN pip3 install --trusted-host pypi.python.org -r requirements.txt -c constraints.txt

APIの実行に必要な依存パッケージのインストールを行っています。 プログラムが直接依存しているパッケージをrequirements.txtにバージョン指定無しで記載し、

awscli
boto3
falcon
gunicorn
jsonschema
pipdeptree
pydub
python-json-logger
requests

constraints.txtにこれらの依存パッケージが依存しているものも含めた、全ての依存パッケージをバージョン指定と共に記載することで、柔軟にパッケージ管理を行えるようにしています。

appnope==0.1.0
astroid==1.5.3
awscli==1.12.1
.
. (省略)
.
traitlets==4.3.2
urllib3==1.22
wcwidth==0.1.7
wrapt==1.10.11

詳しくは下記で解説されています。

RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]

APIサーバーを起動するためのスクリプトに実行権限を付与してから、実行しています。docker buildコマンドでImageを作成する際に、COPY . /appした時のオリジナルファイルに実行権限があれば良いのですが、念のために設定しておいた方が安心です。(私も実際にここで少しハマりました。。。)

entrypoint.shでは単純にPython製のWSGI(Web Server Gateway Interface)サーバであるgunicornを起動しているだけですが、後述のECSの「タスク定義」で環境変数ENVを設定する事により、gunicornの設定ファイルを開発用と本番用で切り替えています。

#!/bin/ash

if [[ "${ENV}" = 'prod' ]]; then
  gunicorn --config ./environments/prod/gunicorn.conf.py 'audio-processor.app:get_app()'
else
  gunicorn --config ./environments/dev/gunicorn.conf.py 'audio-processor.app:get_app()'
fi

gunicornの起動設定以外にも環境変数を使って、下記のような制御を行っています。

  • データベースやS3などの接続先を本番用と開発用で切り替え。
  • 本番環境ではログ分析を行いやすくするためにJSON形式で出力し、開発環境では見やすいプレーンテキストでログ出力。

このへんの設計方針に関しては、The Twelve-Factor App (日本語訳)が非常に参考になりますので、まだご覧になっていない方は是非読んでみてください。

コンテナオーケストレーションとしてのECS

最後に、このプロジェクトを通して最も悩まされたAWSでのコンテナ管理サービスの選定についてです。 本エントリーを執筆中の2017年12月12日時点では選択肢としては下記の3つになるかと思います。

これら3つの違いについてはこの記事がよくまとまっています。

qiita.com

最終的には、採用実績やWeb上のドキュメントが豊富な点でECSを採用しましたが、実際に運用を始めてみると下記のようなデメリットも見えてきたので、AWSでのベストプラクティスについては今後も検討する必要がありそうです。

  • Dockerコンテナに対するロードバランシングとコンテナのホストマシンであるEC2インスタンに対するロードバランシングを2重で管理する必要がある。
  • SwarmやKubernetesといったDockerネイティブのオーケストレーターを使えない。 ※ Kubernetesについては次期バージョンでネイティブサポート。
    [速報]DockerがKubernetesとの統合およびサポートを発表。DockerCon EU 2017 - Publickey
  • クラスター、サービス、タスクといったECS独自の概念を覚える必要があり、これがまた少し複雑。

Docker for AWSAWSではなくDockerコミュニティ製のオーケストレーターですが、CloudWatch Logsなどのサービスとも簡単に連携でき、機能としては最も魅力的に映ったのでもう少しドキュメントが充実してきたら積極的に採用してみたいところです。

アップデートの早いECSでのコンテナデプロイ環境の構築については公式ドキュメントを読み込むのが一番ですが、上述したECS独自の概念があるため、下記も併せて読むのがオススメです。

コンテナ管理サービスを選定中に「正直、コンテナオーケストレーションではKubernetesがデファクトスタンダードになりつつあるのにな。。。。今回はAWS上で構築すると決めたけど、やっぱりGKE使いたい。。。」と憂鬱な思いにもなりましたが、ついに出ましたね!! 先日開催されたAWS re:Invent 2017で更に2つのコンテナ管理サービスが登場し、AWS上でもKubernetesが使えるようになりそうです。

  • EKS(Elastic Container Service for Kubernetes)
  • AWS Fargate

tech.recruit-mp.co.jp

システム構成

最終的にAudioProcessor周りのシステム構成は下記のようになりました。 (今回のエントリーに関わりの少ないサービスは色を薄くしています。)

f:id:voice-tech:20171212212919p:plain

一例として、Google Home用に複数の音声ファイルを結合する処理の流れを解説します。

  1. パーソナリティが録音アプリで登録した複数の音声ファイルがS3のVoicy App Bucketに登録され、URIがRDSに登録される。
  2. 定期実行されているバッチが結合待ちの音声ファイル情報をRDSから取得し、ELBを通しECSのAudioProcessorに音声ファイル結合のリクエストを送る
  3. AudioProcessorはS3のVoicy App Bucketから音声ファイルをダウンロードし、FFmpegのWrapperライブラリを通し音声ファイルを結合。その後、S3のAudioProcessor Bucketにアップロード。
  4. AudioProcessorからのレスポンスで受け取ったURI情報を元に、AudioProcessor Bucketから結合済みのファイルをダウンロードし、Voicy App Bucketへアップロード。その後、Voicy App BucketURI情報をRDSに登録。(AudioProcessorはサービスとは独立した位置づけにしているため、バケット間の直接移動はあえて行っていません)
  5. Google Homeで音声コンテンツが呼び出された際はAPIサーバを介し、結合済みの音声ファイルをS3から配信。

以上がざっくりとした処理の流れです。 今回のスプリント内では達成できなかった今後の課題としては、

  • TravisCIやCircleCIを使った自動テスト・コンテナの自動デプロイ環境の構築
  • ECSでのコンテナオーケストレーションの最適化

などを進めて行きたいと思っています。

長くなりましたが、今回のエントリーが皆さんのDockerやPythonを使った開発プロジェクトの助けになれば幸いです。 また、Voicyではフロントエンド・バックエンドを問わずフルスタックで新しい技術に挑戦できる環境がありますので、「新しい声の世界・サービスを自分で創ってみたい!!」というエンジニアの方はオフィスに遊びに来てもらえると嬉しいです!

次回のエントリーはVoicyに新卒で入った、美人エンジニアの投稿を予定していますので、乞うご期待ください!