基本的に怠Diary

主に日常と作ったものを書いていく。

昔作ったVoiceVoxアプリを週末に改良した話

週末使ってガチャガチャやってました。

記事なにもやれてなかったので、生成AIに対話形式で内容引き出してもらって記事化した。

Q: 何をいじったの?

何年も前に作ったVoiceVoxのテキスト読み上げアプリがあって、今回は長文対応を追加しました。テキスト入力したら音声ファイルにして再生するやつです。ローカルで動くので外部API使いません。

Q: もともとの構成は?

別に凝ったことしてない普通の構成:

  • フロント: React (Vite)
  • API: Next.js(過去の別プロジェクトから流用)
  • 音声: VoiceVox Docker
  • DB: PostgreSQL + Prisma
  • 処理: 別コンテナでCurl叩いて回してる

React → Next.js API → VoiceVox の単純な流れです。

Q: 今回何を改良したの?

長文対応が今回のメイン。VoiceVoxって文字数制限があるので:

  1. テキスト分割: デフォルト200文字で区切るけど、句読点があればそこで切る(生成AIが改良してくれた)
  2. 一時ファイル: 各チャンクをtempファイルで生成
  3. WAV結合: 同一エンジン・同一パラメータなので音声フォーマット統一が保証される前提で、バイナリレベルで結合
  4. キュー処理: 長文は時間かかるので非同期で処理

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に任せる分担が効率的でした。

週末の数時間で長文対応できたので、まあ満足です。