Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1fdd8ba
fix(stt): respect device and compute_type from config.yaml
Tranquil-Flow Apr 13, 2026
5d86ba6
fix(stt): treat CUBLAS_STATUS_NOT_SUPPORTED as CUDA lib error
liuhao1024 Apr 29, 2026
abd5bdf
fix: avoid local STT crash on Apple Silicon
AnthonyAssistantAi Jul 28, 2026
4d89d79
fix(stt): scope upload size limits to remote providers
ypwcharles Jul 16, 2026
027a2dd
fix(tts): fall through to raw import when lazy_deps fails (#53259)
tusharui Jul 19, 2026
99d66d6
test(tts): add STT fallback regression for _transcribe_mistral (#53259)
tusharui Jul 19, 2026
4ebad50
fix: handle missing transcription module gracefully
RichardHojunJang Apr 13, 2026
3162cb5
fix(stt): report unregistered configured providers
ooiuuii Jun 29, 2026
bc036cb
fix(stt): strip Qwen3-ASR response prefix
LauraGPT Jul 16, 2026
113b67f
fix(stt): anchor Qwen3-ASR envelope stripping
LauraGPT Jul 18, 2026
fbfe93d
fix(stt): anchor qwen asr envelope stripping
LauraGPT Jul 19, 2026
1a1445a
fix(stt): check_voice_requirements() should recognize all STT providers
zehuaw1 Jun 8, 2026
ad38396
fix(stt): check selected provider (not any) + plugin support
zehuaw1 Jul 14, 2026
ba81419
fix(stt): validate selected voice provider availability
zehuaw1 Jul 15, 2026
b2975f5
fix(stt): better error logging when faster-whisper lazy install fails
damiankluk Jun 14, 2026
617f770
fix(stt): preprocess .silk voice notes before transcription
dso2ng Jul 28, 2026
4331d44
transcription: transcode to m4a and retry when OpenAI STT rejects the…
carljborg Jul 21, 2026
1fc603c
fix(stt): lock local model load; allow keyless local OpenAI-compatibl…
teknium1 Jul 28, 2026
d8d4d75
chore: add contributor email mappings for salvaged STT commits
teknium1 Jul 28, 2026
222662d
test: align dispatch tests with provider-scoped validation and named …
teknium1 Jul 28, 2026
213fbd2
test: add BadRequestError to the fake openai module fixture
teknium1 Jul 28, 2026
b569b6b
fix: explicit utf-8 encoding on ffmpeg STT transcode subprocess (Wind…
teknium1 Jul 28, 2026
fa5bf44
fix: stdin=DEVNULL + windows_hide_flags on STT transcode subprocess (…
teknium1 Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors/emails/anthony.ai.assistant@gmail.com
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AnthonyFrancis
1 change: 1 addition & 0 deletions contributors/emails/carl@sempervirens.no
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
carljborg
1 change: 1 addition & 0 deletions contributors/emails/damian.kluk.92@gmail.com
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
damiankluk
1 change: 1 addition & 0 deletions contributors/emails/richardhojunjang@gmail.com
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
RichardHojunJang
1 change: 1 addition & 0 deletions contributors/emails/tusharanshu18@gmail.com
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tusharui
1 change: 1 addition & 0 deletions contributors/emails/zehuaw@mit.edu
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
zehuaw1
12 changes: 11 additions & 1 deletion gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -17971,7 +17971,17 @@ async def _enrich_message_with_transcription(
return f"{prefix}\n\n{user_text}", []
return prefix, []

from tools.transcription_tools import transcribe_audio
try:
from tools.transcription_tools import transcribe_audio
except ModuleNotFoundError as e:
logger.error("Transcription module unavailable: %s", e)
unavailable_note = "[voice message could not be transcribed]"
_placeholder = "(The user sent a message with no text content)"
if user_text and user_text.strip() == _placeholder:
return unavailable_note, []
if user_text:
return f"{unavailable_note}\n\n{user_text}", []
return unavailable_note, []

enriched_parts = []
successful_transcripts: List[str] = []
Expand Down
38 changes: 38 additions & 0 deletions tests/gateway/test_stt_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,44 @@ async def test_enrich_message_with_transcription_returns_tuple_for_empty_content
assert transcripts == ["hello from a captionless voice note"]


@pytest.mark.parametrize(
("user_text", "expected_text"),
[
("caption", "[voice message could not be transcribed]\n\ncaption"),
("", "[voice message could not be transcribed]"),
(
"(The user sent a message with no text content)",
"[voice message could not be transcribed]",
),
],
)
@pytest.mark.asyncio
async def test_enrich_message_with_transcription_handles_missing_transcription_module_gracefully(
user_text,
expected_text,
):
from gateway.run import GatewayRunner

runner = GatewayRunner.__new__(GatewayRunner)
runner.config = GatewayConfig(stt_enabled=True)

real_import = __import__

def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == "tools.transcription_tools":
raise ModuleNotFoundError("No module named 'tools.transcription_tools'")
return real_import(name, globals, locals, fromlist, level)

with patch("builtins.__import__", side_effect=fake_import):
result, transcripts = await runner._enrich_message_with_transcription(
user_text,
["/tmp/voice.ogg"],
)

assert result == expected_text
assert transcripts == []


@pytest.mark.asyncio
async def test_prepare_inbound_message_text_transcribes_queued_voice_event():
from gateway.run import GatewayRunner
Expand Down
28 changes: 28 additions & 0 deletions tests/tools/test_managed_media_gateways.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ def close(self):
APIError=Exception,
APIConnectionError=Exception,
APITimeoutError=Exception,
BadRequestError=type("BadRequestError", (Exception,), {}),
)
sys.modules["openai"] = fake_module

Expand Down Expand Up @@ -347,6 +348,33 @@ def test_transcription_uses_model_specific_response_formats(monkeypatch, tmp_pat
assert json_capture["close_calls"] == 1


@pytest.mark.parametrize(
("transcription", "expected"),
[
("language English<asr_text>Hello from Qwen.", "Hello from Qwen."),
(
types.SimpleNamespace(text="language Chinese<asr_text>Object response."),
"Object response.",
),
(
{"text": "language English<asr_text>Dictionary response."},
"Dictionary response.",
),
],
)
def test_extract_transcript_text_strips_qwen3_asr_prefix(
transcription,
expected,
):
_install_fake_tools_package()
transcription_tools = _load_tool_module(
"tools.transcription_tools",
"transcription_tools.py",
)

assert transcription_tools._extract_transcript_text(transcription) == expected


PLUGINS_DIR = Path(__file__).resolve().parents[2] / "plugins"


Expand Down
33 changes: 27 additions & 6 deletions tests/tools/test_transcription_command_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ def test_model_override_passed_to_template(self, tmp_path):
audio = _make_silent_wav(tmp_path / "input.wav")
# Write the model into the transcript so we can assert it propagated.
interpreter = sys.executable
payload = "import sys; open(sys.argv[2], 'w').write(sys.argv[1])"
payload = "import sys; open(sys.argv[2], 'w', encoding='utf-8').write(sys.argv[1])"
cfg = {
"command": f'"{interpreter}" -c "{payload}" {{model}} {{output_path}}',
"model": "config-model",
Expand All @@ -410,7 +410,7 @@ def test_model_override_passed_to_template(self, tmp_path):
def test_config_model_used_when_no_override(self, tmp_path):
audio = _make_silent_wav(tmp_path / "input.wav")
interpreter = sys.executable
payload = "import sys; open(sys.argv[2], 'w').write(sys.argv[1])"
payload = "import sys; open(sys.argv[2], 'w', encoding='utf-8').write(sys.argv[1])"
cfg = {
"command": f'"{interpreter}" -c "{payload}" {{model}} {{output_path}}',
"model": "config-model",
Expand All @@ -421,7 +421,7 @@ def test_config_model_used_when_no_override(self, tmp_path):
def test_language_from_provider_config_wins(self, tmp_path):
audio = _make_silent_wav(tmp_path / "input.wav")
interpreter = sys.executable
payload = "import sys; open(sys.argv[2], 'w').write(sys.argv[1])"
payload = "import sys; open(sys.argv[2], 'w', encoding='utf-8').write(sys.argv[1])"
cfg = {
"command": f'"{interpreter}" -c "{payload}" {{language}} {{output_path}}',
"language": "fr",
Expand All @@ -435,7 +435,7 @@ def test_language_from_provider_config_wins(self, tmp_path):
def test_language_falls_back_to_stt_section(self, tmp_path):
audio = _make_silent_wav(tmp_path / "input.wav")
interpreter = sys.executable
payload = "import sys; open(sys.argv[2], 'w').write(sys.argv[1])"
payload = "import sys; open(sys.argv[2], 'w', encoding='utf-8').write(sys.argv[1])"
cfg = {
"command": f'"{interpreter}" -c "{payload}" {{language}} {{output_path}}',
}
Expand All @@ -447,7 +447,7 @@ def test_language_falls_back_to_stt_section(self, tmp_path):
def test_language_defaults_to_en(self, tmp_path):
audio = _make_silent_wav(tmp_path / "input.wav")
interpreter = sys.executable
payload = "import sys; open(sys.argv[2], 'w').write(sys.argv[1])"
payload = "import sys; open(sys.argv[2], 'w', encoding='utf-8').write(sys.argv[1])"
cfg = {
"command": f'"{interpreter}" -c "{payload}" {{language}} {{output_path}}',
}
Expand Down Expand Up @@ -486,6 +486,24 @@ def test_command_provider_dispatches_via_transcribe_audio(self, tmp_path):
assert result["transcript"] == "dispatched via command"
assert result["provider"] == "fake-cli"

def test_oversized_command_provider_file_is_rejected(self, tmp_path):
from tools.transcription_tools import MAX_FILE_SIZE

audio = tmp_path / "oversized.wav"
with audio.open("wb") as audio_file:
audio_file.seek(MAX_FILE_SIZE)
audio_file.write(b"\0")
cfg = self._config_with_command_provider("fake-cli", "unused {input_path}")

with patch("tools.transcription_tools._load_stt_config", return_value=cfg), \
patch("tools.transcription_tools._transcribe_command_stt",
return_value={"success": True, "transcript": "hi"}) as mock_command:
result = transcribe_audio(str(audio))

assert result["success"] is False
assert "File too large" in result["error"]
mock_command.assert_not_called()

def test_builtin_name_shadow_does_not_route_to_command(self, tmp_path):
# User mis-configures stt.providers.openai as a command — must NOT
# hijack the real OpenAI built-in. The built-in elif chain owns
Expand All @@ -510,7 +528,10 @@ def test_unknown_provider_no_command_falls_through_to_error(self, tmp_path):
with patch("tools.transcription_tools._load_stt_config", return_value=cfg):
result = transcribe_audio(str(audio))
assert result["success"] is False
assert "No STT provider available" in result["error"]
# Explicitly-configured unknown providers now get a named
# registration error instead of the generic legacy message.
assert result["error_type"] == "provider_not_registered"
assert "unknown-cli" in result["error"]


# ---------------------------------------------------------------------------
Expand Down
Loading
Loading