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).
This commit is contained in:
sudoeren
2026-08-27 21:40:09 +03:00
parent 5fd32d105f
commit 1bb5c9ebbc
2 changed files with 79 additions and 1 deletions
+45 -1
View File
@@ -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."""
+34
View File
@@ -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.