The /api/transcribe/capability probe's resolve_provider() only knew the built-in STT providers, so an explicit stt.provider mapping to a Hermes command-type provider was reported unavailable and the browser fell back unnecessarily. Add command_provider_available(), delegating to the agent's _resolve_command_stt_provider_config(provider, cfg) as the single source of truth, consulted as the final fallthrough after the built-in chain (the agent rejects built-in names internally, so ordering can't shadow a native provider). Added a clarifying comment that command providers are intentionally omitted from the auto-detect tuple to match the agent's _get_provider() precedence. Co-authored-by: LLMinem <LLMinem@users.noreply.github.com>
187 lines
6.3 KiB
Python
187 lines
6.3 KiB
Python
import io
|
|
import json
|
|
import sys
|
|
import types
|
|
|
|
import api.upload as upload
|
|
from api.upload import (
|
|
_stt_provider_capability_from_module,
|
|
handle_transcribe,
|
|
handle_transcribe_capability,
|
|
)
|
|
|
|
|
|
def _multipart_body(fields=None, files=None, boundary=b"voiceboundary"):
|
|
fields = fields or {}
|
|
files = files or {}
|
|
body = b""
|
|
for name, value in fields.items():
|
|
body += b"--" + boundary + b"\r\n"
|
|
body += f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode()
|
|
body += str(value).encode() + b"\r\n"
|
|
for name, (filename, data, content_type) in files.items():
|
|
body += b"--" + boundary + b"\r\n"
|
|
body += (
|
|
f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'
|
|
f'Content-Type: {content_type}\r\n\r\n'
|
|
).encode()
|
|
body += data + b"\r\n"
|
|
body += b"--" + boundary + b"--\r\n"
|
|
return body, f"multipart/form-data; boundary={boundary.decode()}"
|
|
|
|
|
|
class _FakeHandler:
|
|
def __init__(self, body: bytes, content_type: str):
|
|
self.rfile = io.BytesIO(body)
|
|
self.wfile = io.BytesIO()
|
|
self.headers = {
|
|
"Content-Type": content_type,
|
|
"Content-Length": str(len(body)),
|
|
}
|
|
self.status = None
|
|
self.sent_headers = {}
|
|
|
|
def send_response(self, status):
|
|
self.status = status
|
|
|
|
def send_header(self, key, value):
|
|
self.sent_headers[key] = value
|
|
|
|
def end_headers(self):
|
|
pass
|
|
|
|
def payload(self):
|
|
return json.loads(self.wfile.getvalue().decode("utf-8"))
|
|
|
|
|
|
def _install_fake_transcription_tools(monkeypatch, fake_mod):
|
|
monkeypatch.setitem(sys.modules, "tools.transcription_tools", fake_mod)
|
|
tools_pkg = sys.modules.get("tools")
|
|
if tools_pkg is not None:
|
|
monkeypatch.setattr(tools_pkg, "transcription_tools", fake_mod, raising=False)
|
|
|
|
|
|
def test_handle_transcribe_requires_file_field():
|
|
body, content_type = _multipart_body(fields={"note": "missing file"})
|
|
handler = _FakeHandler(body, content_type)
|
|
handle_transcribe(handler)
|
|
assert handler.status == 400
|
|
assert handler.payload()["error"] == "No file field in request"
|
|
|
|
|
|
def test_handle_transcribe_returns_transcript(monkeypatch):
|
|
fake_mod = types.ModuleType("tools.transcription_tools")
|
|
fake_mod.transcribe_audio = lambda path: {"success": True, "transcript": "hello from audio"}
|
|
_install_fake_transcription_tools(monkeypatch, fake_mod)
|
|
|
|
body, content_type = _multipart_body(
|
|
files={"file": ("voice.webm", b"RIFFfakeaudio", "audio/webm")}
|
|
)
|
|
handler = _FakeHandler(body, content_type)
|
|
handle_transcribe(handler)
|
|
|
|
assert handler.status == 200
|
|
assert handler.payload() == {"ok": True, "transcript": "hello from audio"}
|
|
|
|
|
|
def test_handle_transcribe_surfaces_provider_error(monkeypatch):
|
|
fake_mod = types.ModuleType("tools.transcription_tools")
|
|
fake_mod.transcribe_audio = lambda path: {"success": False, "error": "STT not configured"}
|
|
_install_fake_transcription_tools(monkeypatch, fake_mod)
|
|
|
|
body, content_type = _multipart_body(
|
|
files={"file": ("voice.webm", b"RIFFfakeaudio", "audio/webm")}
|
|
)
|
|
handler = _FakeHandler(body, content_type)
|
|
handle_transcribe(handler)
|
|
|
|
assert handler.status == 503
|
|
assert handler.payload()["error"] == "STT not configured"
|
|
|
|
|
|
def test_handle_transcribe_capability_reports_unavailable_without_provider(monkeypatch):
|
|
monkeypatch.setattr(upload, "_stt_provider_capability", lambda: (False, "none"))
|
|
|
|
handler = _FakeHandler(b"", "")
|
|
handle_transcribe_capability(handler)
|
|
|
|
assert handler.status == 200
|
|
assert handler.payload()["ok"] is True
|
|
assert handler.payload()["available"] is False
|
|
|
|
def test_handle_transcribe_capability_reports_available_provider(monkeypatch):
|
|
monkeypatch.setattr(upload, "_stt_provider_capability", lambda: (True, "openai"))
|
|
|
|
handler = _FakeHandler(b"", "")
|
|
handle_transcribe_capability(handler)
|
|
|
|
assert handler.status == 200
|
|
assert handler.payload()["ok"] is True
|
|
assert handler.payload()["available"] is True
|
|
assert handler.payload()["provider"] == "openai"
|
|
|
|
def test_handle_transcribe_capability_requires_ffmpeg_for_local_command_webm(monkeypatch):
|
|
fake_mod = types.ModuleType("tools.transcription_tools")
|
|
fake_mod.__dict__.update(
|
|
{
|
|
"_load_stt_config": lambda: {"provider": "local_command"},
|
|
"is_stt_enabled": lambda cfg=None: True,
|
|
"_HAS_FASTER_WHISPER": False,
|
|
"_HAS_OPENAI": False,
|
|
"_HAS_MISTRAL": False,
|
|
"_has_local_command": lambda: True,
|
|
"_find_ffmpeg_binary": lambda: None,
|
|
}
|
|
)
|
|
available, provider = _stt_provider_capability_from_module(fake_mod)
|
|
|
|
assert available is False
|
|
assert provider == "local_command"
|
|
|
|
|
|
def test_handle_transcribe_capability_reports_actual_local_command_fallback(monkeypatch):
|
|
fake_mod = types.ModuleType("tools.transcription_tools")
|
|
fake_mod.__dict__.update(
|
|
{
|
|
"_load_stt_config": lambda: {"provider": "local"},
|
|
"is_stt_enabled": lambda cfg=None: True,
|
|
"_HAS_FASTER_WHISPER": False,
|
|
"_HAS_OPENAI": False,
|
|
"_HAS_MISTRAL": False,
|
|
"_has_local_command": lambda: True,
|
|
"_find_ffmpeg_binary": lambda: "/usr/bin/ffmpeg",
|
|
}
|
|
)
|
|
available, provider = _stt_provider_capability_from_module(fake_mod)
|
|
|
|
assert available is True
|
|
assert provider == "local_command"
|
|
|
|
|
|
def test_handle_transcribe_capability_reports_named_command_provider(monkeypatch):
|
|
stt_config = {
|
|
"provider": "elevenlabs-gemini",
|
|
"providers": {
|
|
"elevenlabs-gemini": {
|
|
"type": "command",
|
|
"command": "/home/user/.hermes/scripts/hermes-elevenlabs-stt.sh {input}",
|
|
}
|
|
},
|
|
}
|
|
fake_mod = types.ModuleType("tools.transcription_tools")
|
|
fake_mod.__dict__.update(
|
|
{
|
|
"_load_stt_config": lambda: stt_config,
|
|
"is_stt_enabled": lambda cfg=None: True,
|
|
"_HAS_FASTER_WHISPER": False,
|
|
"_HAS_OPENAI": False,
|
|
"_HAS_MISTRAL": False,
|
|
"_resolve_command_stt_provider_config": lambda provider, cfg: cfg["providers"].get(provider),
|
|
}
|
|
)
|
|
|
|
available, provider = _stt_provider_capability_from_module(fake_mod)
|
|
|
|
assert available is True
|
|
assert provider == "elevenlabs-gemini"
|