From be109982647e10af98e9b1abf69253d0d3b17db2 Mon Sep 17 00:00:00 2001 From: Mate Remias Date: Sun, 10 May 2026 14:34:30 +0200 Subject: [PATCH] fix(tools): address Copilot review on Groq STT language hint - Normalize stt.groq.language: cast to str, strip, treat empty/whitespace as unset (parity with xAI's str().strip()). - Clarify "blank = auto-detect" inline comments in 4 docs/configs to reflect the env-var fallback (HERMES_LOCAL_STT_LANGUAGE). - Document that HERMES_LOCAL_STT_LANGUAGE also drives the local faster-whisper provider, not just the CLI fallback. - Add unit tests covering: omitted language when unset, config-supplied language, env fallback, config-over-env precedence, whitespace normalized to unset. Co-Authored-By: Claude Opus 4.7 (1M context) --- cli-config.yaml.example | 2 +- tests/tools/test_transcription_tools.py | 89 +++++++++++++++++++ tools/transcription_tools.py | 4 +- .../docs/reference/environment-variables.md | 2 +- website/docs/user-guide/configuration.md | 4 +- website/docs/user-guide/features/tts.md | 6 +- .../docs/user-guide/features/voice-mode.md | 4 +- 7 files changed, 101 insertions(+), 10 deletions(-) diff --git a/cli-config.yaml.example b/cli-config.yaml.example index 9ad8788a96f3..c5097e9fbee6 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -1128,7 +1128,7 @@ stt: model: "base" # tiny | base | small | medium | large-v3 | turbo # language: "" # auto-detect; set to "en", "es", "fr", etc. to force # groq: - # language: "" # auto-detect; set to "en", "es", "fr", etc. to force + # language: "" # blank = use HERMES_LOCAL_STT_LANGUAGE if set, else auto-detect openai: model: "whisper-1" # whisper-1 | gpt-4o-mini-transcribe | gpt-4o-transcribe language: "" # auto-detect; set to "en", "es", "fr", etc. to force diff --git a/tests/tools/test_transcription_tools.py b/tests/tools/test_transcription_tools.py index 0997872c69fd..0dbebda66b80 100644 --- a/tests/tools/test_transcription_tools.py +++ b/tests/tools/test_transcription_tools.py @@ -311,6 +311,95 @@ class TestTranscribeGroq: assert result["success"] is False assert "Permission denied" in result["error"] + def test_language_hint_omitted_when_unset(self, monkeypatch, sample_wav): + monkeypatch.setenv("GROQ_API_KEY", "gsk-test") + monkeypatch.delenv("HERMES_LOCAL_STT_LANGUAGE", raising=False) + + mock_client = MagicMock() + mock_client.audio.transcriptions.create.return_value = "hi" + + with patch("tools.transcription_tools._HAS_OPENAI", True), \ + patch("openai.OpenAI", return_value=mock_client), \ + patch("tools.transcription_tools._load_stt_config", return_value={}): + from tools.transcription_tools import _transcribe_groq + _transcribe_groq(sample_wav, "whisper-large-v3-turbo") + + kwargs = mock_client.audio.transcriptions.create.call_args.kwargs + assert "language" not in kwargs + + def test_language_hint_from_config(self, monkeypatch, sample_wav): + monkeypatch.setenv("GROQ_API_KEY", "gsk-test") + monkeypatch.delenv("HERMES_LOCAL_STT_LANGUAGE", raising=False) + + mock_client = MagicMock() + mock_client.audio.transcriptions.create.return_value = "hola" + + with patch("tools.transcription_tools._HAS_OPENAI", True), \ + patch("openai.OpenAI", return_value=mock_client), \ + patch( + "tools.transcription_tools._load_stt_config", + return_value={"groq": {"language": "es"}}, + ): + from tools.transcription_tools import _transcribe_groq + _transcribe_groq(sample_wav, "whisper-large-v3-turbo") + + kwargs = mock_client.audio.transcriptions.create.call_args.kwargs + assert kwargs["language"] == "es" + + def test_language_hint_from_env_when_config_missing(self, monkeypatch, sample_wav): + monkeypatch.setenv("GROQ_API_KEY", "gsk-test") + monkeypatch.setenv("HERMES_LOCAL_STT_LANGUAGE", "hu") + + mock_client = MagicMock() + mock_client.audio.transcriptions.create.return_value = "szia" + + with patch("tools.transcription_tools._HAS_OPENAI", True), \ + patch("openai.OpenAI", return_value=mock_client), \ + patch("tools.transcription_tools._load_stt_config", return_value={}): + from tools.transcription_tools import _transcribe_groq + _transcribe_groq(sample_wav, "whisper-large-v3-turbo") + + kwargs = mock_client.audio.transcriptions.create.call_args.kwargs + assert kwargs["language"] == "hu" + + def test_language_config_overrides_env(self, monkeypatch, sample_wav): + monkeypatch.setenv("GROQ_API_KEY", "gsk-test") + monkeypatch.setenv("HERMES_LOCAL_STT_LANGUAGE", "hu") + + mock_client = MagicMock() + mock_client.audio.transcriptions.create.return_value = "hello" + + with patch("tools.transcription_tools._HAS_OPENAI", True), \ + patch("openai.OpenAI", return_value=mock_client), \ + patch( + "tools.transcription_tools._load_stt_config", + return_value={"groq": {"language": "en"}}, + ): + from tools.transcription_tools import _transcribe_groq + _transcribe_groq(sample_wav, "whisper-large-v3-turbo") + + kwargs = mock_client.audio.transcriptions.create.call_args.kwargs + assert kwargs["language"] == "en" + + def test_language_whitespace_treated_as_unset(self, monkeypatch, sample_wav): + monkeypatch.setenv("GROQ_API_KEY", "gsk-test") + monkeypatch.delenv("HERMES_LOCAL_STT_LANGUAGE", raising=False) + + mock_client = MagicMock() + mock_client.audio.transcriptions.create.return_value = "hi" + + with patch("tools.transcription_tools._HAS_OPENAI", True), \ + patch("openai.OpenAI", return_value=mock_client), \ + patch( + "tools.transcription_tools._load_stt_config", + return_value={"groq": {"language": " "}}, + ): + from tools.transcription_tools import _transcribe_groq + _transcribe_groq(sample_wav, "whisper-large-v3-turbo") + + kwargs = mock_client.audio.transcriptions.create.call_args.kwargs + assert "language" not in kwargs + # ============================================================================ # _transcribe_openai — additional tests diff --git a/tools/transcription_tools.py b/tools/transcription_tools.py index bef4b3b63025..1a724eda55ce 100644 --- a/tools/transcription_tools.py +++ b/tools/transcription_tools.py @@ -1321,7 +1321,9 @@ def _transcribe_groq(file_path: str, model_name: str) -> Dict[str, Any]: model_name = DEFAULT_GROQ_STT_MODEL groq_cfg = _load_stt_config().get("groq", {}) - language = groq_cfg.get("language") or os.getenv(LOCAL_STT_LANGUAGE_ENV) + language = str( + groq_cfg.get("language") or os.getenv(LOCAL_STT_LANGUAGE_ENV) or "" + ).strip() or None try: from openai import OpenAI, APIError, APIConnectionError, APITimeoutError diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index ac73691ea468..ec99660fd783 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -102,7 +102,7 @@ Hermes reads environment variables from the process environment and, for user-ma | `HERMES_MODEL` | Override model name at process level (used by cron scheduler; prefer `config.yaml` for normal use) | | `VOICE_TOOLS_OPENAI_KEY` | Preferred OpenAI key for OpenAI speech-to-text and text-to-speech providers | | `HERMES_LOCAL_STT_COMMAND` | Optional local speech-to-text command template. Supports `{input_path}`, `{output_dir}`, `{language}`, and `{model}` placeholders | -| `HERMES_LOCAL_STT_LANGUAGE` | Default language hint for STT. Used by `HERMES_LOCAL_STT_COMMAND`, the local `whisper` CLI fallback (default: `en`), Groq, and xAI when no per-provider `language` is set in `config.yaml` | +| `HERMES_LOCAL_STT_LANGUAGE` | Default language hint for STT. Used by the `local` (faster-whisper) provider, `HERMES_LOCAL_STT_COMMAND`, the local `whisper` CLI fallback (default: `en`), Groq, and xAI when no per-provider `language` is set in `config.yaml` | | `HERMES_HOME` | Override Hermes config directory (default: `~/.hermes`). Also scopes the gateway PID file and systemd service name, so multiple installations can run concurrently | | `HERMES_GIT_BASH_PATH` | **Windows only.** Override `bash.exe` discovery for the terminal tool. Points at any bash — full Git-for-Windows install, WSL bash via symlink, MSYS2, Cygwin. The installer sets this automatically to the PortableGit it provisioned. See the [Windows (Native) Guide](../user-guide/windows-native.md#how-hermes-runs-shell-commands-on-windows) | | `HERMES_DISABLE_WINDOWS_UTF8` | **Windows only.** Set to `1` to disable the UTF-8 stdio shim (`configure_windows_stdio()`) and fall back to the console's locale code page. Useful for bisecting encoding bugs; rarely the right setting in normal operation | diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index f55bd139db4b..8fa1ddc65ffd 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -1715,9 +1715,9 @@ stt: provider: "local" # "local" | "groq" | "openai" | "mistral" local: model: "base" # tiny, base, small, medium, large-v3 - language: "" # optional ISO-639-1 hint (e.g. "en"); blank = auto-detect + language: "" # optional ISO-639-1 hint; blank = use HERMES_LOCAL_STT_LANGUAGE if set, else auto-detect groq: - language: "" # optional ISO-639-1 hint (e.g. "en"); blank = auto-detect + language: "" # optional ISO-639-1 hint; blank = use HERMES_LOCAL_STT_LANGUAGE if set, else auto-detect openai: model: "whisper-1" # whisper-1 | gpt-4o-mini-transcribe | gpt-4o-transcribe language: "" # auto-detect; set to "en", "es", "fr", etc. to force diff --git a/website/docs/user-guide/features/tts.md b/website/docs/user-guide/features/tts.md index 2d8ceb228d75..920f360f7a35 100644 --- a/website/docs/user-guide/features/tts.md +++ b/website/docs/user-guide/features/tts.md @@ -429,16 +429,16 @@ stt: provider: "local" # "local" | "groq" | "openai" | "mistral" | "xai" local: model: "base" # tiny, base, small, medium, large-v3 - language: "" # optional ISO-639-1 hint (e.g. "en"); blank = auto-detect + language: "" # optional ISO-639-1 hint; blank = use HERMES_LOCAL_STT_LANGUAGE if set, else auto-detect groq: - language: "" # optional ISO-639-1 hint (e.g. "en"); blank = auto-detect + language: "" # optional ISO-639-1 hint; blank = use HERMES_LOCAL_STT_LANGUAGE if set, else auto-detect openai: model: "whisper-1" # whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe mistral: model: "voxtral-mini-latest" # voxtral-mini-latest, voxtral-mini-2602 xai: model: "grok-stt" # xAI Grok STT - language: "" # optional ISO-639-1 hint + language: "" # optional ISO-639-1 hint; blank = use HERMES_LOCAL_STT_LANGUAGE if set, else "en" ``` ### Provider Details diff --git a/website/docs/user-guide/features/voice-mode.md b/website/docs/user-guide/features/voice-mode.md index b10ef0dff7c3..a4f64be3f3a4 100644 --- a/website/docs/user-guide/features/voice-mode.md +++ b/website/docs/user-guide/features/voice-mode.md @@ -414,9 +414,9 @@ stt: provider: "local" # "local" (free) | "groq" | "openai" | "mistral" | "xai" local: model: "base" # tiny, base, small, medium, large-v3 - language: "" # optional ISO-639-1 hint (e.g. "en"); blank = auto-detect + language: "" # optional ISO-639-1 hint; blank = use HERMES_LOCAL_STT_LANGUAGE if set, else auto-detect groq: - language: "" # optional ISO-639-1 hint (e.g. "en"); blank = auto-detect + language: "" # optional ISO-639-1 hint; blank = use HERMES_LOCAL_STT_LANGUAGE if set, else auto-detect # model: "whisper-1" # Legacy: used when provider is not set # Text-to-Speech