main
py 425 lines 13.3 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, TranscriptionResponseFormat
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 TranscriptionResponseStreamChoice(OpenAIBaseModel):
38 delta: DeltaMessage
39 finish_reason: str | None = None
40 stop_reason: int | str | None = None
41
42
43 class TranscriptionStreamResponse(OpenAIBaseModel):
44 id: str = Field(default_factory=lambda: f"trsc-{random_uuid()}")
45 object: Literal["transcription.chunk"] = "transcription.chunk"
46 created: int = Field(default_factory=lambda: int(time.time()))
47 model: str
48 choices: list[TranscriptionResponseStreamChoice]
49 usage: UsageInfo | None = Field(default=None)
50
51
52 class TranscriptionRequest(OpenAIBaseModel):
53 # Ordered by official OpenAI API documentation
54 # https://platform.openai.com/docs/api-reference/audio/createTranscription
55
56 file: UploadFile
57 """
58 The audio file object (not file name) to transcribe, 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 language: str | None = None
67 """The language of the input audio.
68
69 Supplying the input language in
70 [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format
71 will improve accuracy and latency.
72 """
73
74 hotwords: str | None = None
75 """
76 hotwords refers to a list of important words or phrases that the model
77 should pay extra attention to during transcription.
78 """
79
80 prompt: str = Field(default="")
81 """An optional text to guide the model's style or continue a previous audio
82 segment.
83
84 The [prompt](https://platform.openai.com/docs/guides/speech-to-text#prompting)
85 should match the audio language.
86 """
87
88 response_format: TranscriptionResponseFormat = Field(default="json")
89 """
90 The format of the output, in one of these options: `json`, `text`, `srt`,
91 `verbose_json`, or `vtt`.
92 """
93
94 ## TODO (varun) : Support if set to 0, certain thresholds are met !!
95
96 timestamp_granularities: list[Literal["word", "segment"]] = Field(
97 alias="timestamp_granularities[]", default=[]
98 )
99 """The timestamp granularities to populate for this transcription.
100
101 `response_format` must be set `verbose_json` to use timestamp granularities.
102 Either or both of these options are supported: `word`, or `segment`. Note:
103 There is no additional latency for segment timestamps, but generating word
104 timestamps incurs additional latency.
105 """
106
107 stream: bool | None = False
108 """When set, it will enable output to be streamed in a similar fashion
109 as the Chat Completion endpoint.
110 """
111 # --8<-- [start:transcription-extra-params]
112 # Flattened stream option to simplify form data.
113 stream_include_usage: bool | None = False
114 stream_continuous_usage_stats: bool | None = False
115
116 vllm_xargs: dict[str, str | int | float | list[str | int | float]] | None = Field(
117 default=None,
118 description=(
119 "Additional request parameters with (list of) string or "
120 "numeric values, used by custom extensions."
121 ),
122 )
123 # --8<-- [end:transcription-extra-params]
124
125 to_language: str | None = None
126 """The language of the output audio we transcribe to.
127
128 Please note that this is not currently used by supported models at this
129 time, but it is a placeholder for future use, matching translation api.
130 """
131
132 # --8<-- [start:transcription-sampling-params]
133 use_beam_search: bool = False
134 """Whether or not beam search should be used."""
135
136 n: int = 1
137 """The number of beams to be used in beam search."""
138
139 length_penalty: float = 1.0
140 """Length penalty to be used for beam search."""
141
142 include_stop_str_in_output: bool = False
143 """Whether to include the stop strings in output text."""
144
145 temperature: float = Field(default=0.0)
146 """The sampling temperature, between 0 and 1.
147
148 Higher values like 0.8 will make the output more random, while lower values
149 like 0.2 will make it more focused / deterministic. If set to 0, the model
150 will use [log probability](https://en.wikipedia.org/wiki/Log_probability)
151 to automatically increase the temperature until certain thresholds are hit.
152 """
153
154 top_p: float | None = None
155 """Enables nucleus (top-p) sampling, where tokens are selected from the
156 smallest possible set whose cumulative probability exceeds `p`.
157 """
158
159 top_k: int | None = None
160 """Limits sampling to the `k` most probable tokens at each step."""
161
162 min_p: float | None = None
163 """Filters out tokens with a probability lower than `min_p`, ensuring a
164 minimum likelihood threshold during sampling.
165 """
166
167 seed: int | None = Field(None, ge=_LONG_INFO.min, le=_LONG_INFO.max)
168 """The seed to use for sampling."""
169
170 frequency_penalty: float | None = 0.0
171 """The frequency penalty to use for sampling."""
172
173 repetition_penalty: float | None = None
174 """The repetition penalty to use for sampling."""
175
176 presence_penalty: float | None = 0.0
177 """The presence penalty to use for sampling."""
178
179 max_completion_tokens: int | None = None
180 """The maximum number of tokens to generate."""
181 # --8<-- [end:transcription-sampling-params]
182
183 # Default sampling parameters for transcription requests.
184 _DEFAULT_SAMPLING_PARAMS: dict = {
185 "repetition_penalty": 1.0,
186 "temperature": 1.0,
187 "top_p": 1.0,
188 "top_k": 0,
189 "min_p": 0.0,
190 }
191
192 def build_stt_params(
193 self,
194 audio: "np.ndarray",
195 stt_config: "SpeechToTextConfig",
196 model_config: "ModelConfig",
197 task_type: str,
198 ) -> SpeechToTextParams:
199 return SpeechToTextParams(
200 audio=audio,
201 stt_config=stt_config,
202 model_config=model_config,
203 language=self.language,
204 task_type=task_type,
205 request_prompt=self.prompt,
206 to_language=self.to_language,
207 hotwords=self.hotwords,
208 )
209
210 def to_beam_search_params(
211 self,
212 default_max_tokens: int,
213 default_sampling_params: dict | None = None,
214 ) -> BeamSearchParams:
215 if default_sampling_params is None:
216 default_sampling_params = {}
217
218 max_tokens = default_max_tokens
219 n = self.n if self.n is not None else 1
220
221 # NOTE: Temp 0 is a different fallback than completions
222 if (temperature := self.temperature) is None:
223 temperature = default_sampling_params.get("temperature", 0)
224
225 return BeamSearchParams(
226 beam_width=n,
227 max_tokens=max_tokens,
228 temperature=temperature,
229 length_penalty=self.length_penalty,
230 include_stop_str_in_output=self.include_stop_str_in_output,
231 )
232
233 def to_sampling_params(
234 self, default_max_tokens: int, default_sampling_params: dict | None = None
235 ) -> SamplingParams:
236 max_tokens = default_max_tokens
237
238 if default_sampling_params is None:
239 default_sampling_params = {}
240
241 # Default parameters
242 if (temperature := self.temperature) is None:
243 temperature = default_sampling_params.get(
244 "temperature", self._DEFAULT_SAMPLING_PARAMS["temperature"]
245 )
246 if (top_p := self.top_p) is None:
247 top_p = default_sampling_params.get(
248 "top_p", self._DEFAULT_SAMPLING_PARAMS["top_p"]
249 )
250 if (top_k := self.top_k) is None:
251 top_k = default_sampling_params.get(
252 "top_k", self._DEFAULT_SAMPLING_PARAMS["top_k"]
253 )
254 if (min_p := self.min_p) is None:
255 min_p = default_sampling_params.get(
256 "min_p", self._DEFAULT_SAMPLING_PARAMS["min_p"]
257 )
258
259 if (repetition_penalty := self.repetition_penalty) is None:
260 repetition_penalty = default_sampling_params.get(
261 "repetition_penalty",
262 self._DEFAULT_SAMPLING_PARAMS["repetition_penalty"],
263 )
264
265 return SamplingParams.from_optional(
266 temperature=temperature,
267 max_tokens=max_tokens,
268 seed=self.seed,
269 top_p=top_p,
270 top_k=top_k,
271 min_p=min_p,
272 frequency_penalty=self.frequency_penalty,
273 repetition_penalty=repetition_penalty,
274 presence_penalty=self.presence_penalty,
275 output_kind=RequestOutputKind.DELTA
276 if self.stream
277 else RequestOutputKind.FINAL_ONLY,
278 extra_args=self.vllm_xargs,
279 skip_clone=True, # Created fresh per request, safe to skip clone
280 )
281
282 @model_validator(mode="before")
283 @classmethod
284 def validate_transcription_request(cls, data):
285 if not isinstance(data, dict):
286 return data
287 if isinstance(data.get("file"), str):
288 raise HTTPException(
289 status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
290 detail="Expected 'file' to be a file-like object, not 'str'.",
291 )
292
293 stream_opts = ["stream_include_usage", "stream_continuous_usage_stats"]
294 stream = data.get("stream", False)
295 if any(bool(data.get(so, False)) for so in stream_opts) and not stream:
296 # Find which specific stream option was set
297 invalid_param = next(
298 (so for so in stream_opts if data.get(so, False)),
299 "stream_include_usage",
300 )
301 raise VLLMValidationError(
302 "Stream options can only be defined when `stream=True`.",
303 parameter=invalid_param,
304 )
305
306 # Parse vllm_xargs from JSON string (form data sends it as a string)
307 xargs = data.get("vllm_xargs")
308 if isinstance(xargs, str):
309 try:
310 data["vllm_xargs"] = json.loads(xargs)
311 except json.JSONDecodeError as e:
312 raise VLLMValidationError(
313 f"Failed to parse vllm_xargs. Must be valid JSON: {e}",
314 parameter="vllm_xargs",
315 ) from e
316
317 return data
318
319
320 # Transcription response objects
321 class TranscriptionUsageAudio(OpenAIBaseModel):
322 type: Literal["duration"] = "duration"
323 seconds: int
324
325
326 class TranscriptionResponse(OpenAIBaseModel):
327 text: str
328 """The transcribed text."""
329 usage: TranscriptionUsageAudio
330
331
332 class TranscriptionWord(OpenAIBaseModel):
333 end: float
334 """End time of the word in seconds."""
335
336 start: float
337 """Start time of the word in seconds."""
338
339 word: str
340 """The text content of the word."""
341
342
343 class TranscriptionSegment(OpenAIBaseModel):
344 id: int
345 """Unique identifier of the segment."""
346
347 avg_logprob: float
348 """Average logprob of the segment.
349
350 If the value is lower than -1, consider the logprobs failed.
351 """
352
353 compression_ratio: float
354 """Compression ratio of the segment.
355
356 If the value is greater than 2.4, consider the compression failed.
357 """
358
359 end: float
360 """End time of the segment in seconds."""
361
362 no_speech_prob: float | None = None
363 """Probability of no speech in the segment.
364
365 If the value is higher than 1.0 and the `avg_logprob` is below -1, consider
366 this segment silent.
367 """
368
369 seek: int
370 """Seek offset of the segment."""
371
372 start: float
373 """Start time of the segment in seconds."""
374
375 temperature: float
376 """Temperature parameter used for generating the segment."""
377
378 text: str
379 """Text content of the segment."""
380
381 tokens: list[int]
382 """Array of token IDs for the text content."""
383
384
385 class TranscriptionResponseVerbose(OpenAIBaseModel):
386 duration: float
387 """The duration of the input audio."""
388
389 language: str
390 """The language of the input audio."""
391
392 text: str
393 """The transcribed text."""
394
395 segments: list[TranscriptionSegment] | None = None
396 """Segments of the transcribed text and their corresponding details."""
397
398 words: list[TranscriptionWord] | None = None
399 """Extracted words and their corresponding timestamps."""
400
401
402 class TranscriptionDiarizedSegment(OpenAIBaseModel):
403 """A speaker-attributed transcription segment."""
404
405 type: Literal["transcript.text.segment"] = "transcript.text.segment"
406 id: str
407 start: float
408 end: float
409 text: str
410 speaker: str
411
412
413 class TranscriptionResponseDiarized(OpenAIBaseModel):
414 """OpenAI-compatible diarized transcription response."""
415
416 task: Literal["transcribe"] = "transcribe"
417 duration: float
418 text: str
419 segments: list[TranscriptionDiarizedSegment]
420 usage: TranscriptionUsageAudio
421
422
423 TranscriptionResponseVariant: TypeAlias = (
424 TranscriptionResponse | TranscriptionResponseVerbose | TranscriptionResponseDiarized
425 )