fix: make transcript refinement language-aware

This commit is contained in:
Jamie Pine
2026-07-21 12:35:20 -07:00
parent 52f8d8dd38
commit 7ac663fd0a
20 changed files with 1240 additions and 44 deletions
+38
View File
@@ -21,6 +21,15 @@ import numpy as np
DEFAULT_LLM_MAX_TOKENS = 512
DEFAULT_LLM_TEMPERATURE = 0.7
@dataclass(frozen=True)
class TranscriptionResult:
"""Text and language metadata returned by an STT backend."""
text: str
language: Optional[str] = None
from ..utils.platform_detect import get_backend_type
LANGUAGE_CODE_TO_NAME = {
@@ -154,6 +163,15 @@ class STTBackend(Protocol):
"""
...
async def transcribe_with_metadata(
self,
audio_path: str,
language: Optional[str] = None,
model_size: Optional[str] = None,
) -> TranscriptionResult:
"""Transcribe audio and return text with the resolved language."""
...
def unload_model(self) -> None:
"""Unload model to free memory."""
...
@@ -163,6 +181,26 @@ class STTBackend(Protocol):
...
async def transcribe_with_metadata(
backend: STTBackend,
audio_path: str,
language: Optional[str] = None,
model_size: Optional[str] = None,
) -> TranscriptionResult:
"""Use STT metadata when available while retaining legacy backends."""
metadata_method = getattr(backend, "transcribe_with_metadata", None)
if callable(metadata_method):
result = await metadata_method(audio_path, language, model_size)
if isinstance(result, TranscriptionResult):
return result
if isinstance(result, str):
return TranscriptionResult(text=result.strip(), language=language)
raise TypeError("STT metadata method returned an unsupported result")
text = await backend.transcribe(audio_path, language, model_size)
return TranscriptionResult(text=text.strip(), language=language)
@runtime_checkable
class LLMBackend(Protocol):
"""Protocol for local LLM (chat/completion) backend implementations."""
+33 -7
View File
@@ -17,7 +17,13 @@ from ..utils.hf_offline_patch import patch_huggingface_hub_offline, ensure_origi
patch_huggingface_hub_offline()
ensure_original_qwen_config_cached()
from . import TTSBackend, STTBackend, LANGUAGE_CODE_TO_NAME, WHISPER_HF_REPOS
from . import (
LANGUAGE_CODE_TO_NAME,
STTBackend,
TTSBackend,
TranscriptionResult,
WHISPER_HF_REPOS,
)
from .base import is_model_cached, combine_voice_prompts as _combine_voice_prompts, model_load_progress
from ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_prompt
@@ -327,6 +333,15 @@ class MLXSTTBackend:
language: Optional[str] = None,
model_size: Optional[str] = None,
) -> str:
result = await self.transcribe_with_metadata(audio_path, language, model_size)
return result.text
async def transcribe_with_metadata(
self,
audio_path: str,
language: Optional[str] = None,
model_size: Optional[str] = None,
) -> TranscriptionResult:
"""
Transcribe audio to text.
@@ -336,7 +351,7 @@ class MLXSTTBackend:
model_size: Optional model size override
Returns:
Transcribed text
Transcribed text and resolved language
"""
await self.load_model_async(model_size)
@@ -353,15 +368,26 @@ class MLXSTTBackend:
# regression this revert fixes (issue #462).
result = self.model.generate(str(audio_path), **decode_options)
# Extract text from result
# mlx-audio's Whisper output carries the detected language when
# auto-detection is used. Preserve it instead of collapsing the
# result to a bare string.
if isinstance(result, str):
return result.strip()
text = result
detected_language = language
elif isinstance(result, dict):
return result.get("text", "").strip()
text = result.get("text", "")
detected_language = result.get("language") or language
elif hasattr(result, "text"):
return result.text.strip()
text = result.text
detected_language = getattr(result, "language", None) or language
else:
return str(result).strip()
text = str(result)
detected_language = language
return TranscriptionResult(
text=text.strip(),
language=detected_language,
)
# Run blocking transcription in thread pool
return await asyncio.to_thread(_transcribe_sync)
+45 -5
View File
@@ -10,7 +10,13 @@ import numpy as np
logger = logging.getLogger(__name__)
from . import TTSBackend, STTBackend, LANGUAGE_CODE_TO_NAME, WHISPER_HF_REPOS
from . import (
LANGUAGE_CODE_TO_NAME,
STTBackend,
TTSBackend,
TranscriptionResult,
WHISPER_HF_REPOS,
)
from .base import (
is_model_cached,
get_torch_device,
@@ -23,6 +29,14 @@ from ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_pr
from ..utils.audio import load_audio
def whisper_language_code_from_token_id(generation_config, token_id: int) -> Optional[str]:
"""Resolve a Whisper language token ID to its canonical language code."""
for token, candidate_id in getattr(generation_config, "lang_to_id", {}).items():
if candidate_id == token_id and token.startswith("<|") and token.endswith("|>"):
return token[2:-2]
return None
class PyTorchTTSBackend:
"""PyTorch-based TTS backend using Qwen3-TTS."""
@@ -320,6 +334,15 @@ class PyTorchSTTBackend:
language: Optional[str] = None,
model_size: Optional[str] = None,
) -> str:
result = await self.transcribe_with_metadata(audio_path, language, model_size)
return result.text
async def transcribe_with_metadata(
self,
audio_path: str,
language: Optional[str] = None,
model_size: Optional[str] = None,
) -> TranscriptionResult:
"""
Transcribe audio to text.
@@ -329,7 +352,7 @@ class PyTorchSTTBackend:
model_size: Optional model size override
Returns:
Transcribed text
Transcribed text and resolved language
"""
await self.load_model_async(model_size)
@@ -350,9 +373,23 @@ class PyTorchSTTBackend:
)
inputs = inputs.to(self.device)
# Generate transcription
# If language is provided, force it; otherwise let Whisper auto-detect
# Resolve the language before generation so auto-detection can be
# persisted alongside the transcript instead of being discarded.
resolved_language = language
if resolved_language is None:
language_token = self.model.detect_language(
input_features=inputs["input_features"],
generation_config=self.model.generation_config,
)[0].item()
resolved_language = whisper_language_code_from_token_id(
self.model.generation_config,
language_token,
)
generate_kwargs = {}
# Preserve Whisper's existing auto-detection behavior during
# generation. The separately detected code above is metadata only;
# force a decoder language solely when the caller requested one.
if language:
forced_decoder_ids = self.processor.get_decoder_prompt_ids(
language=language,
@@ -372,7 +409,10 @@ class PyTorchSTTBackend:
skip_special_tokens=True,
)[0]
return transcription.strip()
return TranscriptionResult(
text=transcription.strip(),
language=resolved_language,
)
# Run blocking transcription in thread pool
return await asyncio.to_thread(_transcribe_sync)