週末使ってガチャガチャやってました。
記事なにもやれてなかったので、生成AIに対話形式で内容引き出してもらって記事化した。
Q: 何をいじったの?
何年も前に作ったVoiceVoxのテキスト読み上げアプリがあって、今回は長文対応を追加しました。テキスト入力したら音声ファイルにして再生するやつです。ローカルで動くので外部API使いません。
Q: もともとの構成は?
別に凝ったことしてない普通の構成:
- フロント: React (Vite)
- API: Next.js(過去の別プロジェクトから流用)
- 音声: VoiceVox Docker
- DB: PostgreSQL + Prisma
- 処理: 別コンテナでCurl叩いて回してる
React → Next.js API → VoiceVox の単純な流れです。
Q: 今回何を改良したの?
長文対応が今回のメイン。VoiceVoxって文字数制限があるので:
- テキスト分割: デフォルト200文字で区切るけど、句読点があればそこで切る(生成AIが改良してくれた)
- 一時ファイル: 各チャンクをtempファイルで生成
- WAV結合: 同一エンジン・同一パラメータなので音声フォーマット統一が保証される前提で、バイナリレベルで結合
- キュー処理: 長文は時間かかるので非同期で処理
Q: なんでキュー方式にしたの?
同期だとタイムアウトするから。リクエスト来たらジョブID返して、別プロセスで処理して、フロントでポーリングしてます。DBのステータスカラムで進捗管理してるだけの単純な仕組み。
Q: データ管理はどうしてる?
PostgreSQL + Prismaで:
- 音声ファイルのメタデータ(テキスト、パラメータ)
- 処理ステータス
- ファイル本体はNext.js側のVolumeに保存
履歴機能もあるので、前に作った音声を再利用できます。
Q: 実装で工夫したところは?
WAVファイル結合が一番面倒でした:
// WAVヘッダーの40バイト目からデータサイズ取得 const dataSize1 = view1.getUint32(40, true); const dataSize2 = view2.getUint32(40, true); // 新しいサイズでバッファ作成して音声データをコピー
あとは句読点での分割。最初は文字数だけで切ってたけど、生成AIが句読点考慮するよう改良してくれました。
private splitTextIntoChunks(text: string): string[] { const sentences = text.split(/([。!?\n])/); // 文の途中で切れないよう調整 }
Q: 課題とかある?
いくつかありますが、個人用途なので当面放置:
- 音声の繋ぎ: 分割した音声間に無音挿入してない
- リソース管理: 古いファイル削除とか特にしてない
- 可用性: VoiceVoxコンテナ止まったら当然動かない
VoiceVoxの特殊記法使えば無音問題は解決できそうですが、まあいつか。
Q: 参考になりそうなところは?
リポジトリ1(メイン処理):
- VoiceGenerator.createLongVoice(): 長文処理のメインロジック
- splitTextIntoChunks(): 句読点考慮の分割
- concatenateAudioBuffers(): WAVバイナリ結合
リポジトリ2(インフラ):
- docker-compose.yml: VoiceVoxコンテナ設定
- Prismaスキーマ: キュー管理のテーブル定義
- キューポーリング処理
特にWAVのバイナリ操作と非同期キューあたりは、似たようなことやる人には参考になるかも。
Q: 今回の改良で学んだことは?
既存コードの活用が一番大事ですね。Next.js APIの流用、過去のDB設計の再利用で開発時間短縮できました。
あと生成AIとの協業。コア部分の設計は自分でやって、細かい改良(句読点分割とか)は生成AIに任せる分担が効率的でした。
週末の数時間で長文対応できたので、まあ満足です。