.
This commit is contained in:
+5
-1
@@ -37,6 +37,8 @@ class BaseSTT(ABC):
|
||||
audio_bytes: bytes,
|
||||
*,
|
||||
keyterms: Sequence[str] | None = None,
|
||||
language_code: str | None = None,
|
||||
force_batch: bool = False,
|
||||
) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -88,8 +90,10 @@ class MockSTT(BaseSTT):
|
||||
audio_bytes: bytes,
|
||||
*,
|
||||
keyterms: Sequence[str] | None = None,
|
||||
language_code: str | None = None,
|
||||
force_batch: bool = False,
|
||||
) -> str:
|
||||
del keyterms
|
||||
del keyterms, language_code, force_batch
|
||||
await asyncio.sleep(self._latency_ms / 1000.0)
|
||||
self._call_count += 1
|
||||
if self._scripted_transcripts:
|
||||
|
||||
+79
-24
@@ -52,6 +52,42 @@ def _normalize_keyterms(
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_elevenlabs_language_code(language_code: str | None) -> str | None:
|
||||
normalized = str(language_code or "").strip()
|
||||
if not normalized:
|
||||
return None
|
||||
lowered = normalized.lower().replace("_", "-")
|
||||
mapping = {
|
||||
"kz": "kaz",
|
||||
"kk": "kaz",
|
||||
"kaz": "kaz",
|
||||
"kk-kz": "kaz",
|
||||
"kz-kz": "kaz",
|
||||
"ru": "rus",
|
||||
"rus": "rus",
|
||||
"ru-ru": "rus",
|
||||
}
|
||||
return mapping.get(lowered, normalized)
|
||||
|
||||
|
||||
def _normalize_yandex_language_code(language_code: str | None) -> str | None:
|
||||
normalized = str(language_code or "").strip()
|
||||
if not normalized:
|
||||
return None
|
||||
lowered = normalized.lower().replace("_", "-")
|
||||
mapping = {
|
||||
"ru": "ru-RU",
|
||||
"rus": "ru-RU",
|
||||
"ru-ru": "ru-RU",
|
||||
"kk": "kk-KZ",
|
||||
"kaz": "kk-KZ",
|
||||
"kz": "kk-KZ",
|
||||
"kk-kz": "kk-KZ",
|
||||
"kz-kz": "kk-KZ",
|
||||
}
|
||||
return mapping.get(lowered, normalized)
|
||||
|
||||
|
||||
def _api_base() -> str:
|
||||
return (os.getenv("ELEVENLABS_API_BASE", "https://api.elevenlabs.io").strip() or "https://api.elevenlabs.io").rstrip("/")
|
||||
|
||||
@@ -107,18 +143,7 @@ def _yandex_language() -> str:
|
||||
or os.getenv("AI_VOICE_ASR_YANDEX_LANGUAGE", "").strip()
|
||||
or "ru-RU"
|
||||
)
|
||||
normalized = raw.lower().replace("_", "-")
|
||||
mapping = {
|
||||
"ru": "ru-RU",
|
||||
"rus": "ru-RU",
|
||||
"ru-ru": "ru-RU",
|
||||
"kk": "kk-KZ",
|
||||
"kaz": "kk-KZ",
|
||||
"kz": "kk-KZ",
|
||||
"kk-kz": "kk-KZ",
|
||||
"kz-kz": "kk-KZ",
|
||||
}
|
||||
return mapping.get(normalized, raw)
|
||||
return _normalize_yandex_language_code(raw) or "ru-RU"
|
||||
|
||||
|
||||
def _yandex_topic() -> str:
|
||||
@@ -562,21 +587,26 @@ class ElevenLabsSTT(BaseSTT):
|
||||
audio_bytes: bytes,
|
||||
*,
|
||||
keyterms: Sequence[str] | None = None,
|
||||
language_code: str | None = None,
|
||||
force_batch: bool = False,
|
||||
) -> str:
|
||||
if not audio_bytes:
|
||||
return ""
|
||||
if not self._api_key:
|
||||
raise RuntimeError("ELEVENLABS_API_KEY is required for ElevenLabs STT")
|
||||
|
||||
effective_language_code = _normalize_elevenlabs_language_code(language_code) or self._language_code
|
||||
pcm_bytes, sample_rate_hz = self._extract_pcm(audio_bytes)
|
||||
LOGGER.info(
|
||||
"ElevenLabs STT transcribe start: input_bytes=%s extracted_pcm_bytes=%s sample_rate=%s duration_ms=%s "
|
||||
"use_realtime=%s keyterms=%s",
|
||||
"use_realtime=%s force_batch=%s language=%s keyterms=%s",
|
||||
len(audio_bytes),
|
||||
len(pcm_bytes),
|
||||
sample_rate_hz,
|
||||
_pcm_duration_ms(pcm_bytes, sample_rate_hz=sample_rate_hz),
|
||||
self._use_realtime,
|
||||
force_batch,
|
||||
effective_language_code,
|
||||
len(keyterms) if keyterms else 0,
|
||||
)
|
||||
if sample_rate_hz != self._target_sample_rate_hz:
|
||||
@@ -596,12 +626,13 @@ class ElevenLabsSTT(BaseSTT):
|
||||
len(pcm_bytes),
|
||||
)
|
||||
|
||||
if self._use_realtime:
|
||||
if self._use_realtime and not force_batch:
|
||||
try:
|
||||
transcript = await self._transcribe_realtime(
|
||||
pcm_bytes=pcm_bytes,
|
||||
sample_rate_hz=sample_rate_hz,
|
||||
keyterms=keyterms,
|
||||
language_code=effective_language_code,
|
||||
)
|
||||
LOGGER.info(
|
||||
"ElevenLabs STT realtime transcript result: chars=%s transcript=%r",
|
||||
@@ -618,6 +649,7 @@ class ElevenLabsSTT(BaseSTT):
|
||||
pcm_bytes=pcm_bytes,
|
||||
sample_rate_hz=sample_rate_hz,
|
||||
keyterms=keyterms,
|
||||
language_code=effective_language_code,
|
||||
)
|
||||
LOGGER.info(
|
||||
"ElevenLabs STT batch transcript result: chars=%s transcript=%r",
|
||||
@@ -678,6 +710,7 @@ class ElevenLabsSTT(BaseSTT):
|
||||
pcm_bytes: bytes,
|
||||
sample_rate_hz: int,
|
||||
keyterms: Sequence[str] | None = None,
|
||||
language_code: str | None = None,
|
||||
) -> str:
|
||||
try:
|
||||
from websockets.exceptions import WebSocketException
|
||||
@@ -688,6 +721,7 @@ class ElevenLabsSTT(BaseSTT):
|
||||
websocket_url = self._build_realtime_websocket_url(
|
||||
sample_rate_hz=sample_rate_hz,
|
||||
keyterms=keyterms,
|
||||
language_code=language_code,
|
||||
)
|
||||
chunk_bytes = max(int(sample_rate_hz * self._realtime_chunk_duration_ms / 1000.0) * 2, 320)
|
||||
started_monotonic = time.perf_counter()
|
||||
@@ -790,6 +824,7 @@ class ElevenLabsSTT(BaseSTT):
|
||||
pcm_bytes: bytes,
|
||||
sample_rate_hz: int,
|
||||
keyterms: Sequence[str] | None = None,
|
||||
language_code: str | None = None,
|
||||
) -> str:
|
||||
# TODO: Architectural Bottleneck: Рассмотреть замену STT на Deepgram WebSocket API для достижения true-streaming latency.
|
||||
try:
|
||||
@@ -817,15 +852,16 @@ class ElevenLabsSTT(BaseSTT):
|
||||
len(pcm_bytes),
|
||||
len(wav_bytes),
|
||||
_pcm_duration_ms(pcm_bytes, sample_rate_hz=sample_rate_hz),
|
||||
self._language_code,
|
||||
language_code or self._language_code,
|
||||
len(normalized_keyterms),
|
||||
)
|
||||
form = aiohttp.FormData()
|
||||
form.add_field("model_id", self._model_id)
|
||||
form.add_field("timestamps_granularity", "none")
|
||||
form.add_field("diarize", "false")
|
||||
if self._language_code:
|
||||
form.add_field("language_code", self._language_code)
|
||||
effective_language_code = language_code or self._language_code
|
||||
if effective_language_code:
|
||||
form.add_field("language_code", effective_language_code)
|
||||
if normalized_keyterms:
|
||||
form.add_field("keyterms", json.dumps(normalized_keyterms, ensure_ascii=False))
|
||||
form.add_field(
|
||||
@@ -927,6 +963,7 @@ class ElevenLabsSTT(BaseSTT):
|
||||
*,
|
||||
sample_rate_hz: int,
|
||||
keyterms: Sequence[str] | None = None,
|
||||
language_code: str | None = None,
|
||||
) -> str:
|
||||
query: list[tuple[str, str]] = [
|
||||
("model_id", self._realtime_model_id),
|
||||
@@ -934,8 +971,9 @@ class ElevenLabsSTT(BaseSTT):
|
||||
("commit_strategy", "manual"),
|
||||
("include_timestamps", "false"),
|
||||
]
|
||||
if self._language_code:
|
||||
query.append(("language_code", self._language_code))
|
||||
effective_language_code = language_code or self._language_code
|
||||
if effective_language_code:
|
||||
query.append(("language_code", effective_language_code))
|
||||
for term in _normalize_keyterms(
|
||||
keyterms,
|
||||
max_count=_REALTIME_MAX_KEYTERMS,
|
||||
@@ -1004,20 +1042,25 @@ class YandexSpeechKitSTT(BaseSTT):
|
||||
audio_bytes: bytes,
|
||||
*,
|
||||
keyterms: Sequence[str] | None = None,
|
||||
language_code: str | None = None,
|
||||
force_batch: bool = False,
|
||||
) -> str:
|
||||
del keyterms
|
||||
del keyterms, force_batch
|
||||
if not audio_bytes:
|
||||
return ""
|
||||
if not self._api_key and not self._iam_token:
|
||||
raise RuntimeError("YANDEX_STT_API_KEY or YANDEX_STT_IAM_TOKEN is required for Yandex STT")
|
||||
|
||||
effective_language = _normalize_yandex_language_code(language_code) or self._language
|
||||
pcm_bytes, sample_rate_hz = self._extract_pcm(audio_bytes)
|
||||
LOGGER.info(
|
||||
"Yandex SpeechKit STT transcribe start: input_bytes=%s extracted_pcm_bytes=%s sample_rate=%s duration_ms=%s",
|
||||
"Yandex SpeechKit STT transcribe start: input_bytes=%s extracted_pcm_bytes=%s sample_rate=%s "
|
||||
"duration_ms=%s language=%s",
|
||||
len(audio_bytes),
|
||||
len(pcm_bytes),
|
||||
sample_rate_hz,
|
||||
_pcm_duration_ms(pcm_bytes, sample_rate_hz=sample_rate_hz),
|
||||
effective_language,
|
||||
)
|
||||
if sample_rate_hz != self._target_sample_rate_hz:
|
||||
before_rate_hz = sample_rate_hz
|
||||
@@ -1038,7 +1081,7 @@ class YandexSpeechKitSTT(BaseSTT):
|
||||
|
||||
session = await self._get_client_session()
|
||||
params = {
|
||||
"lang": self._language,
|
||||
"lang": effective_language,
|
||||
"topic": self._topic,
|
||||
"format": "lpcm",
|
||||
"sampleRateHertz": str(sample_rate_hz),
|
||||
@@ -1145,16 +1188,28 @@ class FallbackSTT(BaseSTT):
|
||||
audio_bytes: bytes,
|
||||
*,
|
||||
keyterms: Sequence[str] | None = None,
|
||||
language_code: str | None = None,
|
||||
force_batch: bool = False,
|
||||
) -> str:
|
||||
try:
|
||||
return await self._primary.transcribe(audio_bytes, keyterms=keyterms)
|
||||
return await self._primary.transcribe(
|
||||
audio_bytes,
|
||||
keyterms=keyterms,
|
||||
language_code=language_code,
|
||||
force_batch=force_batch,
|
||||
)
|
||||
except Exception:
|
||||
LOGGER.exception(
|
||||
"Primary STT provider failed; falling back: primary=%s fallback=%s",
|
||||
type(self._primary).__name__,
|
||||
type(self._fallback).__name__,
|
||||
)
|
||||
return await self._fallback.transcribe(audio_bytes, keyterms=keyterms)
|
||||
return await self._fallback.transcribe(
|
||||
audio_bytes,
|
||||
keyterms=keyterms,
|
||||
language_code=language_code,
|
||||
force_batch=force_batch,
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
for provider in (self._primary, self._fallback):
|
||||
|
||||
+19
-4
@@ -15,6 +15,18 @@ from realtime_voice_service.providers.base import BaseSTT
|
||||
LOGGER = logging.getLogger("uvicorn.error")
|
||||
|
||||
|
||||
def _normalize_openai_language_code(language_code: str | None) -> str | None:
|
||||
normalized = str(language_code or "").strip()
|
||||
if not normalized:
|
||||
return None
|
||||
lowered = normalized.lower()
|
||||
if lowered in {"kz", "kk", "kaz", "kk-kz"}:
|
||||
return "kk"
|
||||
if lowered in {"ru", "rus", "ru-ru"}:
|
||||
return "ru"
|
||||
return normalized
|
||||
|
||||
|
||||
def _timeout_seconds() -> float:
|
||||
raw = os.getenv("OPENAI_STT_TIMEOUT_SECONDS") or os.getenv("OPENAI_TIMEOUT_SECONDS")
|
||||
if raw is None:
|
||||
@@ -100,13 +112,16 @@ class OpenAISTT(BaseSTT):
|
||||
audio_bytes: bytes,
|
||||
*,
|
||||
keyterms: Sequence[str] | None = None,
|
||||
language_code: str | None = None,
|
||||
force_batch: bool = False,
|
||||
) -> str:
|
||||
del keyterms
|
||||
del keyterms, force_batch
|
||||
if not audio_bytes:
|
||||
return ""
|
||||
if not self._api_key:
|
||||
raise RuntimeError("OPENAI_API_KEY is required for OpenAI STT")
|
||||
|
||||
effective_language = _normalize_openai_language_code(language_code) or self._language
|
||||
wav_bytes = _pcm16le_to_wav_bytes(audio_bytes, sample_rate_hz=self._input_sample_rate_hz)
|
||||
audio_file = io.BytesIO(wav_bytes)
|
||||
audio_file.name = "utterance.wav"
|
||||
@@ -118,7 +133,7 @@ class OpenAISTT(BaseSTT):
|
||||
len(audio_bytes),
|
||||
len(wav_bytes),
|
||||
self._input_sample_rate_hz,
|
||||
self._language,
|
||||
effective_language,
|
||||
_preview_text(self._prompt),
|
||||
)
|
||||
|
||||
@@ -129,8 +144,8 @@ class OpenAISTT(BaseSTT):
|
||||
}
|
||||
if self._prompt:
|
||||
request["prompt"] = self._prompt
|
||||
if self._language:
|
||||
request["language"] = self._language
|
||||
if effective_language:
|
||||
request["language"] = effective_language
|
||||
|
||||
try:
|
||||
response = await self._get_client().audio.transcriptions.create(**request)
|
||||
|
||||
Reference in New Issue
Block a user