ap-voice

command module
v1.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 9 Imported by: 0

README

✍️ AP Voice

CI Status Language Platform

🚀 概要 (About) - 台本を先に作り、確認してから声にする

AP Voice は、ドキュメントをナレーション音声に変換する Cloud Run + Cloud Tasks 上のサービスです。

Web 記事や GCS 上の文書を読み込み、Gemini に話者とスタイルを指定した台本(JSON)を生成させます。 台本と音声は別の工程です。台本ができたら履歴に並ぶので、内容を確認してから VOICEVOX エンジンで並列合成します。読みや話者を直したければ、生成をやり直さずに 合成だけ何度でもかけ直せます。

1つのイメージを SERVER_ROLEWeb 面(公開)と Worker 面(非公開)の2サービスとして デプロイします。入出力はどちらも Web URL / GCS (gs://) / ローカルを透過的に扱います。


📦 使い方

1. 環境設定

ValidateEssentialConfig はロールごとに必要なものだけを検証します。担当しない面の設定を 要求すると、使わない認証情報へのアクセス権を配ることになるためです。

どのロールでも必須

変数名 説明
SERVER_ROLE web / worker / bothboth はローカル開発用)。未設定・未知の値は起動時エラーです。担当する面だけを組み立て、ルートもその面のものだけを登録します。
GEMINI_MODELS Gemini モデル名。カンマ区切りで複数指定でき、先頭が既定モデルです。タスクの ai_model が空ならこれを使います。既定値は持たず、未設定なら起動時にエラーになります。
GCP_PROJECT_ID GCP Project ID。Gemini は Vertex AI 経由でのみ呼びます(API キー経路は持ちません)。ローカル実行では ADC が必要です。
GCS_VOICE_BUCKET 台本と音声の置き場。出力先は利用者に入力させず、ジョブ ID から gs://<bucket>/voice/<jobID>/audio.wav を導きます。web は履歴の表示に、worker は保存済み台本の読み出しに使うため、どちらのロールでも必須です。

Web 面(web / both)で必須

変数名 説明
CLOUD_TASKS_QUEUE_ID 投入先のキュー名。
WORKER_URL worker サービスの URL。パスは含めません
TASK_CALLER_SERVICE_ACCOUNT_EMAIL タスクに載せる caller SA。トークンを発行するのは Cloud Tasks であって、このプロセスが署名するわけではありません。
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET Google OAuth のクライアント。
SESSION_FIRESTORE_DATABASE / SESSION_FIRESTORE_COLLECTION セッションを置く Firestore(既定はどちらも sessions)。ジョブ状態用とは別のデータベースを指します
ALLOWED_EMAILS / ALLOWED_DOMAINS ログインを許可する相手(カンマ区切り)。どちらも空だと起動しません。
ALLOWED_M2M_SERVICE_ACCOUNTS 機械(MCP サーバーなど)が OIDC Bearer で叩くときに許可する SA(カンマ区切り)。任意 — 空なら M2M 検証は常に失敗し、すべてセッション認証に落ちます。

Worker 面(worker / both)で必須

変数名 説明
TASK_AUDIENCE_URL OIDC 検証の audience(Worker 自身の URL)。未設定なら SERVICE_URL を使います。
ALLOWED_TASK_SERVICE_ACCOUNTS 受け付ける caller SA(カンマ区切り)。投入側の SA を指定します。web/worker で実行 SA を分けるため、worker には「他人の SA」が並びます。

任意

