Merge pull request #63 from sudoeren/auto-language-detect

Detect the spoken language instead of fixing one
This commit is contained in:
Yusuf İpek
2026-09-09 11:13:54 +03:00
committed by GitHub
11 changed files with 219 additions and 33 deletions
+5
View File
@@ -220,6 +220,11 @@ running.
written for subtitles, so the lines keep their place and nothing is shortened. written for subtitles, so the lines keep their place and nothing is shortened.
- **History** of every dictation under Settings → History, with a size limit and - **History** of every dictation under Settings → History, with a size limit and
right-click to delete. right-click to delete.
- **The speech language is detected, not picked.** Auto is the default: whisper
on this machine says what it heard, the hosted providers transcribe in
whatever language comes in without being told, and the detected language
lands in the history and decides which cleanup prompt (Turkish or the
language-agnostic one) a run gets. A fixed language still overrides it.
- **Turkish and English interface**, following the system locale by default. - **Turkish and English interface**, following the system locale by default.
## The global shortcuts, and the logout KDE needs ## The global shortcuts, and the logout KDE needs
+5
View File
@@ -214,6 +214,11 @@ olmasını ister.
yerinde kalır, hiçbir şey kısaltılmaz. yerinde kalır, hiçbir şey kısaltılmaz.
- **Geçmiş** Ayarlar → Geçmiş sekmesinde; boyut sınırı var, sağ tıklayıp - **Geçmiş** Ayarlar → Geçmiş sekmesinde; boyut sınırı var, sağ tıklayıp
silebilirsin. silebilirsin.
- **Konuşma dili seçilmez, algılanır.** Varsayılan otomatiktir: bu makinedeki
whisper ne duyduğunu söyler, bulut sağlayıcılar söylenmeden de hangi dilde
konuşuluyorsa o dilde yazar; algılanan dil geçmişe düşer ve bir kaydın hangi
temizleme promptunu alacağını belirler (Türkçe mi, dile duyarsız olanı mı).
Sabit bir dil yine de bunun önüne geçer.
- **Türkçe ve İngilizce arayüz**, varsayılan olarak sistem dilini izler. - **Türkçe ve İngilizce arayüz**, varsayılan olarak sistem dilini izler.
## Global kısayollar ve KDE'nin istediği oturum kapatma ## Global kısayollar ve KDE'nin istediği oturum kapatma
+46 -1
View File
@@ -367,7 +367,8 @@ def local_failure(service, server, exc):
def _transcribe_request(target, audio_path, language, prompt, response_format, 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": if target.provider == "local":
# The timeouts here are sized for a hosted API, where a slow answer is a # 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. # bill running. Locally the only thing being spent is time.
@@ -379,6 +380,12 @@ def _transcribe_request(target, audio_path, language, prompt, response_format,
fields = [("model", target.model), ("response_format", response_format)] fields = [("model", target.model), ("response_format", response_format)]
if language and language != "auto": if language and language != "auto":
fields.append(("language", language)) 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. # 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 # The same words still reach the cleanup model as a glossary. whisper.cpp
# takes it as the initial prompt, the way OpenAI does. # takes it as the initial prompt, the way OpenAI does.
@@ -546,6 +553,44 @@ def transcribe(target, audio_path, language="", prompt="", timeout=300, aborter=
return text 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."))
detected = data.get("detected_language")
code = _DETECTED_TO_CODE.get(
detected.strip().lower(), "") if isinstance(detected, str) else ""
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, def transcribe_segments(target, audio_path, language="", prompt="", timeout=300,
aborter=None): aborter=None):
"""[(start_seconds, end_seconds, text)] using whisper-1's verbose response.""" """[(start_seconds, end_seconds, text)] using whisper-1's verbose response."""
+6 -4
View File
@@ -1084,7 +1084,7 @@ class Dikte:
if not self._transcripts_pending: if not self._transcripts_pending:
self._settle(DICTATION, payload) self._settle(DICTATION, payload)
def _on_finished(self, _raw, text, warning): def _on_finished(self, _raw, text, warning, speech_language):
if warning: if warning:
# The text was still pasted, but cleanup did not run. Say so loudly: # The text was still pasted, but cleanup did not run. Say so loudly:
# a rejected key otherwise looks exactly like working dictation. # a rejected key otherwise looks exactly like working dictation.
@@ -1105,9 +1105,10 @@ class Dikte:
t("{action}: {preview}", action=action, preview=_preview(text)) t("{action}: {preview}", action=action, preview=_preview(text))
) )
self._transcript_settled({"ok": True, "text": text, "raw": _raw, self._transcript_settled({"ok": True, "text": text, "raw": _raw,
"warning": warning}) "warning": warning,
"speech_language": speech_language})
def _on_ask_finished(self, _raw, text, warning): def _on_ask_finished(self, _raw, text, warning, speech_language):
agent = assistant.display_name(self.conf) agent = assistant.display_name(self.conf)
if warning: if warning:
# A tool the agent was not allowed to touch otherwise looks exactly # A tool the agent was not allowed to touch otherwise looks exactly
@@ -1128,7 +1129,8 @@ class Dikte:
) )
self._set_ask_state(IDLE) self._set_ask_state(IDLE)
self._settle(ASK, {"ok": True, "answer": text, "question": _raw, self._settle(ASK, {"ok": True, "answer": text, "question": _raw,
"warning": warning, "agent": agent}) "warning": warning, "agent": agent,
"speech_language": speech_language})
def _on_ask_cancelled(self): def _on_ask_cancelled(self):
self.ask_overlay.show_done(t("Stopped."), 2000) self.ask_overlay.show_done(t("Stopped."), 2000)
+17 -5
View File
@@ -400,7 +400,9 @@ DEFAULTS = {
# What a timestamped run (subtitles) asks OpenRouter for: not every model # What a timestamped run (subtitles) asks OpenRouter for: not every model
# there returns segment times. Empty -> openai/whisper-1. # there returns segment times. Empty -> openai/whisper-1.
"openrouter_file_model": "", "openrouter_file_model": "",
"language": "tr", # A stored language overrides this default. Hosted providers receive no
# language hint in auto mode; local whisper also reports the detected code.
"language": "auto",
"transcribe_prompt": "", "transcribe_prompt": "",
# --- whisper.cpp, on this machine --------------------------------------- # --- whisper.cpp, on this machine ---------------------------------------
@@ -733,13 +735,23 @@ class Config:
return self["cleanup_provider"] == "local" return self["cleanup_provider"] == "local"
def cleanup_prompt(self, with_timestamps=False, with_speakers=False, def cleanup_prompt(self, with_timestamps=False, with_speakers=False,
subtitles=False): subtitles=False, speech=""):
turkish = i18n.language() == "tr" """`speech` is the two-letter code of the language that was heard, when
the transcription model reported one. The default prompts and the
glossary rule only exist in Turkish and English, so a detected Turkish
recording gets the Turkish prompt and any other detected language, or
none at all, the English one, which is written not to care what
language the transcript is in. Nothing else calls this with it, so the
interface language keeps deciding everywhere the speech was not asked
about."""
turkish = (speech == "tr") if speech else i18n.language() == "tr"
if subtitles: if subtitles:
prompt = (self["file_cleanup_prompt"].strip() prompt = (self["file_cleanup_prompt"].strip()
or default_file_cleanup_prompt()) or (FILE_CLEANUP_PROMPT_TR if turkish
else FILE_CLEANUP_PROMPT_EN))
else: else:
prompt = self["cleanup_prompt"].strip() or default_cleanup_prompt() prompt = (self["cleanup_prompt"].strip()
or (CLEANUP_PROMPT_TR if turkish else CLEANUP_PROMPT_EN))
glossary = self["transcribe_prompt"].strip() glossary = self["transcribe_prompt"].strip()
if with_speakers: if with_speakers:
glossary = "\n".join(x for x in (glossary, self.participants()) if x) glossary = "\n".join(x for x in (glossary, self.participants()) if x)
+4 -3
View File
@@ -1547,12 +1547,13 @@ def _whisper_args(settings):
binary, "-m", str(model), binary, "-m", str(model),
"--inference-path", INFERENCE_PATH, "--inference-path", INFERENCE_PATH,
# Whatever language the request does not name. api.py leaves the field # Whatever language the request does not name. api.py leaves the field
# out when the language is "auto", and the server's own default is # out when the language is "auto", and the server's own language is
# English rather than detection. # set here: "auto" makes whisper.cpp detect what it hears.
"-l", "auto", "-l", "auto",
# Stock phrases invented for near-silence come from non-speech tokens, # Stock phrases invented for near-silence come from non-speech tokens,
# and verbose_json otherwise pays for a language probability sweep # and verbose_json otherwise pays for a language probability sweep
# nothing here reads. # nobody asked for. A request that wants the detected language switches
# that back on per request.
"-sns", "-nlp", "-sns", "-nlp",
] ]
if int(settings["threads"]) > 0: if int(settings["threads"]) > 0:
+20 -3
View File
@@ -37,7 +37,7 @@ _paste_lock = threading.Lock()
class Pipeline(QObject): class Pipeline(QObject):
stage = pyqtSignal(str) # human-readable progress line stage = pyqtSignal(str) # human-readable progress line
finished = pyqtSignal(str, str, str) # raw transcript, final text, warning finished = pyqtSignal(str, str, str, str) # raw, final text, warning, language
failed = pyqtSignal(str) failed = pyqtSignal(str)
cancelled = pyqtSignal() cancelled = pyqtSignal()
@@ -120,12 +120,24 @@ class Pipeline(QObject):
try: try:
self.stage.emit(t("Transcribing…")) self.stage.emit(t("Transcribing…"))
target = conf.transcribe_target() target = conf.transcribe_target()
# The spoken language is only knowable after the fact, and only the
# local server says what it heard: auto mode asks it there, and
# every other run (a fixed language, or a hosted provider that
# detects but stays silent) transcribes as before.
auto = conf["language"] == "auto"
if auto:
raw, detected = api.transcribe_detected(
target, wav_path, language=conf["language"],
prompt=conf["transcribe_prompt"],
)
else:
raw = api.transcribe( raw = api.transcribe(
target, target,
wav_path, wav_path,
language=conf["language"], language=conf["language"],
prompt=conf["transcribe_prompt"], prompt=conf["transcribe_prompt"],
) )
detected = ""
if conf["filter_hallucinations"] and vad.looks_like_hallucination(raw, duration): if conf["filter_hallucinations"] and vad.looks_like_hallucination(raw, duration):
self._discard(wav_path) self._discard(wav_path)
@@ -134,6 +146,10 @@ class Pipeline(QObject):
text = raw text = raw
warning = "" warning = ""
# The language the run actually spoke, reported to the window, the
# clipboard path and the history alike: the detected code, or the
# configured one when nothing was detected to replace it.
speech_language = detected or conf["language"]
# Remembered rather than re-derived at the history write below: the # Remembered rather than re-derived at the history write below: the
# ask path runs cleanup under a different setting, and the record # ask path runs cleanup under a different setting, and the record
# should say what happened, not what one of the two gates implies. # should say what happened, not what one of the two gates implies.
@@ -145,7 +161,7 @@ class Pipeline(QObject):
self.stage.emit(t("Cleaning up…")) self.stage.emit(t("Cleaning up…"))
cleaned = True cleaned = True
try: try:
text = cleanup.run(raw, conf, conf.cleanup_prompt()) text = cleanup.run(raw, conf, conf.cleanup_prompt(speech=detected))
except api.ApiError as exc: except api.ApiError as exc:
# Keep the transcript, but never let the failure pass unseen: # Keep the transcript, but never let the failure pass unseen:
# a rejected key would otherwise look like working dictation. # a rejected key would otherwise look like working dictation.
@@ -183,6 +199,7 @@ class Pipeline(QObject):
"question": question, "question": question,
"assistant": assistant.provider(conf) if ask else "", "assistant": assistant.provider(conf) if ask else "",
"assistant_model": assistant.model(conf) if ask else "", "assistant_model": assistant.model(conf) if ask else "",
"speech_language": speech_language,
"raw": raw, "raw": raw,
"text": text, "text": text,
} }
@@ -221,7 +238,7 @@ class Pipeline(QObject):
time.sleep(0.35) time.sleep(0.35)
paste.copy_bytes(previous) paste.copy_bytes(previous)
self.finished.emit(raw, text, warning) self.finished.emit(raw, text, warning, speech_language)
except assistant.Cancelled: except assistant.Cancelled:
self.cancelled.emit() self.cancelled.emit()
+34
View File
@@ -827,6 +827,40 @@ class TranscribeHere(DikteTest):
api.transcribe_segments(LOCAL, self.wav) api.transcribe_segments(LOCAL, self.wav)
self.assertEqual(multipart_fields(calls[0])["model"], "ggml-base.bin") 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): class Stopping(unittest.TestCase):
"""The Stop button, from the far end: a request already blocked on a reply. """The Stop button, from the far end: a request already blocked on a reply.
+7
View File
@@ -746,6 +746,13 @@ class Replies(DikteTest):
self.assertEqual(code, 0) self.assertEqual(code, 0)
self.assertEqual(out.strip(), "Book it for Thursday.") self.assertEqual(out.strip(), "Book it for Thursday.")
def test_the_json_answer_carries_the_detected_language(self):
code, out, _ = self.run_verb(
["--json", "record"],
{"ok": True, "text": "Selam", "speech_language": "tr"})
self.assertEqual(code, 0)
self.assertEqual(json.loads(out)["speech_language"], "tr")
def test_a_dictation_that_failed(self): def test_a_dictation_that_failed(self):
code, out, err = self.run_verb(["stop", "--wait"], code, out, err = self.run_verb(["stop", "--wait"],
{"ok": False, "error": "No speech detected"}) {"ok": False, "error": "No speech detected"})
+12
View File
@@ -274,6 +274,18 @@ class CleanupPrompt(DikteTest):
def test_no_glossary_means_no_rule_about_one(self): def test_no_glossary_means_no_rule_about_one(self):
self.assertEqual(cfg.Config().cleanup_prompt(), cfg.CLEANUP_PROMPT_EN) self.assertEqual(cfg.Config().cleanup_prompt(), cfg.CLEANUP_PROMPT_EN)
def test_a_detected_turkish_recording_gets_the_turkish_prompt(self):
"""Auto mode learns what was heard, and that decides the prompt rather
than the interface language."""
self.write_config({"ui_language": "en", "transcribe_prompt": "Paraşüt"})
conf = cfg.Config()
prompt = conf.cleanup_prompt(speech="tr")
self.assertEqual(prompt, cfg.CLEANUP_PROMPT_TR
+ cfg.GLOSSARY_RULE_TR.format(glossary="Paraşüt"))
self.assertIn("KONUŞMACININ KULLANDIĞI İSİM VE TERİMLER", prompt)
self.assertIn("NAMES AND TERMS THE SPEAKER USES",
conf.cleanup_prompt(speech="de"))
def test_subtitles_use_their_own_prompt(self): def test_subtitles_use_their_own_prompt(self):
conf = cfg.Config() conf = cfg.Config()
self.assertNotEqual(conf.cleanup_prompt(subtitles=True), conf.cleanup_prompt()) self.assertNotEqual(conf.cleanup_prompt(subtitles=True), conf.cleanup_prompt())
+57 -11
View File
@@ -35,7 +35,7 @@ class Chain(DikteTest):
cleaned="Book it for Thursday.", cleaned="Book it for Thursday.",
cleanup_error=None, answer=("Booked.", ""), rms=None, cleanup_error=None, answer=("Booked.", ""), rms=None,
clipboard=b"what was there before", paste_error=None, clipboard=b"what was there before", paste_error=None,
focus=None): detected="en", focus=None):
pipeline = worker.Pipeline(self.conf) pipeline = worker.Pipeline(self.conf)
done, failures, stages, cancels = [], [], [], [] done, failures, stages, cancels = [], [], [], []
pipeline.finished.connect(lambda *args: done.append(args)) pipeline.finished.connect(lambda *args: done.append(args))
@@ -45,14 +45,19 @@ class Chain(DikteTest):
cleanup = (mock.Mock(side_effect=cleanup_error) if cleanup_error cleanup = (mock.Mock(side_effect=cleanup_error) if cleanup_error
else mock.Mock(return_value=cleaned)) else mock.Mock(return_value=cleaned))
# Auto mode takes the detection path; a fixed language the plain one.
# Both are mocked so the chain runs either way without a server.
behavior = {"side_effect": transcribe_error} if transcribe_error \
else {"return_value": transcript}
detect_behavior = {"side_effect": transcribe_error} if transcribe_error \
else {"return_value": (transcript, detected)}
calls = {} calls = {}
# The chain reports its own failures on stderr, which a test run has no # The chain reports its own failures on stderr, which a test run has no
# use for. # use for.
with contextlib.redirect_stderr(io.StringIO()), \ with contextlib.redirect_stderr(io.StringIO()), \
mock.patch.object( mock.patch.object(api, "transcribe", **behavior) as tr, \
api, "transcribe", mock.patch.object(api, "transcribe_detected",
**({"side_effect": transcribe_error} if transcribe_error **detect_behavior) as tdet, \
else {"return_value": transcript})) as tr, \
mock.patch.object(api, "cleanup", cleanup), \ mock.patch.object(api, "cleanup", cleanup), \
mock.patch.object(assistant, "ask", return_value=answer) as ask_call, \ mock.patch.object(assistant, "ask", return_value=answer) as ask_call, \
mock.patch.object(paste, "copy") as copy, \ mock.patch.object(paste, "copy") as copy, \
@@ -62,7 +67,8 @@ class Chain(DikteTest):
return_value=clipboard) as read_clipboard, \ return_value=clipboard) as read_clipboard, \
mock.patch.object(worker.time, "sleep", lambda seconds: None): mock.patch.object(worker.time, "sleep", lambda seconds: None):
press.side_effect = paste_error press.side_effect = paste_error
calls = {"transcribe": tr, "cleanup": cleanup, "ask": ask_call, calls = {"transcribe": tr, "transcribe_detected": tdet,
"cleanup": cleanup, "ask": ask_call,
"copy": copy, "copy_bytes": copy_bytes, "press": press, "copy": copy, "copy_bytes": copy_bytes, "press": press,
"read_clipboard": read_clipboard} "read_clipboard": read_clipboard}
pipeline._work(self.wav, duration, pipeline._work(self.wav, duration,
@@ -77,7 +83,8 @@ class Chain(DikteTest):
run = self.run_chain() run = self.run_chain()
self.assertEqual(run["failures"], []) self.assertEqual(run["failures"], [])
self.assertEqual(run["done"][0], self.assertEqual(run["done"][0],
("uh, book it for Thursday", "Book it for Thursday.", "")) ("uh, book it for Thursday", "Book it for Thursday.",
"", "en"))
run["copy"].assert_called_once_with("Book it for Thursday.") run["copy"].assert_called_once_with("Book it for Thursday.")
run["press"].assert_called_once_with(self.conf["paste_shortcut"], run["press"].assert_called_once_with(self.conf["paste_shortcut"],
focus=None) focus=None)
@@ -129,7 +136,7 @@ class Chain(DikteTest):
self.conf["restore_clipboard"] = True self.conf["restore_clipboard"] = True
run = self.run_chain(paste_error=paste.PasteError("not trusted")) run = self.run_chain(paste_error=paste.PasteError("not trusted"))
self.assertEqual(run["failures"], []) self.assertEqual(run["failures"], [])
raw, text, warning = run["done"][0] raw, text, warning, _lang = run["done"][0]
self.assertIn("not trusted", warning) self.assertIn("not trusted", warning)
run["copy_bytes"].assert_not_called() run["copy_bytes"].assert_not_called()
@@ -182,17 +189,41 @@ class Chain(DikteTest):
self.assertEqual(run["transcribe"].call_args.kwargs["language"], "tr") self.assertEqual(run["transcribe"].call_args.kwargs["language"], "tr")
self.assertEqual(run["transcribe"].call_args.kwargs["prompt"], "Paraşüt") self.assertEqual(run["transcribe"].call_args.kwargs["prompt"], "Paraşüt")
def test_auto_mode_asks_for_the_detected_language_and_records_it(self):
run = self.run_chain(detected="tr")
told = run["transcribe_detected"].call_args.kwargs
self.assertEqual(told["language"], "auto")
self.assertEqual(cfg.read_history()[0]["speech_language"], "tr")
self.assertEqual(run["done"][0][3], "tr")
run["transcribe"].assert_not_called()
def test_the_detected_language_is_told_to_the_cleanup_prompt(self):
# The mock stands in for api.cleanup, which the cleanup module calls
# with (text, key, model, system_prompt, …); the prompt is the fourth.
self.conf["transcribe_prompt"] = "Paraşüt"
run = self.run_chain(detected="tr")
prompt = run["cleanup"].call_args.args[3]
# Turkish was detected, so the Turkish glossary rule is appended.
self.assertIn("KONUŞMACININ KULLANDIĞI İSİM VE TERİMLER", prompt)
def test_a_fixed_language_needs_no_detection(self):
self.conf["language"] = "en"
run = self.run_chain()
run["transcribe"].assert_called_once()
run["transcribe_detected"].assert_not_called()
self.assertEqual(cfg.read_history()[0]["speech_language"], "en")
# ---- silence and stock phrases ---------------------------------------- # ---- silence and stock phrases ----------------------------------------
def test_room_tone_costs_no_api_call(self): def test_room_tone_costs_no_api_call(self):
run = self.run_chain(rms=[0.00001] * 60) run = self.run_chain(rms=[0.00001] * 60)
run["transcribe"].assert_not_called() run["transcribe_detected"].assert_not_called()
self.assertIn("No speech", run["failures"][0]) self.assertIn("No speech", run["failures"][0])
def test_the_silence_check_can_be_switched_off(self): def test_the_silence_check_can_be_switched_off(self):
self.conf["skip_silent"] = False self.conf["skip_silent"] = False
run = self.run_chain(rms=[0.00001] * 60) run = self.run_chain(rms=[0.00001] * 60)
run["transcribe"].assert_called_once() run["transcribe_detected"].assert_called_once()
def test_a_stock_phrase_from_a_short_clip_is_thrown_away(self): def test_a_stock_phrase_from_a_short_clip_is_thrown_away(self):
run = self.run_chain(duration=2.0, transcript="Altyazı M.K.") run = self.run_chain(duration=2.0, transcript="Altyazı M.K.")
@@ -208,7 +239,7 @@ class Chain(DikteTest):
def test_a_failed_cleanup_still_pastes_the_transcript(self): def test_a_failed_cleanup_still_pastes_the_transcript(self):
run = self.run_chain(cleanup_error=api.ApiError("rate limited")) run = self.run_chain(cleanup_error=api.ApiError("rate limited"))
_raw, text, warning = run["done"][0] _raw, text, warning, _lang = run["done"][0]
self.assertEqual(text, "uh, book it for Thursday") self.assertEqual(text, "uh, book it for Thursday")
self.assertIn("rate limited", warning) self.assertIn("rate limited", warning)
run["copy"].assert_called_once_with("uh, book it for Thursday") run["copy"].assert_called_once_with("uh, book it for Thursday")
@@ -220,6 +251,9 @@ class Chain(DikteTest):
self.assertEqual(cfg.read_history()[0]["cleanup_error"], "bad key") self.assertEqual(cfg.read_history()[0]["cleanup_error"], "bad key")
def test_a_failed_transcription_ends_the_run(self): def test_a_failed_transcription_ends_the_run(self):
# This path mocks api.transcribe, so it wants
# the plain (fixed-language) transcription.
self.conf["language"] = "tr"
pipeline = worker.Pipeline(self.conf) pipeline = worker.Pipeline(self.conf)
failures = [] failures = []
pipeline.failed.connect(failures.append) pipeline.failed.connect(failures.append)
@@ -231,6 +265,9 @@ class Chain(DikteTest):
copy.assert_not_called() copy.assert_not_called()
def test_a_clipboard_that_will_not_take_it(self): def test_a_clipboard_that_will_not_take_it(self):
# This path mocks api.transcribe, so it wants
# the plain (fixed-language) transcription.
self.conf["language"] = "tr"
pipeline = worker.Pipeline(self.conf) pipeline = worker.Pipeline(self.conf)
failures = [] failures = []
pipeline.failed.connect(failures.append) pipeline.failed.connect(failures.append)
@@ -243,6 +280,9 @@ class Chain(DikteTest):
self.assertIn("wl-copy", failures[0]) self.assertIn("wl-copy", failures[0])
def test_an_unexpected_error_is_reported_rather_than_swallowed(self): def test_an_unexpected_error_is_reported_rather_than_swallowed(self):
# This path mocks api.transcribe, so it wants
# the plain (fixed-language) transcription.
self.conf["language"] = "tr"
pipeline = worker.Pipeline(self.conf) pipeline = worker.Pipeline(self.conf)
failures = [] failures = []
pipeline.failed.connect(failures.append) pipeline.failed.connect(failures.append)
@@ -280,6 +320,9 @@ class Chain(DikteTest):
run["press"].assert_not_called() run["press"].assert_not_called()
def test_a_command_that_was_cancelled(self): def test_a_command_that_was_cancelled(self):
# This path mocks api.transcribe, so it wants
# the plain (fixed-language) transcription.
self.conf["language"] = "tr"
pipeline = worker.Pipeline(self.conf) pipeline = worker.Pipeline(self.conf)
cancels = [] cancels = []
pipeline.cancelled.connect(lambda: cancels.append(True)) pipeline.cancelled.connect(lambda: cancels.append(True))
@@ -289,6 +332,9 @@ class Chain(DikteTest):
self.assertEqual(cancels, [True]) self.assertEqual(cancels, [True])
def test_an_agent_that_is_not_installed(self): def test_an_agent_that_is_not_installed(self):
# This path mocks api.transcribe, so it wants
# the plain (fixed-language) transcription.
self.conf["language"] = "tr"
pipeline = worker.Pipeline(self.conf) pipeline = worker.Pipeline(self.conf)
failures = [] failures = []
pipeline.failed.connect(failures.append) pipeline.failed.connect(failures.append)