seed-vc-streaming / API_SPECIFICATION.md
Akatuki25's picture
Update API spec: add preset reference audio documentation
28892a1

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

ヘルスチェック用エンドポイント

リクエスト

GET /health

レスポンス

{
  "status": "ok"
}

2. POST /session

新しい変換セッションを作成

リクエスト

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 チャンク間のオーバーラップ (ミリ秒)

レスポンス

{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "sample_rate": 16000,
  "chunk_len_ms": 500,
  "overlap_ms": 100
}

3. POST /session/ref

参照音声(ターゲット話者音声)をアップロード

リクエスト

POST /session/ref
Content-Type: multipart/form-data

session_id: <session_id>
ref_audio: <WAVファイル>

パラメータ

フィールド 必須 説明
session_id str Yes セッションID
ref_audio file Yes 参照音声WAVファイル (任意のサンプルレート、自動リサンプル)

レスポンス

{
  "status": "ok"
}

処理内容

  • 参照音声を22050Hzにリサンプル
  • 最大25秒に切り詰め
  • Whisperセマンティック特徴量を抽出
  • CAMPPlusスタイル埋め込みを計算
  • メルスペクトログラムを生成
  • セッションに紐付けて保存

4. POST /chunk

音声チャンクを変換

リクエスト

POST /chunk
Content-Type: multipart/form-data

session_id: <session_id>
chunk_id: <chunk_id>
audio: <WAVファイル>

パラメータ

フィールド 必須 説明
session_id str Yes セッションID
chunk_id int Yes チャンクID (0始まりの連番)
audio file Yes 音声チャンクWAVファイル

レスポンス

Content-Type: audio/wav
X-Chunk-Id: <chunk_id>

<WAVバイナリデータ>

処理フロー

  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

セッションを終了

リクエスト

POST /end
Content-Type: application/json

{
  "session_id": "<session_id>"
}

レスポンス

{
  "status": "ended"
}

使用例

Python完全実装例

パターンA: プリセット参照音声を使用(推奨)

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: カスタム参照音声をアップロード

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: プリセット参照音声を使用

#!/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: カスタム参照音声をアップロード

#!/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未満でリアルタイム処理可能。

推奨設定

{
  "chunk_len_ms": 500,
  "overlap_ms": 100
}

理由:

  • 初回ウォームアップ後、RTF ~2.0x (実用的)
  • 適度なクロスフェード品質
  • ネットワークオーバーヘッドとのバランス

エラーハンドリング

HTTP 400 エラー

{
  "detail": "Invalid session_id"
}

原因:

  • セッションIDが存在しない
  • セッションが期限切れ (600秒無操作)

対処: 新しいセッションを作成


{
  "detail": "Sample rate mismatch: expected 16000, got 44100"
}

原因: チャンクのサンプルレートがセッション作成時と異なる

対処: 音声を正しいサンプルレートにリサンプル


HTTP 500 エラー

原因: サーバー内部エラー (モデル推論失敗等)

対処:

  1. チャンク長を変更して再試行
  2. 参照音声を別のものに変更
  3. 数秒待ってリトライ

ベストプラクティス

1. 参照音声の選び方

プリセット参照音声を使う場合(推奨)

# デフォルトプリセット使用(最も簡単)
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. チャンク分割

# ❌ 悪い例: オーバーラップ考慮なし
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. セッション管理

# セッション再利用(同一話者の複数音声変換)
for source_file in source_files:
    # チャンク処理...
    pass
# 最後に1回だけ終了
requests.post(f"{API_BASE}/end", json={"session_id": session_id})

4. エラーリトライ

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環境必須です。


サポート・問い合わせ


変更履歴

バージョン 日付 変更内容
1.0.0 2025-11-22 初版リリース