変数名 説明
SERVICE_URL / PORT 公開 URL と待ち受けポート (Default: http://localhost:8080 / 8080)。
VOICEVOX_API_URL エンジンの URL。未設定なら http://localhost:50021 を使います(ローカル実行と Cloud Run のサイドカー構成のどちらもこの値でよいため)。
VOICEVOX_MAX_PARALLEL_SEGMENTS 1ジョブ内で同時に投げるセグメント数 (Default: 4 = エンジンの vCPU 数)。スループットを縛っているのはこの値です。 合成は CPU バウンドなので 4 vCPU は同時4本で飽和し、実測でも 8 にするとスループットは横ばいのまま1件あたりの所要が倍になりました。上書きは戻すための口です。
VOICEVOX_SEGMENT_RATE_LIMIT セグメントの投入間隔 (Default: 100ms)。スループットのつまみではありません(実測の実効 0.24 件/秒 に対し、100ms は 10 件/秒 を許容)。起動時にエンジンを一斉に叩かないための保険で、同時実行数を縛るのは VOICEVOX_MAX_PARALLEL_SEGMENTS です。
VOICEVOX_SEGMENT_TIMEOUT セグメント1件あたりの上限 (Default: 120s)。「1件」はクエリと合成の2往復ぶんで、各往復は 1 回までリトライします。1往復ぶんの上限は HTTP_TIMEOUT です。
GCP_LOCATION_ID Cloud Tasks キューのリージョン (Default: asia-northeast1)。Vertex AI のエンドポイントとは別物で、そちらは global に固定してあります。
FIRESTORE_DATABASE ジョブ状態を置く Firestore データベース名 (Default: job-status)。名前付きを使うのは (default) の枠を占めないためです。コレクション名は設定にしません(サービスの身元であってデプロイごとに変わらないため)。
HTTP_TIMEOUT 外部 HTTP 通信(HTTP 1往復)のタイムアウト (Default: 60s)。セグメント1件の上限(VOICEVOX_SEGMENT_TIMEOUT)の半分にしてあります — 同じ値まで上げると、最初の1往復がセグメントの持ち時間を使い切ってリトライが入らなくなります。1往復を延ばしたいときにセグメント側を上げても効きません(小さいほうが先に効きます)。Slack への通知も同じクライアントを使うため、応答が無いときの待ち時間もこの値です。
PIPELINE_TIMEOUT ジョブ1件の実行上限 (Default: 25m)。Cloud Tasks より先にアプリが諦めるための値で、超えると失敗を通知して終わります。
TASK_DISPATCH_DEADLINE Cloud Tasks がワーカーの応答を待つ上限 (Default: 30m、Cloud Tasks の上限)。PIPELINE_TIMEOUT より長くします。
AP_MUSIC_BUCKET 楽曲レシピ(music.Recipe 形式の recipe.json)の置き場 (Default: ap-music)。作成画面の「楽曲レシピ」タブが、楽曲生成サービスのジョブ ID から gs://<bucket>/music/<jobID>/recipe.json を組み立てるために使います。動画生成サービスと同じ変数名・同じ規則です。
SLACK_WEBHOOK_URL 完了・失敗の通知先。未設定なら通知は無効になります。
GOOGLE_APPLICATION_CREDENTIALS gs:// を読み書きする場合のみ(ADC 利用時)。

環境変数が持つのはデプロイ先が決める設定だけです。入力元・出力先・生成モードといった 実行ごとに変わる値は、タスクのペイロード(JSON)で渡します。

2. 起動
go run .        # SERVER_ROLE が必須

SERVER_ROLE が担う面だけを組み立てます。

ロール 組み立てるもの 公開されるルート
web 投入フォーム・モード一覧・履歴画面・Cloud Tasks への投入 /, /modes/*, /speakers, /reading/preview, /jobs/*, /auth/*
worker パイプライン(Gemini + VOICEVOX + GCS + 通知) POST /tasks/generate
both 両方(ローカル開発用) 上記すべて
3. HTTP エンドポイント

認証は 1 つです。 auth.Protected が OIDC の Bearer とセッションの両方を通すため、 同じ URL を人も機械も叩けます(姉妹サービスと同じ形)。GET /health/static/* だけが 認証の外側で、ロールに関係なく登録されます。

メソッド パス 用途
GET /health ヘルスチェック(/healthz は Cloud Run の既定ドメイン側で予約パス扱いになりコンテナまで届かないため使いません)。認証不要
GET /static/* 埋め込みの CSS / JS と vendor/ 配下の Bootstrap。認証不要。バージョンがパスに入る vendor/public, max-age=31536000, immutable、URL が変わらない自前アセットは public, max-age=300, must-revalidate
GET /auth/login /auth/callback /auth/logout Google OAuth のログイン・コールバック・ログアウト
GET / 投入フォーム(入力ソース / 楽曲レシピ / 台本 JSON の 3 タブ)
GET /modes 選べるモードの一覧(キー・表示名・説明)。front matter が唯一の出所です
GET /modes/{mode} モード 1 つの詳細。実際に Gemini へ渡るプロンプト本文(partial 展開済み)を見せます。一覧に無いキーは 404
GET /speakers 話者ごとに使えるスタイル。実在しない組み合わせは保存時に弾かれるので、選ぶ前にここを見ます
POST /reading/preview 合成したらどう読まれるかを行ごとに返します(合成はしません)。「水面」は ミナモ ではなく スイメン です。編集画面の「読みを確認」と機械の両方が使います
POST /jobs ジョブを投入。本文がフォームなら画面の 3 タブ、JSON なら機械です。JSON の commandgenerate / generate_and_synthesize(入力ソースから AI に書かせる)/ synthesizescript を渡して自分の台本を喋らせる。Gemini を呼びません)。受付は 202Location: /jobs/{jobID}
GET /jobs ジョブを新しい順に。?page= / ?per_page=(既定・上限とも 100)/ ?state=queued / running / succeeded / failed)。1 件ごとに state が付くので、実行中と失敗を 1 件ずつ引かずに見分けられます。?state= は Firestore の複合索引(state 昇順 + queued_at 降順)が要ります — 索引が無い環境では絞り込んだときだけ失敗します
GET /jobs/{jobID} ジョブ 1 件。投入から削除まで同じ URL です。ブラウザには詳細画面(台本をここで直します — 行の追加・並べ替え・削除、読みの確認。台本がまだ無いジョブでも開き、記録された状態と失敗理由を出します)。Accept: application/json には進行状況(queued / running / succeeded / failed)と、成果物の在り処(audio_uri / script_uri)、台本を作ったときの mode / input_uri / ai_model(作り直しに使います)。記録が無ければ 404 で、呼び出し側は unknown として扱います。詳細画面の自動更新と機械のポーリングが同じものを読みます
DELETE /jobs/{jobID} 成果物をまとめて削除。画面の削除ボタンも fetch で DELETE を送ります。成果物を 1 つも持たないジョブ(台本を書く前に失敗したもの)は記録だけを消します。記録も無ければ 404
GET /jobs/{jobID}/audio 音声の再生できるリンク(署名付き URL、1 時間)。ブラウザには 302、Accept: application/json には URL そのものを返します。状態や一覧には載せません — 期限があり、ポーリングのたびに発行するのは無駄なためです。音声が無ければ 404
GET /jobs/{jobID}/script 保存済みの台本。ブラウザから開くと <jobID>.json として落ちます。小さな JSON なので、音声と違って署名付き URL を挟まずそのまま返します
PUT /jobs/{jobID}/script 台本を差し替え。合成はしません(何度か直してから 1 度だけ合成できます)
POST /jobs/{jobID}/synthesize 音声を作る。JSON(本文なし)は保存済みの台本から。フォーム(編集画面のボタン)は編集中の台本を保存してから。台本はタスクに載せません(先に保存してジョブ ID だけを渡します)。202Location
POST /jobs/{jobID}/regenerate 同じ入力ソースから台本を作り直す。入力ソースは記録から復元するので貼り直し不要です。ジョブ ID は変わりません
POST /tasks/generate Cloud Tasks 専用のワーカー。OIDC 検証を通らないリクエストは 401、SERVER_ROLE=web ではルートごと登録されないため 404

同じリソースはルートも 1 本です。 表現は Accept で決まり、application/json を送れば JSON が、ブラウザの Accept なら画面が返ります。エラー本文も同じ判定で {"error": "..."} になります。

/api/ 接頭辞は持ちません。 人と機械の違いは本文の形(フォームか JSON か)と Accept で 吸収し、URL は 1 本です。パスの切り方は public-docs の URL 命名規約に従います。

副作用のあるメソッドには CSRF トークンが要ります。 フォームは csrf_token の hidden で、 画面の JS は X-CSRF-Token ヘッダーで送ります。OIDC Bearer で認証した機械はこの検証に入りません (CSRF はクッキーの自動送出を悪用する攻撃への対策で、Bearer を明示的に付ける呼び出しには 当てはまらないためです。代わりに ALLOWED_M2M_SERVICE_ACCOUNTS で呼び出し元を絞ります)。

台本の検証(話者・スタイルが実在するか、行数の上限)は、画面も API も同じ関数を通ります。

4. タスクのペイロード

台本生成と音声合成は別の入口です(理由は概要のとおり)。

command 何をするか 必須フィールド
generate 入力ソースから台本を作る。音声は作りません input_uri, output_uri
synthesize 保存済みの台本から音声を作る(Gemini を呼ばない) output_uri, job_id
generate_and_synthesize 台本を作ってそのまま音声まで作る。確認を挟みません input_uri, output_uri
フィールド 説明
command generate / synthesize / generate_and_synthesize省略できません(台本を持ち込んだまま書き忘れると、その台本が黙って捨てられて生成が走るため)。
input_uri 入力ソースURI。Web URL、GCS (gs://)を指定します。generate で必須。
job_id ジョブの識別子。成果物の置き場もこれで決まります。synthesize では保存済み台本の在り処でもあり、必須です。
output_uri WAV の出力先URI。台本は拡張子だけ .json に替えた隣に置かれます。Web 面から投入する場合は入力しません(ジョブ ID から gs://<bucket>/voice/<jobID>/audio.wav を導きます)。
mode 台本の形式。generate のみ。assets/prompts/<ジャンル>_<形式>.md を置けばモードが増えます。 表示名と説明はファイル冒頭の front matter(label / direction / use_when)から出ます。選択肢の並びは同じ front matter の order(10 刻み、小さいほうが先)で決まります。
一覧は GET /modes で見られます。
ai_model 使用する Gemini モデル名。空なら GEMINI_MODELS の先頭を使います。generate のみ。
{
  "command": "generate",
  "input_uri": "https://example.com/tech-news",
  "output_uri": "gs://my-bucket/audio/tech-news.wav",
  "mode": "tech_dialogue"
}
{
  "command": "synthesize",
  "job_id": "voice-20260814-020913-b1b8b2f9e8d7",
  "output_uri": "gs://my-bucket/voice/voice-20260814-020913-b1b8b2f9e8d7/audio.wav"
}

台本はペイロードに載りません。 長い台本は Cloud Tasks の 1MB 上限に当たりうるため、 投入側が先に保存して job_id だけを渡します。自分で書いた台本を喋らせる場合も同じで、 POST /jobs(JSON で command: "synthesize"script)か画面の「台本 JSON」タブへ渡すと、 そこで保存されてからこの形のタスクになります。


🔄 処理シーケンス図

sequenceDiagram
    autonumber
    actor User as 利用者
    participant Web as Web 面 (公開)
    participant Tasks as Cloud Tasks
    participant Worker as Worker 面 (非公開)
    participant Gemini as Vertex AI
    participant Engine as VOICEVOX (サイドカー)
    participant Store as GCS
    participant State as Firestore
    participant Slack as Slack

    Note over User, Slack: 1. 台本を作る (command=generate)
    User->>Web: POST / (入力ソース・モード・モデル)
    Web->>Web: ジョブIDを発行し、出力先を導出
    Web->>State: queued を記録
    Note right of Web: **enqueue より先に**。逆だと Worker の running を上書きしかねません
    Web->>Tasks: enqueue(Request)
    Web-->>User: 202 受付
    Tasks->>Worker: POST /tasks/generate (OIDC)
    Worker->>Store: 入力を読む (gs:// のとき)
    Worker->>Gemini: 台本を生成 (スキーマ強制)
    Gemini-->>Worker: Script(title + lines)
    Worker->>Store: audio.json を書く
    Worker->>State: succeeded を記録(題名・台本の在り処)
    Note right of Worker: **音声はまだ作りません**
    Worker->>Slack: 完了通知(詳細画面のリンク付き)

    Note over User, Slack: 2. 台本を確認・修正する
    User->>Web: GET /jobs
    Web->>State: ジョブを一覧(成果物は読みません)
    User->>Web: GET /jobs/{jobID}
    Web->>Store: audio.json を読む
    Web-->>User: 台本を表示(話者・スタイル・本文を編集できます)
    User->>Web: POST /reading/preview (「読みを確認」/表の中身をそのまま)
    Web-->>User: 行ごとの読み(合成の直前と同じ変換)

    Note over User, Slack: 3. 音声を作る (command=synthesize)
    User->>Web: POST /jobs/{jobID}/synthesize (必要なら直してから)
    Web->>Store: 直した audio.json を保存
    Web->>State: queued を記録
    Web->>Tasks: enqueue(Request{JobID})
    Note right of Web: 台本は載せません(1MB 上限)。先に保存して ID だけ渡します
    Tasks->>Worker: POST /tasks/generate (OIDC)
    Worker->>Store: audio.json を読む
    loop セグメントごと(並列・レート制限あり)
        Worker->>Engine: POST /audio_query → /synthesis
        Engine-->>Worker: WAV
    end
    Worker->>Worker: WAV を結合
    Worker->>Store: audio.wav と audio.json を書く
    Worker->>State: succeeded を記録(音声の在り処)
    Worker->>Slack: 完了通知(詳細画面のリンク付き)

    Note over User, Slack: 4. 再生する
    User->>Web: GET /jobs/{jobID}/audio
    Web-->>User: 302 → 署名付き URL
    User->>Store: 署名付き URL で直接取得

🌳 プロジェクト構成ツリー図

ap-voice/
├── main.go                  # エントリポイント(サーバー起動)
├── Dockerfile               # scratch イメージ(静的バイナリのみ)
├── cloudbuild.yaml          # ビルドして2サービスへデプロイ
├── assets/                  # 埋め込み(prompts/*.md・speakers.json・templates/*.html・static/)
│                            #   static/js は app.js(全画面)+ 画面ごと(handlers の pageScripts)
└── internal/
    ├── config/              # 環境変数の読み込みとロール別検証
    ├── server/              # chi ルーター・グレースフルシャットダウン
    │   └── handlers/        #   Web 面(投入フォーム・履歴・詳細・再生)
    ├── domain/              # ドメインモデルとポート定義・成果物のパス規約
    ├── app/                 # DI コンテナとリソース管理
    ├── builder/             # 外部依存とハンドラーの組み立て
    ├── repository/          # 成果物の読み出し(台本)とジョブ状態の読み書き(履歴)
    ├── pipeline/            # command による分岐と各段(step_*.go)
    └── adapters/            # Gemini / VOICEVOX / Cloud Tasks / Slack / プロンプト

📜 ライセンス (License)
  • 使用キャラクター: VOICEVOX:ずんだもん、VOICEVOX:四国めたん、VOICEVOX:春日部つむぎ ほか (使える話者は assets/speakers.json = エンジンの /speakers 応答が決めます。 ライブラリ側は一覧を持ちません)
  • このプロジェクトは MIT License の下で公開されています。

Documentation

Overview

ap-voice は、ドキュメントをナレーション音声へ変換するオーケストレータサービスです。

Directories

Path Synopsis
Package assets は、プロンプト・話者一覧・HTML テンプレートを埋め込みリソースとして提供します。
Package assets は、プロンプト・話者一覧・HTML テンプレートを埋め込みリソースとして提供します。
internal
adapters
Package adapters は、外部サービス(Gemini・Slack 等)との接続を実装します。
Package adapters は、外部サービス(Gemini・Slack 等)との接続を実装します。
app
Package app は、アプリケーションの依存関係を組み立てて保持する DI コンテナを提供します。
Package app は、アプリケーションの依存関係を組み立てて保持する DI コンテナを提供します。
builder
Package builder は、設定値から各クライアントと DI コンテナを組み立てます。
Package builder は、設定値から各クライアントと DI コンテナを組み立てます。
config
Package config は、環境変数からアプリケーション設定を読み込み検証します。
Package config は、環境変数からアプリケーション設定を読み込み検証します。
domain
Package domain は、ap-voice のフレームワーク非依存なドメイン型とインターフェースを定義します。
Package domain は、ap-voice のフレームワーク非依存なドメイン型とインターフェースを定義します。
pipeline
Package pipeline は、台本生成と音声合成の各段を実行します。
Package pipeline は、台本生成と音声合成の各段を実行します。
repository
Package repository は、GCS 上の成果物の読み出しと、Firestore 上のジョブ状態の 読み書きを担います。
Package repository は、GCS 上の成果物の読み出しと、Firestore 上のジョブ状態の 読み書きを担います。
server
Package server は、HTTPルーティングとミドルウェアを構成します。
Package server は、HTTPルーティングとミドルウェアを構成します。
server/handlers
Package handlers は、Web 面の HTTP ハンドラーを実装します。
Package handlers は、Web 面の HTTP ハンドラーを実装します。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL