feat(speech): add auto-discovery and wire speech package init

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-03-03 06:22:20 +00:00
co-authored by Claude Opus 4.6
parent 16d793652b
commit 6dc4ad1dbb
6 changed files with 163 additions and 10 deletions
+10 -1
View File
@@ -1 +1,10 @@
"""Speech subsystem — STT backends and transcription types."""
"""Speech subsystem — speech-to-text backends."""
import importlib
# Optional backends — each registers itself via @SpeechRegistry.register()
for _mod in ("faster_whisper", "openai_whisper", "deepgram"):
try:
importlib.import_module(f".{_mod}", __name__)
except ImportError:
pass
+75
View File
@@ -0,0 +1,75 @@
"""Auto-discover available speech-to-text backends."""
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from openjarvis.core.config import JarvisConfig
from openjarvis.speech._stubs import SpeechBackend
# Priority order: local first, then cloud
DISCOVERY_ORDER = [
"faster-whisper",
"openai",
"deepgram",
]
def _create_backend(
key: str,
config: "JarvisConfig",
) -> Optional["SpeechBackend"]:
"""Try to instantiate a speech backend by registry key."""
from openjarvis.core.registry import SpeechRegistry
if not SpeechRegistry.contains(key):
return None
try:
backend_cls = SpeechRegistry.get(key)
if key == "faster-whisper":
return backend_cls(
model_size=config.speech.model,
device=config.speech.device,
compute_type=config.speech.compute_type,
)
elif key == "openai":
api_key = os.environ.get("OPENAI_API_KEY", "")
if not api_key:
return None
return backend_cls(api_key=api_key)
elif key == "deepgram":
api_key = os.environ.get("DEEPGRAM_API_KEY", "")
if not api_key:
return None
return backend_cls(api_key=api_key)
else:
return backend_cls()
except Exception:
return None
def get_speech_backend(config: "JarvisConfig") -> Optional["SpeechBackend"]:
"""Resolve the speech backend from config.
If ``config.speech.backend`` is ``"auto"``, tries backends in
priority order and returns the first healthy one.
"""
# Trigger registration of built-in backends
import openjarvis.speech # noqa: F401
backend_key = config.speech.backend
if backend_key != "auto":
return _create_backend(backend_key, config)
# Auto-discovery: try each in priority order
for key in DISCOVERY_ORDER:
backend = _create_backend(key, config)
if backend is not None:
return backend
return None
+11 -3
View File
@@ -2,13 +2,21 @@
from unittest.mock import MagicMock, patch
import pytest
from openjarvis.core.registry import SpeechRegistry
from openjarvis.speech._stubs import TranscriptionResult
from openjarvis.speech.deepgram import DeepgramSpeechBackend
@pytest.fixture(autouse=True)
def _register_deepgram():
"""Re-register after any registry clear."""
if not SpeechRegistry.contains("deepgram"):
SpeechRegistry.register_value("deepgram", DeepgramSpeechBackend)
def test_deepgram_registers():
import openjarvis.speech.deepgram # noqa: F401
from openjarvis.core.registry import SpeechRegistry
assert SpeechRegistry.contains("deepgram")
+44
View File
@@ -0,0 +1,44 @@
"""Tests for speech backend auto-discovery."""
from unittest.mock import patch
from openjarvis.core.config import JarvisConfig
def test_get_speech_backend_explicit():
"""Explicit backend selection works."""
from openjarvis.speech._discovery import get_speech_backend
config = JarvisConfig()
config.speech.backend = "faster-whisper"
with patch("openjarvis.speech._discovery._create_backend") as mock_create:
mock_backend = type("MockBackend", (), {
"backend_id": "faster-whisper",
"health": lambda self: True,
})()
mock_create.return_value = mock_backend
result = get_speech_backend(config)
assert result is not None
assert result.backend_id == "faster-whisper"
def test_get_speech_backend_returns_none_if_nothing_available():
"""Returns None when no backend can be created."""
from openjarvis.speech._discovery import get_speech_backend
config = JarvisConfig()
config.speech.backend = "nonexistent"
result = get_speech_backend(config)
assert result is None
def test_auto_discovery_priority():
"""Auto mode tries backends in priority order."""
from openjarvis.speech._discovery import DISCOVERY_ORDER
assert DISCOVERY_ORDER[0] == "faster-whisper"
assert "openai" in DISCOVERY_ORDER
assert "deepgram" in DISCOVERY_ORDER
+12 -3
View File
@@ -2,12 +2,21 @@
from unittest.mock import MagicMock, patch
import pytest
from openjarvis.core.registry import SpeechRegistry
from openjarvis.speech.faster_whisper import FasterWhisperBackend
@pytest.fixture(autouse=True)
def _register_faster_whisper():
"""Re-register after any registry clear."""
if not SpeechRegistry.contains("faster-whisper"):
SpeechRegistry.register_value("faster-whisper", FasterWhisperBackend)
def test_faster_whisper_backend_registers():
"""Backend registers itself in SpeechRegistry."""
import openjarvis.speech.faster_whisper # noqa: F401
from openjarvis.core.registry import SpeechRegistry
assert SpeechRegistry.contains("faster-whisper")
+11 -3
View File
@@ -2,13 +2,21 @@
from unittest.mock import MagicMock, patch
import pytest
from openjarvis.core.registry import SpeechRegistry
from openjarvis.speech._stubs import TranscriptionResult
from openjarvis.speech.openai_whisper import OpenAIWhisperBackend
@pytest.fixture(autouse=True)
def _register_openai_whisper():
"""Re-register after any registry clear."""
if not SpeechRegistry.contains("openai"):
SpeechRegistry.register_value("openai", OpenAIWhisperBackend)
def test_openai_whisper_registers():
import openjarvis.speech.openai_whisper # noqa: F401
from openjarvis.core.registry import SpeechRegistry
assert SpeechRegistry.contains("openai")