mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 22:30:40 -07:00
fix: make transcript refinement language-aware
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user