はじめに
VOICEVOXを活用して、ずんだもんとおしゃべりができるアプリを作りたいと考えています。
リアルタイムでスムーズに会話を行うには、音声合成の処理速度が非常に大事です。VOICEVOXは、サーバーのスペックが高いほど音声の生成速度も向上しますが、その分コストも増加します。
サービスの規模や利用状況がまだ見通せない段階では、従量課金で柔軟にスケーリングできる仕組みが理想的です。
そこで、「高速な音声合成」と「コストの最適化」の両立を目指し、AWS Lambdaでメモリを最大まで割り当てた環境にVOICEVOXを組み込めないか検討しました。
最終的にできたもの
最終的に構築した仕組みは以下のとおりです。
API(Lambda Function URL)経由でリクエストを受け取り、AWS Lambda上でVOICEVOXを用いて音声を合成します。生成された音声データはS3にアップロードされ、その後、クライアント側には音声ファイル名(またはS3オブジェクトキー)を返します。
この構成により、サーバーレスでコストを抑えつつ、ある程度のリアルタイム性を確保した音声合成が可能になりました。初期段階のプロトタイプやスケーラビリティを重視したサービス構築にも適しています。
curl -X POST -H "Content-Type: application/json" \
-d '{"text" : "このテキストが音声になってS3に保存されるのだ!"}' \
${LAMBDA_FUNCTION_URL}
{
"filename": "e4138441-b08d-4789-bc84-cd6d12c92069.wav"
}
手順
VOICEVOXと関連ファイルのダウンロード
AWS Lambda で VOICEVOX を動作させるためには、Lambda の実行環境に対応したバイナリ(Linux ARM 64bit)が必要です。しかし、ダウンローダースクリプト download は ARM アーキテクチャの Linux 環境でしか実行できません。
そのため、ARM アーキテクチャの EC2インスタンスを一時的に用意し、そこで VOICEVOX Core をダウンロードしたうえで、ファイルを S3 にアップロードし、最終的にローカル環境へ移動させました。
以下のコマンドを EC2インスタンス上で実行します。
curl -sSfL https://github.com/VOICEVOX/voicevox_core/releases/download/0.16.0/download-linux-arm64 -o download
chmod +x download
./download
実行中に利用規約への同意が求められますので、内容を確認のうえ「同意する」選択肢を選んでください。
ダウンロードが完了すると、以下のようなファイル構成になります。
.
├── download
└── voicevox_core
├── c_api
│ ...
│ ├── include
│ │ └── voicevox_core.h
│ └── lib
│ └── libvoicevox_core.so
├── dict
│ └── open_jtalk_dic_utf_8-1.11
│ ...
├── models
│ ...
│ └── vvms
│ ├── 0.vvm
│ ├── 1.vvm
│ ...
└── onnxruntime
├── lib
│ └── libvoicevox_onnxruntime.so.1.17.3
...
voicevox_core
ディレクトリを S3 にアップロードします。
まず、アップロード用の S3 バケットを作成し、EC2 インスタンスにそのバケットへアップロードできる権限を付与します。
以下のコマンドを EC2 上で実行してください。
aws s3 sync voicevox_core s3://${S3_BUCKET_NAME}/voicevox_core
S3 にアップロードされたファイルをローカル環境にダウンロードします。
以下のコマンドをローカル環境で実行してください。
aws s3 sync s3://${S3_BUCKET_NAME}/voicevox_core voicevox_core
Go言語から VoiceVox の APIを利用する。
全体のコードとディレクトリ構成については、以下のリポジトリを参照してください:
takoikatakotako/voicevox-aws-lambda
1. VoiceVox Core API のラッパー定義
まずは C の VoiceVox API をラップする voicevox_core.go
を定義します。
package main
/*
#cgo LDFLAGS: -L. -lvoicevox_core
#include "voicevox_core.h"
*/
import "C"
// VoicevoxCore is a function group that wraps the C API
type VoicevoxCore struct{}
// const char *voicevox_get_onnxruntime_lib_versioned_filename(void);
func (r *VoicevoxCore) voicevoxGetOnnxruntimeLibVersionedFilename() *C.char {
return C.voicevox_get_onnxruntime_lib_versioned_filename()
}
...
2. VoiceVox Wrapper の作成
VoicevoxCore を使いやすくラップする voicevox_wrapper
を定義します。
package main
/*
#cgo LDFLAGS: -L. -lvoicevox_core
#include "voicevox_core.h"
*/
import "C"
import (
"errors"
"fmt"
"unsafe"
)
type VoicevoxWrapper struct{}
func (v *VoicevoxWrapper) generate(text string, openJTalkDicDir string, onnxruntimePath string, voiceModelPath string, styleId int) ([]byte, error) {
core := VoicevoxCore{}
...
}
3. AWS Lambda ハンドラの実装
Lambda 関数のエントリポイントである main.go を定義します。API Gateway からのリクエストを受け取り、VoiceVox で音声を生成し、S3 に保存します。
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/google/uuid"
"net/http"
)
...
func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
...
// Parse Request
var req Request
if err := json.Unmarshal([]byte(request.Body), &req); err != nil {
return errorResponse(http.StatusBadRequest, "不正なJSONなのだ")
}
// Generate Voice
voicevox := VoicevoxWrapper{}
data, err := voicevox.generate(
req.Text,
openJTalkDirPath,
onnxruntimePath,
voiceModelPath,
styleID,
)
...
// Upload Audio File
filename := fmt.Sprintf("%s.wav", uuid.New().String())
client := s3.NewFromConfig(cfg)
input := &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(filename),
Body: bytes.NewReader(data),
ContentType: aws.String("audio/wav"),
}
...
// Return Response
body, _ := json.Marshal(Response{Filename: filename})
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Body: string(body),
Headers: map[string]string{
"Content-Type": "application/json",
},
}, nil
}
...
func main() {
lambda.Start(handler)
}
ビルドとLambdaの作成
ECRを作成し、イメージをビルドしてプッシュします。
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${ECR_URL}
docker build -t voicevox .
docker tag voicevox:latest ${ECR_URL}/voicevox:latest
docker push ${ECR_URL}/voicevox:latest
続いて、上記で作成したイメージを使用して Lambda 関数を作成します。
メモリは最大値の 10240MB、タイムアウトは 5分 に設定しました。(※メモリは後で実際の使用量に応じて調整すると良いです)。
また、Lambda の IAM ロールには S3 への書き込み(PutObject)権限を付与し、環境変数として出力先バケット名を設定します。
最後に、関数 URL を有効化し、その URL に対してリクエストを送ることで音声合成が実行され、S3 に音声ファイルが保存されます。
curl -X POST -H "Content-Type: application/json" \
-d '{"text" : "このテキストが音声になってS3に保存されるのだ!"}' \
${LAMBDA_FUNCTION_URL}
{
"filename": "e4138441-b08d-4789-bc84-cd6d12c92069.wav"
}
コストについて
「このテキストが音声になってS3に保存されるのだ!」という内容を送信した場合、Lambda 関数の実行時間はおよそ 5000ms(5秒) でした。
使用メモリ:10,240MB(10GB)
実行時間:5秒
GB-秒単価:USD 0.0000133334
10 GB × 5 秒 = 50 GB-秒
50 × $0.0000133334 = $0.00066667
+ リクエストあたりの固定費 $0.0000002
合計 = $0.00066687
日本円換算(1ドル=155円と仮定):約 0.103円 ≒ 約 0.1円 / 回
実際のメモリ使用量は以下の通りでした:
REPORT RequestId: fcb1b803-8f4d-4c54-9b0f-748cd3373539
Duration: 4885.11 ms
Billed Duration: 4886 ms
Memory Size: 10240 MB
Max Memory Used: 1168 MB
約10GBを割り当てていても、実際には約1.1GBしか使用されていないことがわかります。
コスト削減のため、適切なメモリサイズに調整するとよいかもしれません。