main
py 381 lines 12 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import json
5 import time
6 from http import HTTPStatus
7 from typing import TYPE_CHECKING, Literal, TypeAlias
8
9 from fastapi import HTTPException, UploadFile
10 from pydantic import (
11 Field,
12 model_validator,
13 )
14
15 from vllm.config.speech_to_text import SpeechToTextParams
16 from vllm.entrypoints.generate.base.protocol import DeltaMessage
17 from vllm.entrypoints.serve.engine.protocol import OpenAIBaseModel, UsageInfo
18 from vllm.exceptions import VLLMValidationError
19 from vllm.logger import init_logger
20 from vllm.sampling_params import (
21 BeamSearchParams,
22 RequestOutputKind,
23 SamplingParams,
24 )
25 from vllm.utils import random_uuid
26
27 from ..base.protocol import _LONG_INFO, AudioResponseFormat
28
29 if TYPE_CHECKING:
30 import numpy as np
31
32 from vllm.config import ModelConfig, SpeechToTextConfig
33
34 logger = init_logger(__name__)
35
36
37 class TranslationResponseStreamChoice(OpenAIBaseModel):
38 delta: DeltaMessage
39 finish_reason: str | None = None
40 stop_reason: int | str | None = None
41
42
43 class TranslationStreamResponse(OpenAIBaseModel):
44 id: str = Field(default_factory=lambda: f"trsl-{random_uuid()}")
45 object: Literal["translation.chunk"] = "translation.chunk"
46 created: int = Field(default_factory=lambda: int(time.time()))
47 model: str
48 choices: list[TranslationResponseStreamChoice]
49 usage: UsageInfo | None = Field(default=None)
50
51
52 class TranslationRequest(OpenAIBaseModel):
53 # Ordered by official OpenAI API documentation
54 # https://platform.openai.com/docs/api-reference/audio/createTranslation
55
56 file: UploadFile
57 """
58 The audio file object (not file name) to translate, in one of these
59 formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
60 """
61
62 model: str | None = None
63 """ID of the model to use.
64 """
65
66 prompt: str = Field(default="")
67 """An optional text to guide the model's style or continue a previous audio
68 segment.
69
70 The [prompt](https://platform.openai.com/docs/guides/speech-to-text#prompting)
71 should match the audio language.
72 """
73
74 response_format: AudioResponseFormat = Field(default="json")
75 """
76 The format of the output, in one of these options: `json`, `text`, `srt`,
77 `verbose_json`, or `vtt`.
78 """
79
80 # --8<-- [start:translation-sampling-params]
81 use_beam_search: bool = False
82 """Whether or not beam search should be used."""
83
84 n: int = 1
85 """The number of beams to be used in beam search."""
86
87 length_penalty: float = 1.0
88 """Length penalty to be used for beam search."""
89
90 include_stop_str_in_output: bool = False
91 """Whether to include the stop strings in output text."""
92
93 seed: int | None = Field(None, ge=_LONG_INFO.min, le=_LONG_INFO.max)
94 """The seed to use for sampling."""
95
96 temperature: float = Field(default=0.0)
97 """The sampling temperature, between 0 and 1.
98
99 Higher values like 0.8 will make the output more random, while lower values
100 like 0.2 will make it more focused / deterministic. If set to 0, the model
101 will use [log probability](https://en.wikipedia.org/wiki/Log_probability)
102 to automatically increase the temperature until certain thresholds are hit.
103 """
104
105 top_p: float | None = None
106 """Enables nucleus (top-p) sampling, where tokens are selected from the
107 smallest possible set whose cumulative probability exceeds `p`.
108 """
109
110 top_k: int | None = None
111 """Limits sampling to the `k` most probable tokens at each step."""
112
113 min_p: float | None = None
114 """Filters out tokens with a probability lower than `min_p`, ensuring a
115 minimum likelihood threshold during sampling.
116 """
117
118 frequency_penalty: float | None = 0.0
119 """The frequency penalty to use for sampling."""
120
121 repetition_penalty: float | None = None
122 """The repetition penalty to use for sampling."""
123
124 presence_penalty: float | None = 0.0
125 """The presence penalty to use for sampling."""
126 # --8<-- [end:translation-sampling-params]
127
128 # --8<-- [start:translation-extra-params]
129 language: str | None = None
130 """The language of the input audio we translate from.
131
132 Supplying the input language in
133 [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format
134 will improve accuracy.
135 """
136
137 hotwords: str | None = None
138 """
139 hotwords refers to a list of important words or phrases that the model
140 should pay extra attention to during transcription.
141 """
142
143 to_language: str | None = None
144 """The language of the input audio we translate to.
145
146 Please note that this is not supported by all models, refer to the specific
147 model documentation for more details.
148 For instance, Whisper only supports `to_language=en`.
149 """
150
151 stream: bool | None = False
152 """Custom field not present in the original OpenAI definition. When set,
153 it will enable output to be streamed in a similar fashion as the Chat
154 Completion endpoint.
155 """
156 # Flattened stream option to simplify form data.
157 stream_include_usage: bool | None = False
158 stream_continuous_usage_stats: bool | None = False
159
160 max_completion_tokens: int | None = None
161 """The maximum number of tokens to generate."""
162
163 vllm_xargs: dict[str, str | int | float | list[str | int | float]] | None = Field(
164 default=None,
165 description=(
166 "Additional request parameters with (list of) string or "
167 "numeric values, used by custom extensions."
168 ),
169 )
170 # --8<-- [end:translation-extra-params]
171
172 # Default sampling parameters for translation requests.
173 _DEFAULT_SAMPLING_PARAMS: dict = {
174 "repetition_penalty": 1.0,
175 "temperature": 0,
176 "top_p": 1.0,
177 "top_k": 0,
178 "min_p": 0.0,
179 }
180
181 def build_stt_params(
182 self,
183 audio: "np.ndarray",
184 stt_config: "SpeechToTextConfig",
185 model_config: "ModelConfig",
186 task_type: str,
187 ) -> SpeechToTextParams:
188 return SpeechToTextParams(
189 audio=audio,
190 stt_config=stt_config,
191 model_config=model_config,
192 language=self.language,
193 task_type=task_type,
194 request_prompt=self.prompt,
195 to_language=self.to_language,
196 hotwords=self.hotwords,
197 )
198
199 def to_beam_search_params(
200 self,
201 default_max_tokens: int,
202 default_sampling_params: dict | None = None,
203 ) -> BeamSearchParams:
204 if default_sampling_params is None:
205 default_sampling_params = {}
206
207 max_tokens = default_max_tokens
208 n = self.n if self.n is not None else 1
209
210 # NOTE: Temp 0 is a different fallback than completions
211 if (temperature := self.temperature) is None:
212 temperature = default_sampling_params.get("temperature", 0)
213
214 return BeamSearchParams(
215 beam_width=n,
216 max_tokens=max_tokens,
217 temperature=temperature,
218 length_penalty=self.length_penalty,
219 include_stop_str_in_output=self.include_stop_str_in_output,
220 )
221
222 def to_sampling_params(
223 self, default_max_tokens: int, default_sampling_params: dict | None = None
224 ) -> SamplingParams:
225 max_tokens = default_max_tokens
226
227 if default_sampling_params is None:
228 default_sampling_params = {}
229 # Default parameters
230 if (temperature := self.temperature) is None:
231 temperature = default_sampling_params.get(
232 "temperature", self._DEFAULT_SAMPLING_PARAMS["temperature"]
233 )
234 if (top_p := self.top_p) is None:
235 top_p = default_sampling_params.get(
236 "top_p", self._DEFAULT_SAMPLING_PARAMS["top_p"]
237 )
238 if (top_k := self.top_k) is None:
239 top_k = default_sampling_params.get(
240 "top_k", self._DEFAULT_SAMPLING_PARAMS["top_k"]
241 )
242 if (min_p := self.min_p) is None:
243 min_p = default_sampling_params.get(
244 "min_p", self._DEFAULT_SAMPLING_PARAMS["min_p"]
245 )
246 if (repetition_penalty := self.repetition_penalty) is None:
247 repetition_penalty = default_sampling_params.get(
248 "repetition_penalty",
249 self._DEFAULT_SAMPLING_PARAMS["repetition_penalty"],
250 )
251
252 return SamplingParams.from_optional(
253 temperature=temperature,
254 max_tokens=max_tokens,
255 seed=self.seed,
256 top_p=top_p,
257 top_k=top_k,
258 min_p=min_p,
259 frequency_penalty=self.frequency_penalty,
260 repetition_penalty=repetition_penalty,
261 presence_penalty=self.presence_penalty,
262 output_kind=RequestOutputKind.DELTA
263 if self.stream
264 else RequestOutputKind.FINAL_ONLY,
265 extra_args=self.vllm_xargs,
266 skip_clone=True, # Created fresh per request, safe to skip clone
267 )
268
269 @model_validator(mode="before")
270 @classmethod
271 def validate_stream_options(cls, data):
272 if not isinstance(data, dict):
273 return data
274 if isinstance(data.get("file"), str):
275 raise HTTPException(
276 status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
277 detail="Expected 'file' to be a file-like object, not 'str'.",
278 )
279 stream_opts = ["stream_include_usage", "stream_continuous_usage_stats"]
280 stream = data.get("stream", False)
281 if any(bool(data.get(so, False)) for so in stream_opts) and not stream:
282 # Find which specific stream option was set
283 invalid_param = next(
284 (so for so in stream_opts if data.get(so, False)),
285 "stream_include_usage",
286 )
287 raise VLLMValidationError(
288 "Stream options can only be defined when `stream=True`.",
289 parameter=invalid_param,
290 )
291
292 xargs = data.get("vllm_xargs")
293 if isinstance(xargs, str):
294 try:
295 data["vllm_xargs"] = json.loads(xargs)
296 except json.JSONDecodeError as e:
297 raise VLLMValidationError(
298 f"Failed to parse vllm_xargs. Must be valid JSON: {e}",
299 parameter="vllm_xargs",
300 ) from e
301
302 return data
303
304
305 # Translation response objects
306 class TranslationResponse(OpenAIBaseModel):
307 text: str
308 """The translated text."""
309
310
311 class TranslationWord(OpenAIBaseModel):
312 end: float
313 """End time of the word in seconds."""
314
315 start: float
316 """Start time of the word in seconds."""
317
318 word: str
319 """The text content of the word."""
320
321
322 class TranslationSegment(OpenAIBaseModel):
323 id: int
324 """Unique identifier of the segment."""
325
326 avg_logprob: float
327 """Average logprob of the segment.
328
329 If the value is lower than -1, consider the logprobs failed.
330 """
331
332 compression_ratio: float
333 """Compression ratio of the segment.
334
335 If the value is greater than 2.4, consider the compression failed.
336 """
337
338 end: float
339 """End time of the segment in seconds."""
340
341 no_speech_prob: float | None = None
342 """Probability of no speech in the segment.
343
344 If the value is higher than 1.0 and the `avg_logprob` is below -1, consider
345 this segment silent.
346 """
347
348 seek: int
349 """Seek offset of the segment."""
350
351 start: float
352 """Start time of the segment in seconds."""
353
354 temperature: float
355 """Temperature parameter used for generating the segment."""
356
357 text: str
358 """Text content of the segment."""
359
360 tokens: list[int]
361 """Array of token IDs for the text content."""
362
363
364 class TranslationResponseVerbose(OpenAIBaseModel):
365 duration: float
366 """The duration of the input audio."""
367
368 language: str
369 """The language of the input audio."""
370
371 text: str
372 """The translated text."""
373
374 segments: list[TranslationSegment] | None = None
375 """Segments of the translated text and their corresponding details."""
376
377 words: list[TranslationWord] | None = None
378 """Extracted words and their corresponding timestamps."""
379
380
381 TranslationResponseVariant: TypeAlias = TranslationResponse | TranslationResponseVerbose