From 1bb5c9ebbc40c71695fe4634f3c232458df26386 Mon Sep 17 00:00:00 2001 From: sudoeren Date: Thu, 27 Aug 2026 21:25:24 +0300 Subject: [PATCH] Ask whisper.cpp for the detected language when auto mode runs it The spoken language is only knowable when the transcription model reports it, and only whisper.cpp does: the hosted endpoints accept auto but never say what they heard. transcribe_detected() asks for detection exactly where it can be answered, the local server in auto mode, by requesting verbose_json with no_language_probabilities switched back on for that one request (the server runs with -nlp, which keeps the sweep off everything else). --- dikte/api.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- tests/test_api.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/dikte/api.py b/dikte/api.py index 98a6031..04a0473 100644 --- a/dikte/api.py +++ b/dikte/api.py @@ -356,7 +356,8 @@ def local_failure(service, server, exc): def _transcribe_request(target, audio_path, language, prompt, response_format, - granularity=None, timeout=300, aborter=None): + granularity=None, timeout=300, aborter=None, + detect_language=False): if target.provider == "local": # The timeouts here are sized for a hosted API, where a slow answer is a # bill running. Locally the only thing being spent is time. @@ -368,6 +369,12 @@ def _transcribe_request(target, audio_path, language, prompt, response_format, fields = [("model", target.model), ("response_format", response_format)] if language and language != "auto": fields.append(("language", language)) + if detect_language: + # whisper.cpp was started with -nlp, which keeps the language + # probability sweep off every request. Detection is only worth that + # sweep for the run that asked for it, so it is switched back on here, + # per request, and reported in the verbose_json answer. + fields.append(("no_language_probabilities", "false")) # OpenRouter takes the hint field and throws it away, so spare it the bytes. # The same words still reach the cleanup model as a glossary. whisper.cpp # takes it as the initial prompt, the way OpenAI does. @@ -440,6 +447,43 @@ def transcribe(target, audio_path, language="", prompt="", timeout=300, aborter= return text +# whisper.cpp reports what it heard as a lowercase full name ("turkish", +# "english", "german"…); the settings and the cleanup prompt speak in two-letter +# codes. Only the handful Dikte offers as a fixed choice get a code; anything +# else is left as the empty string, which the caller reads as "unknown" rather +# than guessing at a language it has no label for. +_DETECTED_TO_CODE = { + "english": "en", "turkish": "tr", "german": "de", + "french": "fr", "spanish": "es", "arabic": "ar", +} + + +def transcribe_detected(target, audio_path, language="", prompt="", timeout=300, + aborter=None): + """(text, code) with the language the model heard. + + The spoken language is only knowable when the transcription model reports + it, and only whisper.cpp does: the hosted endpoints accept "auto" but never + say what they heard. So detection is asked for exactly where it can be + answered — the local server, in auto mode — and every other run transcribes + as before and hands back an empty code. + """ + if target.provider == "local" and language == "auto": + data = _transcribe_request( + target, audio_path, language, prompt, "verbose_json", + detect_language=True, timeout=timeout, aborter=aborter, + ) + text = _local_text(data.get("text") or "").strip() + if not text: + raise ApiError(t("Transcript came back empty.")) + code = _DETECTED_TO_CODE.get( + (data.get("detected_language") or "").strip().lower(), "") + return text, code + text = transcribe(target, audio_path, language=language, prompt=prompt, + timeout=timeout, aborter=aborter) + return text, "" + + def transcribe_segments(target, audio_path, language="", prompt="", timeout=300, aborter=None): """[(start_seconds, end_seconds, text)] using whisper-1's verbose response.""" diff --git a/tests/test_api.py b/tests/test_api.py index c914017..d40cb7e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -691,6 +691,40 @@ class TranscribeHere(DikteTest): api.transcribe_segments(LOCAL, self.wav) self.assertEqual(multipart_fields(calls[0])["model"], "ggml-base.bin") + # ---- the detected language -------------------------------------------- + + def test_auto_mode_asks_whisper_for_the_detected_language(self): + # The -nlp the server was started with is switched back on for this one + # request, so whisper's verbose_json reports what it heard. + reply = {"text": " Merhaba dünya. ", "detected_language": "turkish"} + with fake_urlopen(reply) as calls: + text, code = api.transcribe_detected(LOCAL, self.wav, language="auto") + fields = multipart_fields(calls[0]) + self.assertEqual(fields["response_format"], "verbose_json") + self.assertEqual(fields["no_language_probabilities"], "false") + self.assertNotIn("language", fields) + self.assertEqual(text, "Merhaba dünya.") + self.assertEqual(code, "tr") + + def test_a_fixed_language_reports_no_detection(self): + with fake_urlopen({"text": "hello"}) as calls: + text, code = api.transcribe_detected(LOCAL, self.wav, language="tr") + self.assertNotIn("no_language_probabilities", multipart_fields(calls[0])) + self.assertEqual(text, "hello") + self.assertEqual(code, "") + + def test_a_detected_language_without_a_code_stays_unknown(self): + with fake_urlopen({"text": "hello", "detected_language": "somali"}): + _text, code = api.transcribe_detected(LOCAL, self.wav, language="auto") + self.assertEqual(code, "") + + def test_a_hosted_auto_run_transcribes_without_detection(self): + with fake_urlopen({"text": "hi"}) as calls: + text, code = api.transcribe_detected(OPENAI, self.wav, language="auto") + self.assertNotIn("no_language_probabilities", multipart_fields(calls[0])) + self.assertEqual(text, "hi") + self.assertEqual(code, "") + class Stopping(unittest.TestCase): """The Stop button, from the far end: a request already blocked on a reply.