mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
Merge fd40828950 into d392e7faae
This commit is contained in:
commit
22df024ecf
2 changed files with 85 additions and 0 deletions
|
|
@ -130,6 +130,8 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
|
|||
if "words" in response_json:
|
||||
response["words"] = []
|
||||
for word_data in response_json["words"]:
|
||||
if not isinstance(word_data, dict):
|
||||
continue
|
||||
# Only include actual words, skip spacing and audio events
|
||||
if word_data.get("type") == "word":
|
||||
response["words"].append(
|
||||
|
|
@ -140,6 +142,21 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
|
|||
}
|
||||
)
|
||||
|
||||
# ElevenLabs does not return a top-level duration; derive it from
|
||||
# the last timestamp so per-second cost tracking
|
||||
# (input_cost_per_second x duration) bills the real audio length
|
||||
# instead of 0.
|
||||
duration = 0.0
|
||||
for word_data in response_json.get("words") or []:
|
||||
if not isinstance(word_data, dict):
|
||||
continue
|
||||
try:
|
||||
duration = max(duration, float(word_data.get("end") or 0.0))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if duration > 0:
|
||||
response["duration"] = duration
|
||||
|
||||
# Store full response in hidden params
|
||||
response._hidden_params = response_json
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../../../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
from litellm.llms.elevenlabs.audio_transcription.transformation import (
|
||||
ElevenLabsAudioTranscriptionConfig,
|
||||
)
|
||||
|
||||
|
||||
def _mock_raw_response(response_json: dict) -> MagicMock:
|
||||
raw = MagicMock()
|
||||
raw.json.return_value = response_json
|
||||
raw.text = str(response_json)
|
||||
return raw
|
||||
|
||||
|
||||
class TestElevenLabsTransformResponse:
|
||||
def test_sets_duration_from_last_timestamp(self):
|
||||
"""Duration must be derived from word timestamps so per-second cost
|
||||
tracking (input_cost_per_second x duration) bills real audio length —
|
||||
without it every ElevenLabs transcription logs $0 spend."""
|
||||
config = ElevenLabsAudioTranscriptionConfig()
|
||||
response = config.transform_audio_transcription_response(
|
||||
_mock_raw_response(
|
||||
{
|
||||
"text": "hello world",
|
||||
"language_code": "en",
|
||||
"words": [
|
||||
{"type": "word", "text": "hello", "start": 0.1, "end": 0.5},
|
||||
{"type": "spacing", "text": " ", "start": 0.5, "end": 0.6},
|
||||
{"type": "word", "text": "world", "start": 0.6, "end": 1.9},
|
||||
{"type": "audio_event", "text": "(door)", "start": 2.0, "end": 3.4},
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert response.text == "hello world"
|
||||
assert response.duration == 3.4
|
||||
|
||||
def test_no_words_leaves_duration_unset(self):
|
||||
config = ElevenLabsAudioTranscriptionConfig()
|
||||
response = config.transform_audio_transcription_response(
|
||||
_mock_raw_response({"text": "hello", "language_code": "en"})
|
||||
)
|
||||
assert getattr(response, "duration", None) is None
|
||||
|
||||
def test_malformed_word_timestamps_do_not_raise(self):
|
||||
config = ElevenLabsAudioTranscriptionConfig()
|
||||
response = config.transform_audio_transcription_response(
|
||||
_mock_raw_response(
|
||||
{
|
||||
"text": "hello",
|
||||
"language_code": "en",
|
||||
"words": [
|
||||
{"type": "word", "text": "hello", "start": 0.0, "end": None},
|
||||
{"type": "word", "text": "x", "start": 0.0, "end": "bad"},
|
||||
None,
|
||||
"not-a-dict",
|
||||
{"type": "word", "text": "y", "start": 0.0, "end": 2.5},
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
assert response.duration == 2.5
|
||||
Loading…
Add table
Reference in a new issue