📢

AWS Lambda で TTS (pyopenjtalk) をしてみる

2024/10/05に公開

こんにちは、初めましての方は初めまして。株式会社 Fusic の瓦です。地球の気温調整が下手くそすぎて急に気温が下がったせいで最近体調を崩しがちです。「下手くそなのは地球の気温調整じゃなくてお前の服の選択だぞ」という意見は知りません。

この記事では、Lambda と pyopenjtalk を使ってサーバレスで音声合成をしてみます。最近はサーバレスでどこまで機械学習が動かせるのかを色々試しており、前回は LLM を Lambda で動かしてみた(3B が LLM かどうかという議論はありますが…)こともあり、今回は「音」という技術に注目して Lambda で動かしてみます。音声合成は色々なモデルがありますが、今回は手軽に使用できる pyopenjtalk で動かしてみることにします。

AWS Lambda で動かしてみる

SAM プロジェクトの準備とデプロイ

今回はサーバレス (Lambda) で動かすので Serverless Application Model (SAM) を使ってデプロイします。SAM の詳細については他記事[1] [2]に譲ります。

今回のファイル構成およびファイル内容は以下のようにしています。後述する理由により、初期化用のファイルを用意しています。

ファイル構成
├── container
│   └── Dockerfile          # 関数を動かすためのイメージ
├── template.yaml           # リソース定義
└── tts_function
    ├── app.py              # 実際に動くファイル
    ├── initialize.py       # 初期化用のファイル
    └── requirements.txt    # 必要なモジュール定義
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  lambda-openjtalk

  Sample SAM Template for lambda-openjtalk

Globals:
  Function:
    Timeout: 180
    MemorySize: 1024

Resources:
  TTSFunction:
    Type: AWS::Serverless::Function 
    Properties:
      PackageType: Image
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api 
          Properties:
            Path: /tts
            Method: POST
    Metadata:
      Dockerfile: container/Dockerfile
      DockerContext: ./

Dockerfile
FROM python:3.12-bullseye

WORKDIR /workspace/
COPY tts_function/requirements.txt /workspace/

RUN cd /workspace && pip install -r requirements.txt
RUN pip install awslambdaric

COPY tts_function/initialize.py /workspace/
RUN /usr/local/bin/python initialize.py

COPY tts_function/app.py /workspace/

ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ]
CMD [ "app.lambda_handler" ]
requirements.txt
scipy
pyopenjtalk
aws-lambda-powertools
initialize.py
import pyopenjtalk

pyopenjtalk.tts("あ")
app.py
import base64
import json
import numpy as np
import pyopenjtalk
from aws_lambda_powertools import Logger
from scipy.io import wavfile

logger = Logger()


@logger.inject_lambda_context(log_event=True)
def lambda_handler(event, context):
    body = json.loads(event["body"])
    input_text = body["text"]

    x, sr = pyopenjtalk.tts(input_text)
    file_path = "/tmp/talk.wav"
    wavfile.write("/tmp/talk.wav", sr, x.astype(np.int16))

    with open(file_path, "rb") as f:
        data = f.read()

    data_bytes = base64.b64encode(data).decode()

    return {
        "statusCode": 200,
        "body": json.dumps({"speech": data_bytes}),
    }

これらのファイルが用意出来たら、sam buildsam deploy コマンドで AWS 上にデプロイします。細かい引数などはドキュメントを参照して適宜つけてあげてください。

リソースがデプロイ出来たら text キーに生成したい値を添えて API を叩いてあげると、wav ファイルを base64 エンコードしたものが返ってきます。base64 デコードしてファイルとして書き出せば音声ファイルとして出力できます。

pyopenjtalk を Lambda で使用する際の注意点

initialize.py を実行しないイメージで Lambda を動かすと以下のようなエラーメッセージが出ます。

[Errno 30] Read-only file system: '/usr/local/lib/python3.12/site-packages/pyopenjtalk/open_jtalk_dic_utf_8-1.11'.

これはエラーメッセージの通り、読み取り専用ディレクトリにファイルを書き込もうとしているために起こっています。ローカルで pyopenjtalk を動かしてみると分かるんですが、pyopenjtalk では一番初めに音声合成に用いる辞書ファイルをダウンロードします。この辞書ファイルをモジュールと同じディレクトリに配置するのですが、Lambda での仕様により実行時には /tmp ディレクトリ以外でのファイルの書き込みは出来ません。 そのため、上のようなエラーとなります。initialize.py はこのエラーを回避するため、イメージビルド時に実行して辞書ファイルをダウンロードしイメージ内に含まれるようにしています。

また今回はこの対応をしていませんが、/tmp ディレクトリは複数の Lambda を実行した際に引き継がれることがあります。そのため、恐らく Lambda 内で書き出した /tmp/talk.wav ファイルは毎回削除するか、UUID などを使って一意な名前にする方が安全だと思います。

まとめ

この記事では pyopenjtalk を使って Lambda で音声合成をしてみました。サーバレスでかなり楽に動かせるので、色々なサービスと組み合わせることが簡単に出来そうですね。ただ Lambda は基本的にコールドスタートなので、最初の実行時は時間がかかります。リアルタイムで推論したい場合は Provisioned Concurrency を使用するか、EC2、もしくは SageMaker などにデプロイして使用出来るので、ユースケースに応じてどのサービスを使うか選択していきましょう。

最後に宣伝になりますが、機械学習でビジネスの成長を加速するために、Fusic の機械学習チームがお手伝いたします。機械学習のPoCから運用まで、すべての場面でサポートした実績があります。もし、困っている方がいましたら、ぜひ Fusic にご相談ください。お問い合わせからでも気軽にご連絡いただけます。また Twitter の DM でのメッセージも大歓迎です。

脚注
  1. https://tech.fusic.co.jp/posts/2021-12-10-http_api_cors/ ↩︎

  2. https://zenn.dev/fusic/articles/d2a307b12e5288 ↩︎

GitHubで編集を提案
Fusic 技術ブログ

Discussion