feat: lots of speech stuff
This commit is contained in:
@@ -24,6 +24,8 @@ def isolate_env(monkeypatch):
|
||||
"MISTRAL_API_KEY",
|
||||
"XAI_API_KEY",
|
||||
"XAI_STT_BASE_URL",
|
||||
"ELEVENLABS_API_KEY",
|
||||
"ELEVENLABS_STT_BASE_URL",
|
||||
):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
@@ -87,6 +89,15 @@ class TestProviderSelectionGate:
|
||||
return_value={"XAI_API_KEY": "dotenv-secret"}):
|
||||
assert tt._get_provider({"enabled": True, "provider": "xai"}) == "xai"
|
||||
|
||||
def test_explicit_elevenlabs_sees_dotenv(self):
|
||||
from tools import transcription_tools as tt
|
||||
|
||||
with patch.object(tt, "_HAS_FASTER_WHISPER", False), \
|
||||
patch.object(tt, "_has_local_command", return_value=False), \
|
||||
patch("hermes_cli.config.load_env",
|
||||
return_value={"ELEVENLABS_API_KEY": "dotenv-secret"}):
|
||||
assert tt._get_provider({"enabled": True, "provider": "elevenlabs"}) == "elevenlabs"
|
||||
|
||||
def test_auto_detect_sees_dotenv_groq(self):
|
||||
"""No local backend, no explicit provider — auto-detect should fall
|
||||
through to Groq when its key lives in dotenv only. Before the fix
|
||||
@@ -193,6 +204,33 @@ class TestTranscribeCallSitesReadDotenv:
|
||||
assert result["success"] is True
|
||||
assert captured["headers"]["Authorization"] == "Bearer xai-dotenv-key"
|
||||
|
||||
def test_transcribe_elevenlabs_forwards_dotenv_key(self):
|
||||
from tools import transcription_tools as tt
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def fake_post(url, **kwargs):
|
||||
captured["url"] = url
|
||||
captured["headers"] = kwargs.get("headers", {})
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.json.return_value = {"text": "hello"}
|
||||
return response
|
||||
|
||||
def fake_get_env_value(name, default=None):
|
||||
if name == "ELEVENLABS_API_KEY":
|
||||
return "elevenlabs-dotenv-key"
|
||||
return None
|
||||
|
||||
with patch.object(tt, "get_env_value", side_effect=fake_get_env_value), \
|
||||
patch.object(tt, "_load_stt_config", return_value={}), \
|
||||
patch("requests.post", side_effect=fake_post), \
|
||||
patch("builtins.open", MagicMock()):
|
||||
result = tt._transcribe_elevenlabs("/tmp/fake.mp3", "scribe_v2")
|
||||
|
||||
assert result["success"] is True
|
||||
assert captured["headers"]["xi-api-key"] == "elevenlabs-dotenv-key"
|
||||
|
||||
|
||||
class TestEndToEndRegressionGuard:
|
||||
"""End-to-end probe: patch ``hermes_cli.config.load_env`` to simulate
|
||||
|
||||
@@ -49,6 +49,7 @@ def clean_env(monkeypatch):
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
monkeypatch.delenv("GROQ_API_KEY", raising=False)
|
||||
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
|
||||
monkeypatch.delenv("ELEVENLABS_API_KEY", raising=False)
|
||||
monkeypatch.delenv("HERMES_LOCAL_STT_COMMAND", raising=False)
|
||||
monkeypatch.delenv("HERMES_LOCAL_STT_LANGUAGE", raising=False)
|
||||
|
||||
@@ -1342,3 +1343,161 @@ class TestTranscribeAudioXAIDispatch:
|
||||
transcribe_audio(sample_ogg, model="custom-stt")
|
||||
|
||||
assert mock_xai.call_args[0][1] == "custom-stt"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# _transcribe_elevenlabs
|
||||
# ============================================================================
|
||||
|
||||
class TestTranscribeElevenLabs:
|
||||
def test_no_key(self, monkeypatch):
|
||||
monkeypatch.delenv("ELEVENLABS_API_KEY", raising=False)
|
||||
from tools.transcription_tools import _transcribe_elevenlabs
|
||||
result = _transcribe_elevenlabs("/tmp/test.ogg", "scribe_v2")
|
||||
assert result["success"] is False
|
||||
assert "ELEVENLABS_API_KEY" in result["error"]
|
||||
|
||||
def test_successful_transcription(self, monkeypatch, sample_ogg):
|
||||
monkeypatch.setenv("ELEVENLABS_API_KEY", "eleven-test-key")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"text": "hello from elevenlabs"}
|
||||
|
||||
config = {
|
||||
"elevenlabs": {
|
||||
"language_code": "eng",
|
||||
"tag_audio_events": True,
|
||||
"diarize": True,
|
||||
}
|
||||
}
|
||||
with patch("tools.transcription_tools._load_stt_config", return_value=config), \
|
||||
patch("requests.post", return_value=mock_response) as mock_post:
|
||||
from tools.transcription_tools import _transcribe_elevenlabs
|
||||
result = _transcribe_elevenlabs(sample_ogg, "scribe_v2")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["transcript"] == "hello from elevenlabs"
|
||||
assert result["provider"] == "elevenlabs"
|
||||
call_kwargs = mock_post.call_args.kwargs
|
||||
assert call_kwargs["headers"]["xi-api-key"] == "eleven-test-key"
|
||||
assert call_kwargs["data"]["model_id"] == "scribe_v2"
|
||||
assert call_kwargs["data"]["language_code"] == "eng"
|
||||
assert call_kwargs["data"]["tag_audio_events"] == "true"
|
||||
assert call_kwargs["data"]["diarize"] == "true"
|
||||
|
||||
def test_api_error_returns_failure(self, monkeypatch, sample_ogg):
|
||||
monkeypatch.setenv("ELEVENLABS_API_KEY", "eleven-test-key")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 401
|
||||
mock_response.json.return_value = {"detail": {"message": "Invalid API key"}}
|
||||
mock_response.text = '{"detail": {"message": "Invalid API key"}}'
|
||||
|
||||
with patch("tools.transcription_tools._load_stt_config", return_value={}), \
|
||||
patch("requests.post", return_value=mock_response):
|
||||
from tools.transcription_tools import _transcribe_elevenlabs
|
||||
result = _transcribe_elevenlabs(sample_ogg, "scribe_v2")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "HTTP 401" in result["error"]
|
||||
assert "Invalid API key" in result["error"]
|
||||
|
||||
def test_empty_transcript_returns_failure(self, monkeypatch, sample_ogg):
|
||||
monkeypatch.setenv("ELEVENLABS_API_KEY", "eleven-test-key")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"text": " "}
|
||||
|
||||
with patch("tools.transcription_tools._load_stt_config", return_value={}), \
|
||||
patch("requests.post", return_value=mock_response):
|
||||
from tools.transcription_tools import _transcribe_elevenlabs
|
||||
result = _transcribe_elevenlabs(sample_ogg, "scribe_v2")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "empty transcript" in result["error"]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# _get_provider — ElevenLabs
|
||||
# ============================================================================
|
||||
|
||||
class TestGetProviderElevenLabs:
|
||||
"""ElevenLabs-specific provider selection tests."""
|
||||
|
||||
def test_elevenlabs_when_key_set(self, monkeypatch):
|
||||
monkeypatch.setenv("ELEVENLABS_API_KEY", "eleven-test")
|
||||
from tools.transcription_tools import _get_provider
|
||||
assert _get_provider({"provider": "elevenlabs"}) == "elevenlabs"
|
||||
|
||||
def test_elevenlabs_explicit_no_key_returns_none(self, monkeypatch):
|
||||
"""Explicit elevenlabs with no key returns none — no cross-provider fallback."""
|
||||
monkeypatch.delenv("ELEVENLABS_API_KEY", raising=False)
|
||||
from tools.transcription_tools import _get_provider
|
||||
assert _get_provider({"provider": "elevenlabs"}) == "none"
|
||||
|
||||
def test_auto_detect_elevenlabs_after_xai(self, monkeypatch):
|
||||
"""Auto-detect: elevenlabs is tried after xai when all above are unavailable."""
|
||||
monkeypatch.delenv("GROQ_API_KEY", raising=False)
|
||||
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
|
||||
monkeypatch.delenv("XAI_API_KEY", raising=False)
|
||||
monkeypatch.setenv("ELEVENLABS_API_KEY", "eleven-test")
|
||||
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
|
||||
patch("tools.transcription_tools._has_local_command", return_value=False), \
|
||||
patch("tools.transcription_tools._HAS_OPENAI", False), \
|
||||
patch("tools.transcription_tools._HAS_MISTRAL", False):
|
||||
from tools.transcription_tools import _get_provider
|
||||
assert _get_provider({}) == "elevenlabs"
|
||||
|
||||
def test_auto_detect_xai_preferred_over_elevenlabs(self, monkeypatch):
|
||||
"""Auto-detect: xai is preferred over elevenlabs."""
|
||||
monkeypatch.setenv("XAI_API_KEY", "xai-test")
|
||||
monkeypatch.setenv("ELEVENLABS_API_KEY", "eleven-test")
|
||||
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
|
||||
patch("tools.transcription_tools._has_local_command", return_value=False), \
|
||||
patch("tools.transcription_tools._HAS_OPENAI", False), \
|
||||
patch("tools.transcription_tools._HAS_MISTRAL", False):
|
||||
from tools.transcription_tools import _get_provider
|
||||
assert _get_provider({}) == "xai"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# transcribe_audio — ElevenLabs dispatch
|
||||
# ============================================================================
|
||||
|
||||
class TestTranscribeAudioElevenLabsDispatch:
|
||||
def test_dispatches_to_elevenlabs(self, sample_ogg):
|
||||
with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "elevenlabs"}), \
|
||||
patch("tools.transcription_tools._get_provider", return_value="elevenlabs"), \
|
||||
patch("tools.transcription_tools._transcribe_elevenlabs",
|
||||
return_value={"success": True, "transcript": "hi", "provider": "elevenlabs"}) as mock_elevenlabs:
|
||||
from tools.transcription_tools import transcribe_audio
|
||||
result = transcribe_audio(sample_ogg)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["provider"] == "elevenlabs"
|
||||
mock_elevenlabs.assert_called_once()
|
||||
|
||||
def test_config_elevenlabs_model_used(self, sample_ogg):
|
||||
config = {"provider": "elevenlabs", "elevenlabs": {"model_id": "scribe_v1"}}
|
||||
with patch("tools.transcription_tools._load_stt_config", return_value=config), \
|
||||
patch("tools.transcription_tools._get_provider", return_value="elevenlabs"), \
|
||||
patch("tools.transcription_tools._transcribe_elevenlabs",
|
||||
return_value={"success": True, "transcript": "hi"}) as mock_elevenlabs:
|
||||
from tools.transcription_tools import transcribe_audio
|
||||
transcribe_audio(sample_ogg, model=None)
|
||||
|
||||
assert mock_elevenlabs.call_args[0][1] == "scribe_v1"
|
||||
|
||||
def test_model_override_passed_to_elevenlabs(self, sample_ogg):
|
||||
with patch("tools.transcription_tools._load_stt_config", return_value={}), \
|
||||
patch("tools.transcription_tools._get_provider", return_value="elevenlabs"), \
|
||||
patch("tools.transcription_tools._transcribe_elevenlabs",
|
||||
return_value={"success": True, "transcript": "hi"}) as mock_elevenlabs:
|
||||
from tools.transcription_tools import transcribe_audio
|
||||
transcribe_audio(sample_ogg, model="scribe_v2")
|
||||
|
||||
assert mock_elevenlabs.call_args[0][1] == "scribe_v2"
|
||||
|
||||
Reference in New Issue
Block a user