Signed-off-by: Mengjie Zhao <zmj0129@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
169 lines
6.4 KiB
Python
169 lines
6.4 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
"""Gepard-1.0 serving adapter for ``/v1/audio/speech``.
|
|
|
|
Zero-shot only: the trained ``null_prefix`` is the default voice. Prompt IDs
|
|
are assembled with the same layout helper the offline path uses. Frame budget
|
|
rides ``SamplingParams.max_tokens`` (one token = one 1024-sample frame).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from vllm.utils.async_utils import make_async
|
|
|
|
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
|
from vllm_omni.entrypoints.openai.tts_adapters.base import (
|
|
ARTTSAdapter,
|
|
OutputPolicy,
|
|
PreparedRequest,
|
|
apply_max_new_tokens,
|
|
conditioning_cache_salt,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
|
from vllm_omni.model_executor.models.gepard.configuration_gepard import GepardConfig
|
|
|
|
_DEFAULT_VOICE = "default"
|
|
|
|
# Declared schema fields Gepard does not consume. Rejecting them here is
|
|
# load-bearing: the pydantic model silently drops undeclared keys, so a
|
|
# declared-but-ignored field would otherwise succeed and mislead.
|
|
_UNSUPPORTED_OPTIONAL_FIELDS: tuple[tuple[str, str], ...] = (
|
|
("ref_audio", "ref_audio"),
|
|
("ref_text", "ref_text"),
|
|
("ref_audio_2", "ref_audio_2"),
|
|
("speaker_embedding", "speaker_embedding"),
|
|
("x_vector_only_mode", "x_vector_only_mode"),
|
|
("task_type", "task_type"),
|
|
("non_streaming_mode", "non_streaming_mode"),
|
|
("instructions", "instructions"),
|
|
("language", "language"),
|
|
("ambient_sound", "ambient_sound"),
|
|
("duration_seconds", "duration_seconds"),
|
|
("initial_codec_chunk_frames", "initial_codec_chunk_frames"),
|
|
)
|
|
|
|
|
|
@register_tts_adapter
|
|
class GepardAdapter(ARTTSAdapter):
|
|
name = "gepard"
|
|
stage_keys = frozenset({"gepard"})
|
|
model_archs = frozenset({"GepardTalkerForConditionalGeneration"})
|
|
|
|
def __init__(self, ctx) -> None:
|
|
super().__init__(ctx)
|
|
self._tokenizer: Any = None
|
|
self._gepard_config: GepardConfig | None = None
|
|
self._build_prompt_async = make_async(self._build_prompt, executor=getattr(ctx.server, "_tts_executor", None))
|
|
|
|
def _load_supported_speakers(self) -> set[str]:
|
|
return {_DEFAULT_VOICE}
|
|
|
|
def validate(self, request: OpenAICreateSpeechRequest) -> str | None:
|
|
if not request.input or not request.input.strip():
|
|
return "Input text cannot be empty"
|
|
|
|
if request.voice is not None:
|
|
voice = request.voice.lower()
|
|
if voice != _DEFAULT_VOICE:
|
|
return (
|
|
f"Invalid voice '{request.voice}'. Supported: {_DEFAULT_VOICE}. "
|
|
"Voice cloning from reference audio is not available yet."
|
|
)
|
|
|
|
if request.speed is not None and request.speed != 1.0:
|
|
return "Gepard does not support 'speed' adjustments; audio is rendered at the native 22.05 kHz rate"
|
|
|
|
if request.extra_params:
|
|
keys = ", ".join(sorted(request.extra_params))
|
|
return (
|
|
f"Gepard does not accept extra_params (got: {keys}). "
|
|
"Sampling is fixed in-model; temperature/top_p/top_k cannot be overridden."
|
|
)
|
|
|
|
if request.word_timestamps:
|
|
return "Gepard does not support 'word_timestamps'; there is no forced-aligner stage in this pipeline"
|
|
|
|
fmt = (request.response_format or "").lower()
|
|
if fmt == "opus":
|
|
return (
|
|
"Gepard does not support 'response_format'='opus'; "
|
|
"native 22.05 kHz is not an Opus sample rate (8000, 12000, 16000, 24000, 48000)"
|
|
)
|
|
|
|
if request.max_new_tokens is not None:
|
|
if request.max_new_tokens < self.max_new_tokens_min or request.max_new_tokens > self.max_new_tokens_max:
|
|
return f"max_new_tokens must be between {self.max_new_tokens_min} and {self.max_new_tokens_max} frames"
|
|
|
|
for attr, field in _UNSUPPORTED_OPTIONAL_FIELDS:
|
|
if getattr(request, attr, None) is not None:
|
|
return f"Gepard does not support '{field}'"
|
|
|
|
return None
|
|
|
|
def apply_sampling_overrides(
|
|
self,
|
|
sampling_params_list: list,
|
|
request: OpenAICreateSpeechRequest,
|
|
prompt: dict | None = None,
|
|
request_id: str | None = None,
|
|
) -> list:
|
|
return apply_max_new_tokens(sampling_params_list, request)
|
|
|
|
async def build(
|
|
self,
|
|
request: OpenAICreateSpeechRequest,
|
|
sampling_params_list: list,
|
|
has_inline_ref_audio: bool,
|
|
) -> PreparedRequest:
|
|
tts_params: dict = {}
|
|
prompt = await self._build_prompt_async(request.input)
|
|
prompt["cache_salt"] = conditioning_cache_salt(request, tts_params)
|
|
return PreparedRequest(
|
|
prompt=prompt,
|
|
tts_params=tts_params,
|
|
model_type=self.name,
|
|
output_policy=OutputPolicy(accumulate_nonstreaming=True),
|
|
)
|
|
|
|
def _build_prompt(self, text: str) -> dict[str, Any]:
|
|
from vllm_omni.model_executor.models.gepard.prompt import build_gepard_prompt_ids
|
|
|
|
prompt_token_ids = build_gepard_prompt_ids(self._tokenize(text), config=self._config())
|
|
return {
|
|
"prompt_token_ids": prompt_token_ids,
|
|
"additional_information": {"text": [text]},
|
|
}
|
|
|
|
def _model_config(self) -> Any:
|
|
engine = self.ctx.engine_client
|
|
assert engine is not None
|
|
return engine.model_config
|
|
|
|
def _config(self) -> GepardConfig:
|
|
if self._gepard_config is None:
|
|
from vllm_omni.model_executor.models.gepard.configuration_gepard import GepardConfig
|
|
|
|
model_config = self._model_config()
|
|
self._gepard_config = GepardConfig.from_checkpoint(
|
|
model_config.model, revision=getattr(model_config, "revision", None)
|
|
)
|
|
return self._gepard_config
|
|
|
|
def _tokenize(self, text: str) -> list[int]:
|
|
tokenizer = self._tokenizer
|
|
if tokenizer is None:
|
|
from transformers import AutoTokenizer
|
|
|
|
model_config = self._model_config()
|
|
tokenizer = AutoTokenizer.from_pretrained(
|
|
model_config.model,
|
|
revision=getattr(model_config, "revision", None),
|
|
trust_remote_code=True,
|
|
)
|
|
self._tokenizer = tokenizer
|
|
return tokenizer(text, add_special_tokens=False)["input_ids"]
|