# Seed-VC Streaming API 仕様書 ## 概要 Seed-VC Streaming APIは、ゼロショット音声変換をチャンク単位で処理するHTTP APIサーバーです。 クライアントが音声を小さなチャンク(例: 500ms)に分割して順次送信し、サーバーが各チャンクを変換して返すことで、低レイテンシなストリーミング処理を実現します。 - **ベースURL**: `https://akatuki25-seed-vc-streaming.hf.space` - **モデル**: Seed-VC (Plachtaa/seed-vc) - **入力サンプルレート**: 16000Hz (推奨) - **出力サンプルレート**: 22050Hz - **推奨チャンクサイズ**: 500ms (overlap 100ms) - **プリセット参照音声**: デフォルトで`default_female`が利用可能(カスタム音声のアップロードも可) --- ## アーキテクチャ ### ストリーミング処理フロー ``` クライアント側: 1. 音声をチャンク分割 (500ms × N個) 2. セッション作成 → session_id取得 3. (オプション) カスタム参照音声アップロード ※プリセット参照音声を使う場合はスキップ 4. チャンクを順次送信 (chunk_0, chunk_1, ...) 5. 各レスポンスを受信・結合 6. セッション終了 サーバー側: 1. セッション管理 (参照音声の特徴量をキャッシュ) ※プリセット使用時はHF Datasetから自動ダウンロード 2. 各チャンクを独立に変換 3. クロスフェード処理 (overlap_msで指定) 4. 変換後チャンクを即座に返却 ``` ### 重要な設計ポイント - **チャンク単位処理**: `/chunk`エンドポイントは1回のリクエストで1チャンクのみ処理・返却 - **クライアント側結合**: 全チャンクを受信後、クライアントが`np.concatenate()`等で結合 - **サーバー側クロスフェード**: `overlap_ms`で指定した重複部分を自動的にクロスフェード - **セッション状態**: 参照音声の特徴量、前回チャンクの末尾を保持 --- ## エンドポイント仕様 ### 1. GET /health ヘルスチェック用エンドポイント **リクエスト** ```bash GET /health ``` **レスポンス** ```json { "status": "ok" } ``` --- ### 2. POST /session 新しい変換セッションを作成 **リクエスト** ```bash POST /session Content-Type: application/json { "sample_rate": 16000, "tgt_speaker_id": null, "ref_preset_id": null, "use_uploaded_ref": true, "chunk_len_ms": 500, "overlap_ms": 100 } ``` **パラメータ** | フィールド | 型 | 必須 | デフォルト | 説明 | |-----------|-----|------|-----------|------| | `sample_rate` | int | No | 16000 | 入力音声のサンプルレート (Hz) | | `tgt_speaker_id` | str | No | null | ターゲット話者ID (未使用) | | `ref_preset_id` | str | No | "default_female" | プリセット参照音声ID ("default_female", "default_male") | | `use_uploaded_ref` | bool | No | false | 参照音声をアップロードする場合true。falseの場合はref_preset_idを使用 | | `chunk_len_ms` | int | No | 1000 | チャンク長 (ミリ秒) | | `overlap_ms` | int | No | 200 | チャンク間のオーバーラップ (ミリ秒) | **レスポンス** ```json { "session_id": "550e8400-e29b-41d4-a716-446655440000", "sample_rate": 16000, "chunk_len_ms": 500, "overlap_ms": 100 } ``` --- ### 3. POST /session/ref 参照音声(ターゲット話者音声)をアップロード **リクエスト** ```bash POST /session/ref Content-Type: multipart/form-data session_id: ref_audio: ``` **パラメータ** | フィールド | 型 | 必須 | 説明 | |-----------|-----|------|------| | `session_id` | str | Yes | セッションID | | `ref_audio` | file | Yes | 参照音声WAVファイル (任意のサンプルレート、自動リサンプル) | **レスポンス** ```json { "status": "ok" } ``` **処理内容** - 参照音声を22050Hzにリサンプル - 最大25秒に切り詰め - Whisperセマンティック特徴量を抽出 - CAMPPlusスタイル埋め込みを計算 - メルスペクトログラムを生成 - セッションに紐付けて保存 --- ### 4. POST /chunk 音声チャンクを変換 **リクエスト** ```bash POST /chunk Content-Type: multipart/form-data session_id: chunk_id: audio: ``` **パラメータ** | フィールド | 型 | 必須 | 説明 | |-----------|-----|------|------| | `session_id` | str | Yes | セッションID | | `chunk_id` | int | Yes | チャンクID (0始まりの連番) | | `audio` | file | Yes | 音声チャンクWAVファイル | **レスポンス** ``` Content-Type: audio/wav X-Chunk-Id: ``` **処理フロー** 1. 音声チャンクを読み込み (セッションのsample_rateと一致確認) 2. Seed-VCで音声変換 - Whisperセマンティック特徴抽出 - Length Regulator適用 - CFM (Conditional Flow Matching) で推論 - BigVGAN Vocoderで音声生成 3. 前回チャンクの末尾とクロスフェード (`overlap_ms`分) 4. 変換後チャンクを返却 (22050Hz WAV) **重要**: このエンドポイントは**1チャンクのみ**を返します。全体音声を得るにはクライアント側で結合が必要です。 --- ### 5. POST /end セッションを終了 **リクエスト** ```bash POST /end Content-Type: application/json { "session_id": "" } ``` **レスポンス** ```json { "status": "ended" } ``` --- ## 使用例 ### Python完全実装例 #### パターンA: プリセット参照音声を使用(推奨) ```python import requests import numpy as np import soundfile as sf import io # ==================== # 設定 # ==================== API_BASE = "https://akatuki25-seed-vc-streaming.hf.space" SOURCE_AUDIO = "source.wav" # 変換したい音声 OUTPUT_AUDIO = "output.wav" SAMPLE_RATE = 16000 CHUNK_LEN_MS = 500 OVERLAP_MS = 100 # ==================== # 1. 音声読み込み # ==================== source, sr = sf.read(SOURCE_AUDIO) if sr != SAMPLE_RATE: import librosa source = librosa.resample(source, orig_sr=sr, target_sr=SAMPLE_RATE) # ==================== # 2. セッション作成(プリセット参照音声使用) # ==================== resp = requests.post(f"{API_BASE}/session", json={ "sample_rate": SAMPLE_RATE, "use_uploaded_ref": False, # プリセットを使用 "ref_preset_id": "default_female", # 省略可(デフォルト) "chunk_len_ms": CHUNK_LEN_MS, "overlap_ms": OVERLAP_MS }) session_id = resp.json()["session_id"] print(f"Session created: {session_id}") # 3. 参照音声アップロードは不要(プリセット使用時) # ==================== # 4. チャンク分割 # ==================== chunk_len_samples = int(SAMPLE_RATE * CHUNK_LEN_MS / 1000) chunks = [] for i in range(0, len(source), chunk_len_samples): chunk = source[i:i + chunk_len_samples] chunks.append(chunk) print(f"Split into {len(chunks)} chunks") # ==================== # 5. チャンク順次送信・受信 # ==================== output_chunks = [] for chunk_id, chunk in enumerate(chunks): # WAVバイト列に変換 buffer = io.BytesIO() sf.write(buffer, chunk, SAMPLE_RATE, format="WAV", subtype="PCM_16") buffer.seek(0) # POSTリクエスト resp = requests.post(f"{API_BASE}/chunk", data={"session_id": session_id, "chunk_id": chunk_id}, files={"audio": ("chunk.wav", buffer, "audio/wav")}) # 変換後チャンク取得 converted_chunk, conv_sr = sf.read(io.BytesIO(resp.content)) output_chunks.append(converted_chunk) print(f"Chunk {chunk_id}/{len(chunks)-1} processed") # ==================== # 6. チャンク結合 # ==================== output_audio = np.concatenate(output_chunks) sf.write(OUTPUT_AUDIO, output_audio, 22050) print(f"Output saved: {OUTPUT_AUDIO}") # ==================== # 7. セッション終了 # ==================== requests.post(f"{API_BASE}/end", json={"session_id": session_id}) print("Session ended") ``` #### パターンB: カスタム参照音声をアップロード ```python import requests import numpy as np import soundfile as sf import io # ==================== # 設定 # ==================== API_BASE = "https://akatuki25-seed-vc-streaming.hf.space" SOURCE_AUDIO = "source.wav" # 変換したい音声 REF_AUDIO = "target_speaker.wav" # ターゲット話者の参照音声 OUTPUT_AUDIO = "output.wav" SAMPLE_RATE = 16000 CHUNK_LEN_MS = 500 OVERLAP_MS = 100 # ==================== # 1. 音声読み込み # ==================== source, sr = sf.read(SOURCE_AUDIO) if sr != SAMPLE_RATE: import librosa source = librosa.resample(source, orig_sr=sr, target_sr=SAMPLE_RATE) # ==================== # 2. セッション作成(カスタム参照音声) # ==================== resp = requests.post(f"{API_BASE}/session", json={ "sample_rate": SAMPLE_RATE, "use_uploaded_ref": True, # カスタム参照音声を使用 "chunk_len_ms": CHUNK_LEN_MS, "overlap_ms": OVERLAP_MS }) session_id = resp.json()["session_id"] print(f"Session created: {session_id}") # ==================== # 3. 参照音声アップロード # ==================== with open(REF_AUDIO, "rb") as f: resp = requests.post(f"{API_BASE}/session/ref", data={"session_id": session_id}, files={"ref_audio": f}) print("Reference audio uploaded") # 4〜7は同じ (チャンク分割、送信、結合、終了) # ... ``` ### curlを使った例 #### パターンA: プリセット参照音声を使用 ```bash #!/bin/bash API_BASE="https://akatuki25-seed-vc-streaming.hf.space" # 1. セッション作成(プリセット参照音声使用) SESSION=$(curl -s -X POST "$API_BASE/session" \ -H "Content-Type: application/json" \ -d '{"sample_rate":16000,"use_uploaded_ref":false,"ref_preset_id":"default_female","chunk_len_ms":500,"overlap_ms":100}' \ | jq -r '.session_id') echo "Session: $SESSION" # 2. 参照音声アップロードは不要 # 3. チャンク送信 (例: chunk_0) curl -X POST "$API_BASE/chunk" \ -F "session_id=$SESSION" \ -F "chunk_id=0" \ -F "audio=@chunk_0.wav" \ -o output_chunk_0.wav # 4. セッション終了 curl -X POST "$API_BASE/end" \ -H "Content-Type: application/json" \ -d "{\"session_id\":\"$SESSION\"}" ``` #### パターンB: カスタム参照音声をアップロード ```bash #!/bin/bash API_BASE="https://akatuki25-seed-vc-streaming.hf.space" # 1. セッション作成 SESSION=$(curl -s -X POST "$API_BASE/session" \ -H "Content-Type: application/json" \ -d '{"sample_rate":16000,"use_uploaded_ref":true,"chunk_len_ms":500,"overlap_ms":100}' \ | jq -r '.session_id') echo "Session: $SESSION" # 2. 参照音声アップロード curl -X POST "$API_BASE/session/ref" \ -F "session_id=$SESSION" \ -F "ref_audio=@target_speaker.wav" # 3. チャンク送信 (例: chunk_0) curl -X POST "$API_BASE/chunk" \ -F "session_id=$SESSION" \ -F "chunk_id=0" \ -F "audio=@chunk_0.wav" \ -o output_chunk_0.wav # 4. セッション終了 curl -X POST "$API_BASE/end" \ -H "Content-Type: application/json" \ -d "{\"session_id\":\"$SESSION\"}" ``` --- ## クロスフェード処理 サーバー側で自動的に処理されます。 ### 仕組み ``` チャンク0: [=============================] ↓ overlap_ms (100ms) チャンク1: [=============================] |<-fade->| 出力0: [========================] (fade-outなし) 出力1: [==|fade-in|==================] 最終結合: [========================================] ``` ### パラメータ調整 | overlap_ms | 効果 | 推奨用途 | |-----------|------|---------| | 0 | クロスフェードなし | デバッグ用 | | 50 | 最小限の平滑化 | 超低レイテンシ優先 | | 100 | 標準 | バランス型 | | 200 | 高品質 | 音質優先 | --- ## パフォーマンス特性 ### レイテンシ測定結果 **環境**: Hugging Face Spaces (NVIDIA T4 GPU) | チャンクサイズ | 初回処理時間 | 2回目以降 | RTF (Real-Time Factor) | |--------------|-------------|----------|----------------------| | 100ms | ~2.0秒 | ~0.5秒 | ~5.0x | | 200ms | ~2.0秒 | ~0.7秒 | ~3.5x | | 500ms | ~2.0秒 | ~1.0秒 | ~2.0x | | 1000ms | ~2.5秒 | ~1.5秒 | ~1.5x | **RTF**: レイテンシ ÷ 入力音声長。1.0未満でリアルタイム処理可能。 ### 推奨設定 ```json { "chunk_len_ms": 500, "overlap_ms": 100 } ``` **理由**: - 初回ウォームアップ後、RTF ~2.0x (実用的) - 適度なクロスフェード品質 - ネットワークオーバーヘッドとのバランス --- ## エラーハンドリング ### HTTP 400 エラー ```json { "detail": "Invalid session_id" } ``` **原因**: - セッションIDが存在しない - セッションが期限切れ (600秒無操作) **対処**: 新しいセッションを作成 --- ```json { "detail": "Sample rate mismatch: expected 16000, got 44100" } ``` **原因**: チャンクのサンプルレートがセッション作成時と異なる **対処**: 音声を正しいサンプルレートにリサンプル --- ### HTTP 500 エラー **原因**: サーバー内部エラー (モデル推論失敗等) **対処**: 1. チャンク長を変更して再試行 2. 参照音声を別のものに変更 3. 数秒待ってリトライ --- ## ベストプラクティス ### 1. 参照音声の選び方 #### プリセット参照音声を使う場合(推奨) ```python # デフォルトプリセット使用(最も簡単) resp = requests.post(f"{API_BASE}/session", json={ "sample_rate": 16000, "use_uploaded_ref": False # プリセット使用 }) # または明示的に指定 resp = requests.post(f"{API_BASE}/session", json={ "sample_rate": 16000, "use_uploaded_ref": False, "ref_preset_id": "default_female" # or "default_male" }) ``` **メリット**: - アップロード不要で即座に利用可能 - 安定した品質の参照音声 - ネットワーク帯域を節約 #### カスタム参照音声をアップロードする場合 - **長さ**: 3〜10秒推奨 (最大25秒まで自動切り詰め) - **品質**: クリーンな音声 (ノイズ・エコー少ない) - **内容**: 単一話者、自然な発話 ### 2. チャンク分割 ```python # ❌ 悪い例: オーバーラップ考慮なし chunks = [audio[i:i+chunk_len] for i in range(0, len(audio), chunk_len)] # ✅ 良い例: オーバーラップなし(サーバー側で処理) chunk_len_samples = int(SAMPLE_RATE * CHUNK_LEN_MS / 1000) chunks = [audio[i:i+chunk_len_samples] for i in range(0, len(audio), chunk_len_samples)] ``` **重要**: クライアント側でオーバーラップを持たせる必要はありません。サーバーが前回チャンクの末尾を保持してクロスフェード処理します。 ### 3. セッション管理 ```python # セッション再利用(同一話者の複数音声変換) for source_file in source_files: # チャンク処理... pass # 最後に1回だけ終了 requests.post(f"{API_BASE}/end", json={"session_id": session_id}) ``` ### 4. エラーリトライ ```python import time MAX_RETRIES = 3 for attempt in range(MAX_RETRIES): try: resp = requests.post(f"{API_BASE}/chunk", ...) resp.raise_for_status() break except requests.RequestException as e: if attempt == MAX_RETRIES - 1: raise time.sleep(2 ** attempt) # Exponential backoff ``` --- ## 技術詳細 ### モデルコンポーネント 1. **Whisper (semantic feature extractor)** - 入力: 16kHz音声 - 出力: セマンティック特徴量 2. **CAMPPlus (speaker encoder)** - 入力: 16kHz音声のFbank特徴量 - 出力: 話者埋め込みベクトル 3. **DiT-based Flow Matching Model** - 入力: セマンティック特徴 + 話者埋め込み - 出力: メルスペクトログラム - 推論ステップ数: 10 - CFG rate: 0.7 4. **BigVGAN Vocoder** - 入力: メルスペクトログラム - 出力: 22050Hz音声波形 ### サンプルレート変換フロー ``` 入力音声 (16kHz) ↓ Seed-VC内部リサンプル (22050Hz) ↓ Whisper用ダウンサンプル (16kHz) ↓ 推論処理 (22050Hz mel) ↓ Vocoder出力 (22050Hz) ``` --- ## 制限事項 1. **リアルタイム性**: GPU環境でもRTF > 1.0 (完全なリアルタイム処理は不可) 2. **セッションタイムアウト**: 600秒無操作で自動削除 3. **参照音声長**: 最大25秒まで 4. **同時セッション数**: Hugging Face Spacesの制限に依存 5. **GPU必須**: CPU環境ではRTF 20〜60x (実用不可) --- ## FAQ ### Q: チャンクサイズを小さくすればレイテンシは下がる? A: 初回コールドスタートのオーバーヘッド(~2秒)が支配的なため、100ms以下にしても劇的な改善はありません。500msが推奨です。 ### Q: クライアント側でクロスフェードする必要は? A: 不要です。サーバーが`overlap_ms`に基づいて自動処理します。受信したチャンクをそのまま結合してください。 ### Q: 複数セッションを同時に使える? A: 可能ですが、各セッションは独立してGPUメモリを消費します。Hugging Face Spacesの無料枠では同時1〜2セッションが現実的です。 ### Q: CPUモードで動作する? A: 動作しますが、RTF 20〜60xと実用的ではありません。GPU環境必須です。 --- ## サポート・問い合わせ - **リポジトリ**: https://huggingface.co/spaces/akatukiseed/seed-vc-streaming - **ベースモデル**: https://github.com/Plachtaa/seed-vc - **Hugging Face Space**: https://akatukiseed-seed-vc-streaming.hf.space --- ## 変更履歴 | バージョン | 日付 | 変更内容 | |----------|------|---------| | 1.0.0 | 2025-11-22 | 初版リリース |