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) <noreply@anthropic.com>
This commit is contained in:
Mate Remias 2026-05-10 14:34:30 +02:00 • committed by Teknium
parent a355db1ef5
commit be10998264
7 changed files with 101 additions and 10 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 |

View file

@ -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

View file

@ -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

View file

@ -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