mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 19:06:11 +00:00
Add Groq transcription support
This commit is contained in:
@@ -32,10 +32,11 @@ dikte # the settings window opens on first run
|
|||||||
`install.sh` adds the `dikte` command, a menu entry, an autostart entry and the
|
`install.sh` adds the `dikte` command, a menu entry, an autostart entry and the
|
||||||
KDE shortcut.
|
KDE shortcut.
|
||||||
|
|
||||||
Two keys go in the settings window: **OpenAI** and **OpenRouter**. Speech to text
|
The settings window accepts **OpenAI**, **Groq** and **OpenRouter** keys. Speech
|
||||||
runs on either one (`gpt-4o-transcribe` by default), cleanup always on
|
to text runs on any of them (`gpt-4o-transcribe` by default), cleanup always on
|
||||||
OpenRouter (`google/gemini-3.5-flash-lite`), so a single OpenRouter key can
|
OpenRouter (`google/gemini-3.5-flash-lite`), so a single OpenRouter key can
|
||||||
cover both. They fall back to `OPENAI_API_KEY` and `OPENROUTER_API_KEY`, and are
|
cover both. They fall back to `OPENAI_API_KEY`, `GROQ_API_KEY` and
|
||||||
|
`OPENROUTER_API_KEY`, and are
|
||||||
stored in `~/.config/dikte/config.json`, mode 600. Cleanup can be switched off,
|
stored in `~/.config/dikte/config.json`, mode 600. Cleanup can be switched off,
|
||||||
in which case the raw transcript is pasted, and a thinking model's effort can be
|
in which case the raw transcript is pasted, and a thinking model's effort can be
|
||||||
set next to it.
|
set next to it.
|
||||||
|
|||||||
+4
-4
@@ -32,11 +32,11 @@ dikte # ilk açılışta ayarlar penceresi gelir
|
|||||||
`install.sh` `dikte` komutunu, menü girdisini, oturum açılışında otomatik
|
`install.sh` `dikte` komutunu, menü girdisini, oturum açılışında otomatik
|
||||||
başlatmayı ve KDE kısayolunu kurar.
|
başlatmayı ve KDE kısayolunu kurar.
|
||||||
|
|
||||||
Ayarlar penceresinde iki anahtar istenir: **OpenAI** ve **OpenRouter**. Sesi
|
Ayarlar penceresinde **OpenAI**, **Groq** ve **OpenRouter** anahtarları bulunur.
|
||||||
yazıya çevirme ikisinden birinde çalışır (varsayılan `gpt-4o-transcribe`),
|
Sesi yazıya çevirme bunlardan birinde çalışır (varsayılan `gpt-4o-transcribe`),
|
||||||
temizleme her zaman OpenRouter'da (`google/gemini-3.5-flash-lite`), yani tek bir
|
temizleme her zaman OpenRouter'da (`google/gemini-3.5-flash-lite`), yani tek bir
|
||||||
OpenRouter anahtarı ikisine de yeter. Boş bırakırsan `OPENAI_API_KEY` ve
|
OpenRouter anahtarı ikisine de yeter. Boş bırakırsan `OPENAI_API_KEY`,
|
||||||
`OPENROUTER_API_KEY` kullanılır; anahtarlar `~/.config/dikte/config.json`
|
`GROQ_API_KEY` ve `OPENROUTER_API_KEY` kullanılır; anahtarlar `~/.config/dikte/config.json`
|
||||||
içinde, izinler 600. Temizlemeyi tamamen kapatabilirsin, o zaman ham transkript
|
içinde, izinler 600. Temizlemeyi tamamen kapatabilirsin, o zaman ham transkript
|
||||||
yapıştırılır; modelin yanındaki kutudan düşünme seviyesini de seçebilirsin.
|
yapıştırılır; modelin yanındaki kutudan düşünme seviyesini de seçebilirsin.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""OpenAI and OpenRouter calls, stdlib only.
|
"""OpenAI-compatible transcription and OpenRouter calls, stdlib only.
|
||||||
|
|
||||||
Transcription runs on either provider: OpenRouter mirrors OpenAI's
|
Transcription runs on either provider: OpenRouter mirrors OpenAI's
|
||||||
/audio/transcriptions endpoint field for field, so one multipart request serves
|
/audio/transcriptions endpoint field for field, so one multipart request serves
|
||||||
@@ -20,6 +20,7 @@ APP_URL = "https://github.com/yusufipk/dikte"
|
|||||||
USER_AGENT = f"dikte/1.0 (+{APP_URL})"
|
USER_AGENT = f"dikte/1.0 (+{APP_URL})"
|
||||||
OPENAI_URL = "https://api.openai.com/v1"
|
OPENAI_URL = "https://api.openai.com/v1"
|
||||||
OPENROUTER_URL = "https://openrouter.ai/api/v1"
|
OPENROUTER_URL = "https://openrouter.ai/api/v1"
|
||||||
|
GROQ_URL = "https://api.groq.com/openai/v1"
|
||||||
|
|
||||||
# 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
|
||||||
@@ -27,8 +28,10 @@ OPENROUTER_URL = "https://openrouter.ai/api/v1"
|
|||||||
Target = collections.namedtuple("Target", "provider service api_key base_url model")
|
Target = collections.namedtuple("Target", "provider service api_key base_url model")
|
||||||
|
|
||||||
|
|
||||||
def timestamp_model(provider):
|
def timestamp_model(provider, selected=""):
|
||||||
"""Only whisper-1 returns segment times, and OpenRouter namespaces the id."""
|
"""Return a timestamp-capable model id for the selected provider."""
|
||||||
|
if provider == "groq":
|
||||||
|
return selected or "whisper-large-v3-turbo"
|
||||||
return "openai/whisper-1" if provider == "openrouter" else "whisper-1"
|
return "openai/whisper-1" if provider == "openrouter" else "whisper-1"
|
||||||
|
|
||||||
|
|
||||||
@@ -126,7 +129,7 @@ def _transcribe_request(target, wav_path, language, prompt, response_format,
|
|||||||
fields.append(("language", language))
|
fields.append(("language", language))
|
||||||
# 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.
|
# The same words still reach the cleanup model as a glossary.
|
||||||
if prompt and target.provider == "openai":
|
if prompt and target.provider != "openrouter":
|
||||||
fields.append(("prompt", prompt))
|
fields.append(("prompt", prompt))
|
||||||
if granularity:
|
if granularity:
|
||||||
fields.append(("timestamp_granularities[]", granularity))
|
fields.append(("timestamp_granularities[]", granularity))
|
||||||
@@ -153,7 +156,7 @@ def transcribe(target, wav_path, language="", prompt="", timeout=300):
|
|||||||
def transcribe_segments(target, wav_path, language="", prompt="", timeout=300):
|
def transcribe_segments(target, wav_path, language="", prompt="", timeout=300):
|
||||||
"""[(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._replace(model=timestamp_model(target.provider, target.model)),
|
||||||
wav_path, language, prompt, "verbose_json",
|
wav_path, language, prompt, "verbose_json",
|
||||||
granularity="segment", timeout=timeout,
|
granularity="segment", timeout=timeout,
|
||||||
)
|
)
|
||||||
@@ -297,17 +300,17 @@ def openrouter_models(api_key="", transcription=False):
|
|||||||
return sorted(m["id"] for m in models if m.get("id"))
|
return sorted(m["id"] for m in models if m.get("id"))
|
||||||
|
|
||||||
|
|
||||||
def openai_models(api_key, base_url=OPENAI_URL):
|
def openai_models(api_key, base_url=OPENAI_URL, service="OpenAI"):
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise ApiError(t("{service} API key is empty. Add it in Settings.",
|
raise ApiError(t("{service} API key is empty. Add it in Settings.",
|
||||||
service="OpenAI"))
|
service=service))
|
||||||
try:
|
try:
|
||||||
data = _get_json(
|
data = _get_json(
|
||||||
f"{base_url.rstrip('/')}/models",
|
f"{base_url.rstrip('/')}/models",
|
||||||
{"Authorization": f"Bearer {api_key}", "User-Agent": USER_AGENT},
|
{"Authorization": f"Bearer {api_key}", "User-Agent": USER_AGENT},
|
||||||
)
|
)
|
||||||
except ApiError as exc:
|
except ApiError as exc:
|
||||||
raise explain(exc, "OpenAI") from None
|
raise explain(exc, service) from None
|
||||||
ids = [m["id"] for m in data.get("data", []) if m.get("id")]
|
ids = [m["id"] for m in data.get("data", []) if m.get("id")]
|
||||||
audio = [i for i in ids if "transcribe" in i or "whisper" in i]
|
audio = [i for i in ids if "transcribe" in i or "whisper" in i]
|
||||||
return sorted(audio or ids)
|
return sorted(audio or ids)
|
||||||
|
|||||||
@@ -363,10 +363,13 @@ DEFAULTS = {
|
|||||||
"ui_language": "auto", # auto | tr | en
|
"ui_language": "auto", # auto | tr | en
|
||||||
"openai_api_key": "",
|
"openai_api_key": "",
|
||||||
"openai_base_url": "https://api.openai.com/v1",
|
"openai_base_url": "https://api.openai.com/v1",
|
||||||
|
"groq_api_key": "",
|
||||||
|
"groq_base_url": "https://api.groq.com/openai/v1",
|
||||||
"openrouter_api_key": "",
|
"openrouter_api_key": "",
|
||||||
"openrouter_base_url": "https://openrouter.ai/api/v1",
|
"openrouter_base_url": "https://openrouter.ai/api/v1",
|
||||||
"transcribe_provider": "openai", # openai | openrouter
|
"transcribe_provider": "openai", # openai | groq | openrouter
|
||||||
"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",
|
||||||
"openrouter_transcribe_model": "openai/gpt-4o-transcribe",
|
"openrouter_transcribe_model": "openai/gpt-4o-transcribe",
|
||||||
"language": "tr",
|
"language": "tr",
|
||||||
"transcribe_prompt": "",
|
"transcribe_prompt": "",
|
||||||
@@ -494,12 +497,19 @@ class Config:
|
|||||||
def openrouter_key(self):
|
def openrouter_key(self):
|
||||||
return self["openrouter_api_key"].strip() or os.environ.get("OPENROUTER_API_KEY", "").strip()
|
return self["openrouter_api_key"].strip() or os.environ.get("OPENROUTER_API_KEY", "").strip()
|
||||||
|
|
||||||
|
def groq_key(self):
|
||||||
|
return self["groq_api_key"].strip() or os.environ.get("GROQ_API_KEY", "").strip()
|
||||||
|
|
||||||
def transcribe_target(self):
|
def transcribe_target(self):
|
||||||
"""Key, endpoint and model for whichever provider does speech to text."""
|
"""Key, endpoint and model for whichever provider does speech to text."""
|
||||||
if self["transcribe_provider"] == "openrouter":
|
if self["transcribe_provider"] == "openrouter":
|
||||||
return api.Target("openrouter", "OpenRouter", self.openrouter_key(),
|
return api.Target("openrouter", "OpenRouter", self.openrouter_key(),
|
||||||
self["openrouter_base_url"],
|
self["openrouter_base_url"],
|
||||||
self["openrouter_transcribe_model"])
|
self["openrouter_transcribe_model"])
|
||||||
|
if self["transcribe_provider"] == "groq":
|
||||||
|
return api.Target("groq", "Groq", self.groq_key(),
|
||||||
|
self["groq_base_url"],
|
||||||
|
self["groq_transcribe_model"])
|
||||||
return api.Target("openai", "OpenAI", self.openai_key(),
|
return api.Target("openai", "OpenAI", self.openai_key(),
|
||||||
self["openai_base_url"], self["transcribe_model"])
|
self["openai_base_url"], self["transcribe_model"])
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ TR = {
|
|||||||
"Model": "Model",
|
"Model": "Model",
|
||||||
"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)",
|
||||||
"sk-or-… (falls back to OPENROUTER_API_KEY)": "sk-or-… (boşsa OPENROUTER_API_KEY kullanılır)",
|
"sk-or-… (falls back to OPENROUTER_API_KEY)": "sk-or-… (boşsa OPENROUTER_API_KEY kullanılır)",
|
||||||
"Test": "Test et",
|
"Test": "Test et",
|
||||||
"Trying…": "Deneniyor…",
|
"Trying…": "Deneniyor…",
|
||||||
|
|||||||
+77
-25
@@ -29,11 +29,14 @@ LANGUAGES = [
|
|||||||
("German", "de"), ("French", "fr"), ("Spanish", "es"), ("Arabic", "ar"),
|
("German", "de"), ("French", "fr"), ("Spanish", "es"), ("Arabic", "ar"),
|
||||||
]
|
]
|
||||||
CORNERS = ["bottom-left", "bottom-right", "top-left", "top-right"]
|
CORNERS = ["bottom-left", "bottom-right", "top-left", "top-right"]
|
||||||
TRANSCRIBE_PROVIDERS = [("OpenAI", "openai"), ("OpenRouter", "openrouter")]
|
TRANSCRIBE_PROVIDERS = [
|
||||||
|
("OpenAI", "openai"), ("Groq", "groq"), ("OpenRouter", "openrouter"),
|
||||||
|
]
|
||||||
# Starting points for the model box; "Fetch model list" replaces them with
|
# Starting points for the model box; "Fetch model list" replaces them with
|
||||||
# whatever the provider offers today.
|
# whatever the provider offers today.
|
||||||
TRANSCRIBE_MODELS = {
|
TRANSCRIBE_MODELS = {
|
||||||
"openai": ["gpt-4o-transcribe", "gpt-4o-mini-transcribe", "whisper-1"],
|
"openai": ["gpt-4o-transcribe", "gpt-4o-mini-transcribe", "whisper-1"],
|
||||||
|
"groq": ["whisper-large-v3-turbo", "whisper-large-v3"],
|
||||||
"openrouter": [
|
"openrouter": [
|
||||||
"openai/gpt-4o-transcribe", "openai/gpt-4o-mini-transcribe",
|
"openai/gpt-4o-transcribe", "openai/gpt-4o-mini-transcribe",
|
||||||
"openai/whisper-1", "openai/whisper-large-v3",
|
"openai/whisper-1", "openai/whisper-large-v3",
|
||||||
@@ -109,6 +112,7 @@ class SettingsWindow(QDialog):
|
|||||||
_models_loaded = pyqtSignal(list, str)
|
_models_loaded = pyqtSignal(list, str)
|
||||||
_transcribe_models_loaded = pyqtSignal(list, str)
|
_transcribe_models_loaded = pyqtSignal(list, str)
|
||||||
_test_done = pyqtSignal(bool, str)
|
_test_done = pyqtSignal(bool, str)
|
||||||
|
_groq_test_done = pyqtSignal(bool, str)
|
||||||
_or_test_done = pyqtSignal(bool, str)
|
_or_test_done = pyqtSignal(bool, str)
|
||||||
|
|
||||||
def __init__(self, conf, launch_command, meeting_command=None,
|
def __init__(self, conf, launch_command, meeting_command=None,
|
||||||
@@ -121,7 +125,7 @@ class SettingsWindow(QDialog):
|
|||||||
self.meetings = meetings
|
self.meetings = meetings
|
||||||
# Each provider keeps its own transcription model, so switching the
|
# Each provider keeps its own transcription model, so switching the
|
||||||
# provider back and forth never overwrites the other one's.
|
# provider back and forth never overwrites the other one's.
|
||||||
self._models = {"openai": "", "openrouter": ""}
|
self._models = {"openai": "", "groq": "", "openrouter": ""}
|
||||||
self._shown_provider = ""
|
self._shown_provider = ""
|
||||||
self.transcriber = FileTranscriber(conf, self)
|
self.transcriber = FileTranscriber(conf, self)
|
||||||
self.setWindowTitle(t("Dikte Settings"))
|
self.setWindowTitle(t("Dikte Settings"))
|
||||||
@@ -152,6 +156,7 @@ class SettingsWindow(QDialog):
|
|||||||
self._models_loaded.connect(self._on_models_loaded)
|
self._models_loaded.connect(self._on_models_loaded)
|
||||||
self._transcribe_models_loaded.connect(self._on_transcribe_models_loaded)
|
self._transcribe_models_loaded.connect(self._on_transcribe_models_loaded)
|
||||||
self._test_done.connect(self._on_test_done)
|
self._test_done.connect(self._on_test_done)
|
||||||
|
self._groq_test_done.connect(self._on_groq_test_done)
|
||||||
self._or_test_done.connect(self._on_or_test_done)
|
self._or_test_done.connect(self._on_or_test_done)
|
||||||
self.transcriber.progress.connect(self._on_file_progress)
|
self.transcriber.progress.connect(self._on_file_progress)
|
||||||
self.transcriber.finished.connect(self._on_file_finished)
|
self.transcriber.finished.connect(self._on_file_finished)
|
||||||
@@ -254,6 +259,16 @@ class SettingsWindow(QDialog):
|
|||||||
keys_form.addRow("OpenAI", self._row(self.openai_key, self.test_button))
|
keys_form.addRow("OpenAI", self._row(self.openai_key, self.test_button))
|
||||||
keys_form.addRow("", self.test_label)
|
keys_form.addRow("", self.test_label)
|
||||||
|
|
||||||
|
self.groq_key = QLineEdit()
|
||||||
|
self.groq_key.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
self.groq_key.setPlaceholderText(t("gsk_… (falls back to GROQ_API_KEY)"))
|
||||||
|
self.groq_test_button = QPushButton(t("Test"))
|
||||||
|
self.groq_test_button.clicked.connect(self._test_groq)
|
||||||
|
self.groq_test_label = QLabel("")
|
||||||
|
self.groq_test_label.setWordWrap(True)
|
||||||
|
keys_form.addRow("Groq", self._row(self.groq_key, self.groq_test_button))
|
||||||
|
keys_form.addRow("", self.groq_test_label)
|
||||||
|
|
||||||
self.openrouter_key = QLineEdit()
|
self.openrouter_key = QLineEdit()
|
||||||
self.openrouter_key.setEchoMode(QLineEdit.EchoMode.Password)
|
self.openrouter_key.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self.openrouter_key.setPlaceholderText(t("sk-or-… (falls back to OPENROUTER_API_KEY)"))
|
self.openrouter_key.setPlaceholderText(t("sk-or-… (falls back to OPENROUTER_API_KEY)"))
|
||||||
@@ -368,7 +383,7 @@ class SettingsWindow(QDialog):
|
|||||||
how = QGroupBox(t("How it runs"))
|
how = QGroupBox(t("How it runs"))
|
||||||
how_form = QFormLayout(how)
|
how_form = QFormLayout(how)
|
||||||
self.assistant_shortcut = self._shortcut_box(t("none"))
|
self.assistant_shortcut = self._shortcut_box(t("none"))
|
||||||
install = QPushButton(t("Install as a KDE shortcut"))
|
install = QPushButton(t("Install as a global shortcut"))
|
||||||
install.clicked.connect(self._install_ask_shortcut)
|
install.clicked.connect(self._install_ask_shortcut)
|
||||||
remove = QPushButton(t("Remove"))
|
remove = QPushButton(t("Remove"))
|
||||||
remove.clicked.connect(self._remove_ask_shortcut)
|
remove.clicked.connect(self._remove_ask_shortcut)
|
||||||
@@ -630,7 +645,7 @@ class SettingsWindow(QDialog):
|
|||||||
recording_form.addRow("", self.meeting_keep_audio)
|
recording_form.addRow("", self.meeting_keep_audio)
|
||||||
|
|
||||||
self.meeting_shortcut = self._shortcut_box(t("none"))
|
self.meeting_shortcut = self._shortcut_box(t("none"))
|
||||||
install = QPushButton(t("Install as a KDE shortcut"))
|
install = QPushButton(t("Install as a global shortcut"))
|
||||||
install.clicked.connect(self._install_meeting_shortcut)
|
install.clicked.connect(self._install_meeting_shortcut)
|
||||||
remove = QPushButton(t("Remove"))
|
remove = QPushButton(t("Remove"))
|
||||||
remove.clicked.connect(self._remove_meeting_shortcut)
|
remove.clicked.connect(self._remove_meeting_shortcut)
|
||||||
@@ -784,7 +799,7 @@ class SettingsWindow(QDialog):
|
|||||||
form.addRow(t("Shortcut"), self.shortcut)
|
form.addRow(t("Shortcut"), self.shortcut)
|
||||||
layout.addLayout(form)
|
layout.addLayout(form)
|
||||||
|
|
||||||
install = QPushButton(t("Install as a KDE shortcut"))
|
install = QPushButton(t("Install as a global shortcut"))
|
||||||
install.clicked.connect(self._install_shortcut)
|
install.clicked.connect(self._install_shortcut)
|
||||||
remove = QPushButton(t("Remove"))
|
remove = QPushButton(t("Remove"))
|
||||||
remove.clicked.connect(self._remove_shortcut)
|
remove.clicked.connect(self._remove_shortcut)
|
||||||
@@ -920,8 +935,10 @@ class SettingsWindow(QDialog):
|
|||||||
self.keep_audio.setChecked(conf["keep_audio"])
|
self.keep_audio.setChecked(conf["keep_audio"])
|
||||||
|
|
||||||
self.openai_key.setText(conf["openai_api_key"])
|
self.openai_key.setText(conf["openai_api_key"])
|
||||||
|
self.groq_key.setText(conf["groq_api_key"])
|
||||||
self.openrouter_key.setText(conf["openrouter_api_key"])
|
self.openrouter_key.setText(conf["openrouter_api_key"])
|
||||||
self._models = {"openai": conf["transcribe_model"],
|
self._models = {"openai": conf["transcribe_model"],
|
||||||
|
"groq": conf["groq_transcribe_model"],
|
||||||
"openrouter": conf["openrouter_transcribe_model"]}
|
"openrouter": conf["openrouter_transcribe_model"]}
|
||||||
self._shown_provider = ""
|
self._shown_provider = ""
|
||||||
self._select_data(self.transcribe_provider, conf["transcribe_provider"])
|
self._select_data(self.transcribe_provider, conf["transcribe_provider"])
|
||||||
@@ -1001,12 +1018,14 @@ class SettingsWindow(QDialog):
|
|||||||
conf["keep_audio"] = self.keep_audio.isChecked()
|
conf["keep_audio"] = self.keep_audio.isChecked()
|
||||||
|
|
||||||
conf["openai_api_key"] = self.openai_key.text().strip()
|
conf["openai_api_key"] = self.openai_key.text().strip()
|
||||||
|
conf["groq_api_key"] = self.groq_key.text().strip()
|
||||||
conf["openrouter_api_key"] = self.openrouter_key.text().strip()
|
conf["openrouter_api_key"] = self.openrouter_key.text().strip()
|
||||||
|
|
||||||
provider = self.transcribe_provider.currentData() or "openai"
|
provider = self.transcribe_provider.currentData() or "openai"
|
||||||
self._models[provider] = self.transcribe_model.currentText().strip()
|
self._models[provider] = self.transcribe_model.currentText().strip()
|
||||||
conf["transcribe_provider"] = provider
|
conf["transcribe_provider"] = provider
|
||||||
for key, name in (("openai", "transcribe_model"),
|
for key, name in (("openai", "transcribe_model"),
|
||||||
|
("groq", "groq_transcribe_model"),
|
||||||
("openrouter", "openrouter_transcribe_model")):
|
("openrouter", "openrouter_transcribe_model")):
|
||||||
conf[name] = self._models[key].strip() or cfg.DEFAULTS[name]
|
conf[name] = self._models[key].strip() or cfg.DEFAULTS[name]
|
||||||
|
|
||||||
@@ -1108,14 +1127,21 @@ class SettingsWindow(QDialog):
|
|||||||
self.refresh_transcribe_models.setEnabled(False)
|
self.refresh_transcribe_models.setEnabled(False)
|
||||||
self.transcribe_status.setText(t("Fetching model list…"))
|
self.transcribe_status.setText(t("Fetching model list…"))
|
||||||
openai_key = self.openai_key.text().strip() or self.conf.openai_key()
|
openai_key = self.openai_key.text().strip() or self.conf.openai_key()
|
||||||
|
groq_key = self.groq_key.text().strip() or self.conf.groq_key()
|
||||||
openrouter_key = self.openrouter_key.text().strip() or self.conf.openrouter_key()
|
openrouter_key = self.openrouter_key.text().strip() or self.conf.openrouter_key()
|
||||||
base = self.conf["openai_base_url"]
|
|
||||||
|
|
||||||
def work():
|
def work():
|
||||||
try:
|
try:
|
||||||
models = (api.openrouter_models(openrouter_key, transcription=True)
|
if provider == "openrouter":
|
||||||
if provider == "openrouter"
|
models = api.openrouter_models(openrouter_key, transcription=True)
|
||||||
else api.openai_models(openai_key, base))
|
elif provider == "groq":
|
||||||
|
models = api.openai_models(
|
||||||
|
groq_key, self.conf["groq_base_url"], "Groq"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
models = api.openai_models(
|
||||||
|
openai_key, self.conf["openai_base_url"]
|
||||||
|
)
|
||||||
self._transcribe_models_loaded.emit(models, "")
|
self._transcribe_models_loaded.emit(models, "")
|
||||||
except api.ApiError as exc:
|
except api.ApiError as exc:
|
||||||
self._transcribe_models_loaded.emit([], str(exc))
|
self._transcribe_models_loaded.emit([], str(exc))
|
||||||
@@ -1188,6 +1214,25 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
threading.Thread(target=work, daemon=True).start()
|
threading.Thread(target=work, daemon=True).start()
|
||||||
|
|
||||||
|
def _test_groq(self):
|
||||||
|
self.groq_test_button.setEnabled(False)
|
||||||
|
self.groq_test_label.setText(t("Trying…"))
|
||||||
|
key = self.groq_key.text().strip() or self.conf.groq_key()
|
||||||
|
|
||||||
|
def work():
|
||||||
|
try:
|
||||||
|
models = api.openai_models(
|
||||||
|
key, self.conf["groq_base_url"], "Groq"
|
||||||
|
)
|
||||||
|
self._groq_test_done.emit(
|
||||||
|
True, t("Connection works. {count} audio models visible.",
|
||||||
|
count=len(models))
|
||||||
|
)
|
||||||
|
except api.ApiError as exc:
|
||||||
|
self._groq_test_done.emit(False, str(exc))
|
||||||
|
|
||||||
|
threading.Thread(target=work, daemon=True).start()
|
||||||
|
|
||||||
def _on_or_test_done(self, ok, message):
|
def _on_or_test_done(self, ok, message):
|
||||||
self.or_test_button.setEnabled(True)
|
self.or_test_button.setEnabled(True)
|
||||||
self.or_test_label.setText(("✓ " if ok else "✗ ") + message)
|
self.or_test_label.setText(("✓ " if ok else "✗ ") + message)
|
||||||
@@ -1196,6 +1241,10 @@ class SettingsWindow(QDialog):
|
|||||||
self.test_button.setEnabled(True)
|
self.test_button.setEnabled(True)
|
||||||
self.test_label.setText(("✓ " if ok else "✗ ") + message)
|
self.test_label.setText(("✓ " if ok else "✗ ") + message)
|
||||||
|
|
||||||
|
def _on_groq_test_done(self, ok, message):
|
||||||
|
self.groq_test_button.setEnabled(True)
|
||||||
|
self.groq_test_label.setText(("✓ " if ok else "✗ ") + message)
|
||||||
|
|
||||||
# ---- audio file ------------------------------------------------------
|
# ---- audio file ------------------------------------------------------
|
||||||
|
|
||||||
def _choose_file(self):
|
def _choose_file(self):
|
||||||
@@ -1280,7 +1329,7 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
def _install_shortcut(self):
|
def _install_shortcut(self):
|
||||||
combo = self.shortcut.currentText().strip() or "Ctrl+Space"
|
combo = self.shortcut.currentText().strip() or "Ctrl+Space"
|
||||||
clashes = hotkey.conflicting_shortcuts(combo)
|
clashes = hotkey.conflicting_shortcuts(combo) if hotkey.desktop_name() == "KDE" else []
|
||||||
if clashes:
|
if clashes:
|
||||||
answer = QMessageBox.question(
|
answer = QMessageBox.question(
|
||||||
self, t("Shortcut conflict"),
|
self, t("Shortcut conflict"),
|
||||||
@@ -1289,7 +1338,7 @@ class SettingsWindow(QDialog):
|
|||||||
)
|
)
|
||||||
if answer != QMessageBox.StandardButton.Yes:
|
if answer != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
ok, message = hotkey.install_kde_shortcut(combo, self.launch_command)
|
ok, message = hotkey.install_shortcut(combo, self.launch_command)
|
||||||
QMessageBox.information(self, t("Shortcut"), message)
|
QMessageBox.information(self, t("Shortcut"), message)
|
||||||
if ok:
|
if ok:
|
||||||
self.conf["shortcut"] = combo
|
self.conf["shortcut"] = combo
|
||||||
@@ -1297,14 +1346,15 @@ class SettingsWindow(QDialog):
|
|||||||
self._refresh_shortcut_status()
|
self._refresh_shortcut_status()
|
||||||
|
|
||||||
def _remove_shortcut(self):
|
def _remove_shortcut(self):
|
||||||
hotkey.remove_kde_shortcut()
|
hotkey.remove_shortcut()
|
||||||
self._refresh_shortcut_status()
|
self._refresh_shortcut_status()
|
||||||
|
|
||||||
def _refresh_shortcut_status(self):
|
def _refresh_shortcut_status(self):
|
||||||
current = hotkey.kde_shortcut_status()
|
current = hotkey.shortcut_status()
|
||||||
self.shortcut_status.setText(
|
self.shortcut_status.setText(
|
||||||
t("Registered in KDE: {shortcut}", shortcut=current) if current
|
t("Registered in {desktop}: {shortcut}",
|
||||||
else t("No KDE shortcut installed.")
|
desktop=hotkey.desktop_name(), shortcut=current) if current
|
||||||
|
else t("No global shortcut installed.")
|
||||||
)
|
)
|
||||||
|
|
||||||
def _install_meeting_shortcut(self):
|
def _install_meeting_shortcut(self):
|
||||||
@@ -1322,7 +1372,7 @@ class SettingsWindow(QDialog):
|
|||||||
)
|
)
|
||||||
if answer != QMessageBox.StandardButton.Yes:
|
if answer != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
ok, message = hotkey.install_kde_shortcut(
|
ok, message = hotkey.install_shortcut(
|
||||||
combo, self.meeting_command, name="Dikte: start/end a meeting recording",
|
combo, self.meeting_command, name="Dikte: start/end a meeting recording",
|
||||||
desktop_id=hotkey.MEETING_DESKTOP_ID,
|
desktop_id=hotkey.MEETING_DESKTOP_ID,
|
||||||
)
|
)
|
||||||
@@ -1333,14 +1383,15 @@ class SettingsWindow(QDialog):
|
|||||||
self._refresh_meeting_shortcut_status()
|
self._refresh_meeting_shortcut_status()
|
||||||
|
|
||||||
def _remove_meeting_shortcut(self):
|
def _remove_meeting_shortcut(self):
|
||||||
hotkey.remove_kde_shortcut(hotkey.MEETING_DESKTOP_ID)
|
hotkey.remove_shortcut(hotkey.MEETING_DESKTOP_ID)
|
||||||
self._refresh_meeting_shortcut_status()
|
self._refresh_meeting_shortcut_status()
|
||||||
|
|
||||||
def _refresh_meeting_shortcut_status(self):
|
def _refresh_meeting_shortcut_status(self):
|
||||||
current = hotkey.kde_shortcut_status(hotkey.MEETING_DESKTOP_ID)
|
current = hotkey.shortcut_status(hotkey.MEETING_DESKTOP_ID)
|
||||||
self.meeting_shortcut_status.setText(
|
self.meeting_shortcut_status.setText(
|
||||||
t("Registered in KDE: {shortcut}", shortcut=current) if current
|
t("Registered in {desktop}: {shortcut}",
|
||||||
else t("No KDE shortcut installed. The tray menu starts a meeting too.")
|
desktop=hotkey.desktop_name(), shortcut=current) if current
|
||||||
|
else t("No global shortcut installed. The tray menu starts a meeting too.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- Claude ----------------------------------------------------------
|
# ---- Claude ----------------------------------------------------------
|
||||||
@@ -1360,7 +1411,7 @@ class SettingsWindow(QDialog):
|
|||||||
)
|
)
|
||||||
if answer != QMessageBox.StandardButton.Yes:
|
if answer != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
ok, message = hotkey.install_kde_shortcut(
|
ok, message = hotkey.install_shortcut(
|
||||||
combo, self.ask_command, name="Dikte: ask Claude Code",
|
combo, self.ask_command, name="Dikte: ask Claude Code",
|
||||||
desktop_id=hotkey.ASK_DESKTOP_ID,
|
desktop_id=hotkey.ASK_DESKTOP_ID,
|
||||||
)
|
)
|
||||||
@@ -1371,14 +1422,15 @@ class SettingsWindow(QDialog):
|
|||||||
self._refresh_ask_shortcut_status()
|
self._refresh_ask_shortcut_status()
|
||||||
|
|
||||||
def _remove_ask_shortcut(self):
|
def _remove_ask_shortcut(self):
|
||||||
hotkey.remove_kde_shortcut(hotkey.ASK_DESKTOP_ID)
|
hotkey.remove_shortcut(hotkey.ASK_DESKTOP_ID)
|
||||||
self._refresh_ask_shortcut_status()
|
self._refresh_ask_shortcut_status()
|
||||||
|
|
||||||
def _refresh_ask_shortcut_status(self):
|
def _refresh_ask_shortcut_status(self):
|
||||||
current = hotkey.kde_shortcut_status(hotkey.ASK_DESKTOP_ID)
|
current = hotkey.shortcut_status(hotkey.ASK_DESKTOP_ID)
|
||||||
self.assistant_shortcut_status.setText(
|
self.assistant_shortcut_status.setText(
|
||||||
t("Registered in KDE: {shortcut}", shortcut=current) if current
|
t("Registered in {desktop}: {shortcut}",
|
||||||
else t("No KDE shortcut installed. The tray menu asks it too.")
|
desktop=hotkey.desktop_name(), shortcut=current) if current
|
||||||
|
else t("No global shortcut installed. The tray menu asks it too.")
|
||||||
)
|
)
|
||||||
|
|
||||||
def _assistant_provider_changed(self):
|
def _assistant_provider_changed(self):
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import api
|
||||||
|
import config
|
||||||
|
import settings_ui
|
||||||
|
|
||||||
|
|
||||||
|
class GroqCompatibilityTests(unittest.TestCase):
|
||||||
|
def test_old_config_gains_groq_defaults(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = pathlib.Path(directory) / "config.json"
|
||||||
|
path.write_text(json.dumps({"transcribe_provider": "openai"}), encoding="utf-8")
|
||||||
|
with mock.patch.object(config, "CONFIG_FILE", path):
|
||||||
|
conf = config.Config()
|
||||||
|
self.assertEqual(conf["groq_base_url"], api.GROQ_URL)
|
||||||
|
self.assertEqual(conf["groq_transcribe_model"], "whisper-large-v3-turbo")
|
||||||
|
|
||||||
|
def test_groq_target_and_timestamp_model(self):
|
||||||
|
with mock.patch.object(config.Config, "load"):
|
||||||
|
conf = config.Config()
|
||||||
|
conf["transcribe_provider"] = "groq"
|
||||||
|
conf["groq_api_key"] = "test-key"
|
||||||
|
target = conf.transcribe_target()
|
||||||
|
self.assertEqual(target.provider, "groq")
|
||||||
|
self.assertEqual(target.base_url, api.GROQ_URL)
|
||||||
|
self.assertEqual(api.timestamp_model(target.provider, target.model), target.model)
|
||||||
|
|
||||||
|
def test_groq_is_available_in_settings(self):
|
||||||
|
self.assertIn(("Groq", "groq"), settings_ui.TRANSCRIBE_PROVIDERS)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user