mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 19:06:11 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda1398a2b | ||
|
|
6e307bd8d0 | ||
|
|
310ef8d7cf | ||
|
|
4f304e3d94 |
+1
-1
@@ -10,4 +10,4 @@ business loading Qt to answer one question.
|
|||||||
# both the .dmg's Info.plist and the AppImage's file name are built from it. A
|
# both the .dmg's Info.plist and the AppImage's file name are built from it. A
|
||||||
# build off master rather than off a tag appends the commit to it, so that a
|
# build off master rather than off a tag appends the commit to it, so that a
|
||||||
# bug report from someone running "latest" names a commit.
|
# bug report from someone running "latest" names a commit.
|
||||||
__version__ = "1.0.2"
|
__version__ = "1.1.0"
|
||||||
|
|||||||
+22
-10
@@ -48,22 +48,33 @@ LOCAL_TIMEOUT = 3600
|
|||||||
|
|
||||||
# Where a transcription request goes; built by config.Config.transcribe_target().
|
# Where a transcription request goes; built by config.Config.transcribe_target().
|
||||||
# `service` is the name the user sees in an error, `provider` the one the code
|
# `service` is the name the user sees in an error, `provider` the one the code
|
||||||
# branches on.
|
# branches on. `file_model` is what a timestamped run asks for instead of
|
||||||
Target = collections.namedtuple("Target", "provider service api_key base_url model")
|
# `model`, where the two differ; empty means the provider's own whisper.
|
||||||
|
Target = collections.namedtuple(
|
||||||
|
"Target", "provider service api_key base_url model file_model",
|
||||||
|
defaults=[""])
|
||||||
|
|
||||||
|
# What answers with segment times on OpenRouter when nothing else was chosen.
|
||||||
|
OPENROUTER_FILE_MODEL = "openai/whisper-1"
|
||||||
|
|
||||||
|
|
||||||
def timestamp_model(provider, selected=""):
|
def timestamp_model(provider, selected="", file_model=""):
|
||||||
"""Which model answers with segment times.
|
"""Which model answers with segment times.
|
||||||
|
|
||||||
OpenAI keeps them to whisper-1 and OpenRouter namespaces that id. Everything
|
OpenAI keeps them to whisper-1. Everything Groq transcribes with is a
|
||||||
Groq transcribes with is a whisper, so the model already chosen does it and
|
whisper, so the model already chosen does it and the fallback is only for a
|
||||||
the fallback is only for a provider left on its default. So is everything the
|
provider left on its default. So is everything the local server runs,
|
||||||
local server runs, whatever the file is called, and there asking for another
|
whatever the file is called, and there asking for another model would name
|
||||||
model would name one it has never heard of.
|
one it has never heard of. OpenRouter fronts several models that do times
|
||||||
|
and several that do not, and a request to the wrong one gets a transcript
|
||||||
|
with no segments in it, so the one to use is a setting of its own
|
||||||
|
(`file_model`) and whisper-1 is only where that setting is left empty.
|
||||||
"""
|
"""
|
||||||
if provider in ("groq", "local"):
|
if provider in ("groq", "local"):
|
||||||
return selected or "whisper-large-v3-turbo"
|
return selected or "whisper-large-v3-turbo"
|
||||||
return "openai/whisper-1" if provider == "openrouter" else "whisper-1"
|
if provider == "openrouter":
|
||||||
|
return file_model or OPENROUTER_FILE_MODEL
|
||||||
|
return "whisper-1"
|
||||||
|
|
||||||
|
|
||||||
# What a gateway in front of the model answers of its own accord: the request
|
# What a gateway in front of the model answers of its own accord: the request
|
||||||
@@ -444,7 +455,8 @@ 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."""
|
||||||
data = _transcribe_request(
|
data = _transcribe_request(
|
||||||
target._replace(model=timestamp_model(target.provider, target.model)),
|
target._replace(model=timestamp_model(target.provider, target.model,
|
||||||
|
target.file_model)),
|
||||||
audio_path, language, prompt, "verbose_json",
|
audio_path, language, prompt, "verbose_json",
|
||||||
granularity="segment", timeout=timeout, aborter=aborter,
|
granularity="segment", timeout=timeout, aborter=aborter,
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-1
@@ -397,6 +397,9 @@ DEFAULTS = {
|
|||||||
"transcribe_model": "gpt-4o-transcribe", # used when provider is openai
|
"transcribe_model": "gpt-4o-transcribe", # used when provider is openai
|
||||||
"groq_transcribe_model": "whisper-large-v3-turbo",
|
"groq_transcribe_model": "whisper-large-v3-turbo",
|
||||||
"openrouter_transcribe_model": "openai/gpt-4o-transcribe",
|
"openrouter_transcribe_model": "openai/gpt-4o-transcribe",
|
||||||
|
# What a timestamped run (subtitles) asks OpenRouter for: not every model
|
||||||
|
# there returns segment times. Empty -> openai/whisper-1.
|
||||||
|
"openrouter_file_model": "",
|
||||||
"language": "tr",
|
"language": "tr",
|
||||||
"transcribe_prompt": "",
|
"transcribe_prompt": "",
|
||||||
|
|
||||||
@@ -669,8 +672,9 @@ class Config:
|
|||||||
# to land on rather than reading it from there.
|
# to land on rather than reading it from there.
|
||||||
name = "openai"
|
name = "openai"
|
||||||
who = TRANSCRIBERS[name]
|
who = TRANSCRIBERS[name]
|
||||||
|
file_model = self["openrouter_file_model"] if name == "openrouter" else ""
|
||||||
return api.Target(name, who.service, self.api_key(who.key),
|
return api.Target(name, who.service, self.api_key(who.key),
|
||||||
self[who.url], self[who.model])
|
self[who.url], self[who.model], file_model.strip())
|
||||||
|
|
||||||
def transcribe_ready(self):
|
def transcribe_ready(self):
|
||||||
"""Whether speech to text could run right now, without opening Settings."""
|
"""Whether speech to text could run right now, without opening Settings."""
|
||||||
|
|||||||
@@ -228,6 +228,11 @@ TR = {
|
|||||||
"Transcript cleanup": "Transkripti temizleme",
|
"Transcript cleanup": "Transkripti temizleme",
|
||||||
"API key": "API anahtarı",
|
"API key": "API anahtarı",
|
||||||
"Model": "Model",
|
"Model": "Model",
|
||||||
|
"Audio file model": "Ses dosyası modeli",
|
||||||
|
"The model a timestamped audio file (subtitles) is sent to. Not every model on "
|
||||||
|
"OpenRouter returns segment times; empty means openai/whisper-1.":
|
||||||
|
"Zaman damgalı bir ses dosyasının (altyazı) gönderildiği model. OpenRouter'daki her "
|
||||||
|
"model segment zamanı döndürmez; boşsa openai/whisper-1 kullanılır.",
|
||||||
"Provider": "Sağlayıcı",
|
"Provider": "Sağlayıcı",
|
||||||
"sk-… (falls back to OPENAI_API_KEY)": "sk-… (boşsa OPENAI_API_KEY kullanılır)",
|
"sk-… (falls back to OPENAI_API_KEY)": "sk-… (boşsa OPENAI_API_KEY kullanılır)",
|
||||||
"gsk_… (falls back to GROQ_API_KEY)": "gsk_… (boşsa GROQ_API_KEY kullanılır)",
|
"gsk_… (falls back to GROQ_API_KEY)": "gsk_… (boşsa GROQ_API_KEY kullanılır)",
|
||||||
|
|||||||
@@ -864,6 +864,15 @@ class SettingsWindow(QDialog):
|
|||||||
self.transcribe_model_row = self._row(self.transcribe_model,
|
self.transcribe_model_row = self._row(self.transcribe_model,
|
||||||
self.refresh_transcribe_models)
|
self.refresh_transcribe_models)
|
||||||
stt_form.addRow(t("Model"), self.transcribe_model_row)
|
stt_form.addRow(t("Model"), self.transcribe_model_row)
|
||||||
|
# OpenRouter only: which of its models a timestamped run asks for.
|
||||||
|
self.file_model = QComboBox()
|
||||||
|
self.file_model.setEditable(True)
|
||||||
|
self.file_model.lineEdit().setPlaceholderText(api.OPENROUTER_FILE_MODEL)
|
||||||
|
self.file_model.setToolTip(
|
||||||
|
t("The model a timestamped audio file (subtitles) is sent to. Not every model "
|
||||||
|
"on OpenRouter returns segment times; empty means openai/whisper-1."))
|
||||||
|
self.file_model_row = self._row(self.file_model)
|
||||||
|
stt_form.addRow(t("Audio file model"), self.file_model_row)
|
||||||
# A spanning row: in the narrow field column a wrapped label gets a
|
# A spanning row: in the narrow field column a wrapped label gets a
|
||||||
# height that fits one line, and the rest of the text is cut off.
|
# height that fits one line, and the rest of the text is cut off.
|
||||||
self.transcribe_status = QLabel("")
|
self.transcribe_status = QLabel("")
|
||||||
@@ -1747,6 +1756,7 @@ class SettingsWindow(QDialog):
|
|||||||
self._shown_provider = ""
|
self._shown_provider = ""
|
||||||
self._select_data(self.transcribe_provider, conf["transcribe_provider"])
|
self._select_data(self.transcribe_provider, conf["transcribe_provider"])
|
||||||
self._provider_changed() # selecting index 0 fires no signal
|
self._provider_changed() # selecting index 0 fires no signal
|
||||||
|
self.file_model.setCurrentText(conf["openrouter_file_model"])
|
||||||
self.local_gpu.setChecked(conf["local_gpu"])
|
self.local_gpu.setChecked(conf["local_gpu"])
|
||||||
self.local_preload.setChecked(conf["local_preload"])
|
self.local_preload.setChecked(conf["local_preload"])
|
||||||
self.local_threads.setValue(int(conf["local_threads"]))
|
self.local_threads.setValue(int(conf["local_threads"]))
|
||||||
@@ -1864,6 +1874,7 @@ class SettingsWindow(QDialog):
|
|||||||
for name, who in cfg.TRANSCRIBERS.items():
|
for name, who in cfg.TRANSCRIBERS.items():
|
||||||
conf[who.key] = self._key_fields[name].text().strip()
|
conf[who.key] = self._key_fields[name].text().strip()
|
||||||
conf[who.model] = self._models[name].strip() or cfg.DEFAULTS[who.model]
|
conf[who.model] = self._models[name].strip() or cfg.DEFAULTS[who.model]
|
||||||
|
conf["openrouter_file_model"] = self.file_model.currentText().strip()
|
||||||
conf["gemini_api_key"] = self.gemini_key.text().strip()
|
conf["gemini_api_key"] = self.gemini_key.text().strip()
|
||||||
conf["opencode_api_key"] = self.opencode_key.text().strip()
|
conf["opencode_api_key"] = self.opencode_key.text().strip()
|
||||||
conf["local_model"] = self.local_whisper.selected()
|
conf["local_model"] = self.local_whisper.selected()
|
||||||
@@ -2037,6 +2048,7 @@ class SettingsWindow(QDialog):
|
|||||||
self._shown_provider = provider
|
self._shown_provider = provider
|
||||||
local = provider == "local"
|
local = provider == "local"
|
||||||
self.stt_form.setRowVisible(self.transcribe_model_row, not local)
|
self.stt_form.setRowVisible(self.transcribe_model_row, not local)
|
||||||
|
self.stt_form.setRowVisible(self.file_model_row, provider == "openrouter")
|
||||||
self.stt_form.setRowVisible(self.transcribe_status, not local)
|
self.stt_form.setRowVisible(self.transcribe_status, not local)
|
||||||
self.stt_form.setRowVisible(self.local_whisper, local)
|
self.stt_form.setRowVisible(self.local_whisper, local)
|
||||||
self.stt_form.setRowVisible(self.local_options, local)
|
self.stt_form.setRowVisible(self.local_options, local)
|
||||||
@@ -2045,8 +2057,16 @@ class SettingsWindow(QDialog):
|
|||||||
self.transcribe_model.clear()
|
self.transcribe_model.clear()
|
||||||
self.transcribe_model.addItems(TRANSCRIBE_MODELS[provider])
|
self.transcribe_model.addItems(TRANSCRIBE_MODELS[provider])
|
||||||
self.transcribe_model.setCurrentText(self._models[provider])
|
self.transcribe_model.setCurrentText(self._models[provider])
|
||||||
|
if provider == "openrouter":
|
||||||
|
self._fill_file_models(TRANSCRIBE_MODELS[provider])
|
||||||
self.transcribe_status.setText("")
|
self.transcribe_status.setText("")
|
||||||
|
|
||||||
|
def _fill_file_models(self, models):
|
||||||
|
current = self.file_model.currentText()
|
||||||
|
self.file_model.clear()
|
||||||
|
self.file_model.addItems(models)
|
||||||
|
self.file_model.setCurrentText(current)
|
||||||
|
|
||||||
def _load_transcribe_models(self):
|
def _load_transcribe_models(self):
|
||||||
"""The model list of whichever provider is selected."""
|
"""The model list of whichever provider is selected."""
|
||||||
provider = self.transcribe_provider.currentData() or "openai"
|
provider = self.transcribe_provider.currentData() or "openai"
|
||||||
@@ -2075,6 +2095,8 @@ class SettingsWindow(QDialog):
|
|||||||
self.transcribe_model.clear()
|
self.transcribe_model.clear()
|
||||||
self.transcribe_model.addItems(models)
|
self.transcribe_model.addItems(models)
|
||||||
self.transcribe_model.setCurrentText(current)
|
self.transcribe_model.setCurrentText(current)
|
||||||
|
if self._shown_provider == "openrouter":
|
||||||
|
self._fill_file_models(models)
|
||||||
self.transcribe_status.setText(t("{count} models loaded.", count=len(models)))
|
self.transcribe_status.setText(t("{count} models loaded.", count=len(models)))
|
||||||
|
|
||||||
def _load_models(self):
|
def _load_models(self):
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ class TimestampModel(unittest.TestCase):
|
|||||||
self.assertEqual(api.timestamp_model("openai", "gpt-4o-transcribe"),
|
self.assertEqual(api.timestamp_model("openai", "gpt-4o-transcribe"),
|
||||||
"whisper-1")
|
"whisper-1")
|
||||||
|
|
||||||
|
def test_openrouter_takes_the_file_model_that_was_set(self):
|
||||||
|
self.assertEqual(
|
||||||
|
api.timestamp_model("openrouter", "openai/gpt-4o-transcribe",
|
||||||
|
"openai/whisper-large-v3"),
|
||||||
|
"openai/whisper-large-v3")
|
||||||
|
|
||||||
|
def test_openrouter_with_no_file_model_falls_back_to_whisper(self):
|
||||||
|
self.assertEqual(api.timestamp_model("openrouter", "openai/gpt-4o-transcribe", ""),
|
||||||
|
"openai/whisper-1")
|
||||||
|
|
||||||
|
|
||||||
class Explain(DikteTest):
|
class Explain(DikteTest):
|
||||||
def error(self, status):
|
def error(self, status):
|
||||||
@@ -318,6 +328,13 @@ class TranscribeSegments(DikteTest):
|
|||||||
api.transcribe_segments(OPENROUTER, self.wav)
|
api.transcribe_segments(OPENROUTER, self.wav)
|
||||||
self.assertEqual(multipart_fields(calls[0])["model"], "openai/whisper-1")
|
self.assertEqual(multipart_fields(calls[0])["model"], "openai/whisper-1")
|
||||||
|
|
||||||
|
def test_openrouter_asks_for_the_file_model_when_one_is_set(self):
|
||||||
|
target = OPENROUTER._replace(file_model="mistralai/voxtral-mini-transcribe")
|
||||||
|
with fake_urlopen(self.reply([{"start": 0, "end": 1, "text": "hi"}])) as calls:
|
||||||
|
api.transcribe_segments(target, self.wav)
|
||||||
|
self.assertEqual(multipart_fields(calls[0])["model"],
|
||||||
|
"mistralai/voxtral-mini-transcribe")
|
||||||
|
|
||||||
def test_groq_stays_on_the_model_it_was_given(self):
|
def test_groq_stays_on_the_model_it_was_given(self):
|
||||||
target = GROQ._replace(model="whisper-large-v3")
|
target = GROQ._replace(model="whisper-large-v3")
|
||||||
with fake_urlopen(self.reply([{"start": 0, "end": 1, "text": "hi"}])) as calls:
|
with fake_urlopen(self.reply([{"start": 0, "end": 1, "text": "hi"}])) as calls:
|
||||||
|
|||||||
@@ -220,6 +220,19 @@ class TranscribeTarget(DikteTest):
|
|||||||
self.assertEqual(target.service, "OpenRouter")
|
self.assertEqual(target.service, "OpenRouter")
|
||||||
self.assertEqual(target.api_key, "sk-or-test")
|
self.assertEqual(target.api_key, "sk-or-test")
|
||||||
self.assertEqual(target.model, "openai/whisper-1")
|
self.assertEqual(target.model, "openai/whisper-1")
|
||||||
|
self.assertEqual(target.file_model, "")
|
||||||
|
|
||||||
|
def test_openrouter_carries_its_file_model(self):
|
||||||
|
conf = self.config(transcribe_provider="openrouter",
|
||||||
|
openrouter_api_key="sk-or-test",
|
||||||
|
openrouter_file_model=" openai/whisper-large-v3 ")
|
||||||
|
self.assertEqual(conf.transcribe_target().file_model,
|
||||||
|
"openai/whisper-large-v3")
|
||||||
|
|
||||||
|
def test_only_openrouter_has_a_file_model(self):
|
||||||
|
conf = self.config(transcribe_provider="openai", openai_api_key="sk-test",
|
||||||
|
openrouter_file_model="openai/whisper-large-v3")
|
||||||
|
self.assertEqual(conf.transcribe_target().file_model, "")
|
||||||
|
|
||||||
def test_groq_when_it_is_picked(self):
|
def test_groq_when_it_is_picked(self):
|
||||||
conf = self.config(transcribe_provider="groq", groq_api_key="gsk-test",
|
conf = self.config(transcribe_provider="groq", groq_api_key="gsk-test",
|
||||||
|
|||||||
@@ -504,6 +504,20 @@ class Settings(DikteTest):
|
|||||||
self.assertEqual(conf["transcribe_model"], "gpt-4o-transcribe")
|
self.assertEqual(conf["transcribe_model"], "gpt-4o-transcribe")
|
||||||
self.assertEqual(conf["groq_transcribe_model"], "whisper-large-v3")
|
self.assertEqual(conf["groq_transcribe_model"], "whisper-large-v3")
|
||||||
|
|
||||||
|
def test_the_file_model_is_saved_and_only_shown_for_openrouter(self):
|
||||||
|
self.write_config({"transcribe_provider": "openrouter",
|
||||||
|
"openrouter_file_model": "openai/whisper-large-v3"})
|
||||||
|
conf = cfg.Config()
|
||||||
|
window = self.window(conf)
|
||||||
|
self.assertEqual(window.file_model.currentText(), "openai/whisper-large-v3")
|
||||||
|
self.assertTrue(window.stt_form.isRowVisible(window.file_model_row))
|
||||||
|
window.file_model.setCurrentText(" deepgram/nova-3 ")
|
||||||
|
window._save()
|
||||||
|
self.assertEqual(conf["openrouter_file_model"], "deepgram/nova-3")
|
||||||
|
window.transcribe_provider.setCurrentIndex(
|
||||||
|
window.transcribe_provider.findData("openai"))
|
||||||
|
self.assertFalse(window.stt_form.isRowVisible(window.file_model_row))
|
||||||
|
|
||||||
def test_the_provider_box_offers_every_provider_config_knows(self):
|
def test_the_provider_box_offers_every_provider_config_knows(self):
|
||||||
window = self.window(cfg.Config())
|
window = self.window(cfg.Config())
|
||||||
offered = [window.transcribe_provider.itemData(i)
|
offered = [window.transcribe_provider.itemData(i)
|
||||||
|
|||||||
Reference in New Issue
Block a user