mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Transcribe on OpenRouter too, not just OpenAI
OpenRouter mirrors OpenAI's /audio/transcriptions field for field, so one multipart request serves both providers and only the key, the base URL and the model id change. That puts a dozen speech models behind the key that was already there for the cleanup, and makes a single OpenRouter key enough for the whole chain. Its transcription endpoint takes no hint field, so the words from Cleanup rules are not sent there; they still reach the cleanup model as a glossary. Timestamps switch to openai/whisper-1, the namespaced id of the only model that returns segment times. The API tab is now grouped by role rather than by service, because a key no longer belongs to a single job: both keys sit at the top, the two jobs below. Each provider keeps its own model, so switching back and forth does not overwrite the other one's.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Dikte
|
# Dikte
|
||||||
|
|
||||||
Press `Ctrl+Space`, talk, press again. The recording goes to OpenAI for
|
Press `Ctrl+Space`, talk, press again. The recording goes to OpenAI or OpenRouter
|
||||||
transcription, a model on OpenRouter cleans it up (dropping the *uh*s, the
|
for transcription, a model on OpenRouter cleans it up (dropping the *uh*s, the
|
||||||
restarts, the missing punctuation), and the result lands in your clipboard and
|
restarts, the missing punctuation), and the result lands in your clipboard and
|
||||||
is pasted into whatever window you were typing in.
|
is pasted into whatever window you were typing in.
|
||||||
|
|
||||||
@@ -32,12 +32,12 @@ 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** for speech to text
|
Two keys go in the settings window: **OpenAI** and **OpenRouter**. Speech to text
|
||||||
(`gpt-4o-transcribe`) and **OpenRouter** for the cleanup
|
runs on either one (`gpt-4o-transcribe` by default), cleanup always on
|
||||||
(`google/gemini-3.5-flash-lite` by default, any model on the list works). They
|
OpenRouter (`google/gemini-3.5-flash-lite`), so a single OpenRouter key can
|
||||||
fall back to `OPENAI_API_KEY` and `OPENROUTER_API_KEY`, and are stored in
|
cover both. They fall back to `OPENAI_API_KEY` and `OPENROUTER_API_KEY`, and are
|
||||||
`~/.config/dikte/config.json`, mode 600. Cleanup can be switched off, in which
|
stored in `~/.config/dikte/config.json`, mode 600. Cleanup can be switched off,
|
||||||
case the raw transcript is pasted.
|
in which case the raw transcript is pasted.
|
||||||
|
|
||||||
## Using it
|
## Using it
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ needs your user in the `input` group: `sudo usermod -aG input $USER`.
|
|||||||
```
|
```
|
||||||
dikte.py entry point, tray icon, state machine, IPC
|
dikte.py entry point, tray icon, state machine, IPC
|
||||||
audio.py raw PCM capture through pw-record plus the level meter
|
audio.py raw PCM capture through pw-record plus the level meter
|
||||||
api.py OpenAI transcription and OpenRouter cleanup (stdlib only)
|
api.py transcription on either provider, OpenRouter cleanup (stdlib only)
|
||||||
worker.py transcribe → clean up → clipboard → paste
|
worker.py transcribe → clean up → clipboard → paste
|
||||||
vad.py deciding whether a recording holds speech at all
|
vad.py deciding whether a recording holds speech at all
|
||||||
filetranscribe.py file transcription: ffmpeg, chunking, timestamps
|
filetranscribe.py file transcription: ffmpeg, chunking, timestamps
|
||||||
|
|||||||
+12
-10
@@ -1,8 +1,9 @@
|
|||||||
# Dikte
|
# Dikte
|
||||||
|
|
||||||
`Ctrl+Space`'e bas, konuş, tekrar bas. Ses OpenAI'ye gidip yazıya çevrilir,
|
`Ctrl+Space`'e bas, konuş, tekrar bas. Ses OpenAI'ye ya da OpenRouter'a gidip
|
||||||
OpenRouter'daki bir model transkripti temizler (ıı'lar, tekrarlar, eksik
|
yazıya çevrilir, OpenRouter'daki bir model transkripti temizler (ıı'lar,
|
||||||
noktalama), sonuç panoya kopyalanır ve o an yazdığın pencereye yapıştırılır.
|
tekrarlar, eksik noktalama), sonuç panoya kopyalanır ve o an yazdığın pencereye
|
||||||
|
yapıştırılır.
|
||||||
|
|
||||||
KDE Plasma 6 / Wayland için yazıldı. Sistem paketleri dışında bağımlılığı yok:
|
KDE Plasma 6 / Wayland için yazıldı. Sistem paketleri dışında bağımlılığı yok:
|
||||||
sadece Python standart kütüphanesi ve PyQt6.
|
sadece Python standart kütüphanesi ve PyQt6.
|
||||||
@@ -31,12 +32,13 @@ 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: sesi yazıya çeviren **OpenAI**
|
Ayarlar penceresinde iki anahtar istenir: **OpenAI** ve **OpenRouter**. Sesi
|
||||||
(`gpt-4o-transcribe`) ve transkripti temizleyen **OpenRouter** (varsayılan
|
yazıya çevirme ikisinden birinde çalışır (varsayılan `gpt-4o-transcribe`),
|
||||||
`google/gemini-3.5-flash-lite`, listedeki her model çalışır). Boş bırakırsan
|
temizleme her zaman OpenRouter'da (`google/gemini-3.5-flash-lite`), yani tek bir
|
||||||
`OPENAI_API_KEY` ve `OPENROUTER_API_KEY` kullanılır; anahtarlar
|
OpenRouter anahtarı ikisine de yeter. Boş bırakırsan `OPENAI_API_KEY` ve
|
||||||
`~/.config/dikte/config.json` içinde, izinler 600. Temizlemeyi tamamen
|
`OPENROUTER_API_KEY` kullanılır; anahtarlar `~/.config/dikte/config.json`
|
||||||
kapatabilirsin, o zaman ham transkript yapıştırılır.
|
içinde, izinler 600. Temizlemeyi tamamen kapatabilirsin, o zaman ham transkript
|
||||||
|
yapıştırılır.
|
||||||
|
|
||||||
## Kullanım
|
## Kullanım
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ grubunda olmasını gerektirir: `sudo usermod -aG input $USER`.
|
|||||||
```
|
```
|
||||||
dikte.py giriş noktası, tepsi simgesi, durum makinesi, IPC
|
dikte.py giriş noktası, tepsi simgesi, durum makinesi, IPC
|
||||||
audio.py pw-record ile ham PCM kaydı ve seviye ölçer
|
audio.py pw-record ile ham PCM kaydı ve seviye ölçer
|
||||||
api.py OpenAI transkript + OpenRouter temizleme (yalnız stdlib)
|
api.py iki sağlayıcıda transkript + OpenRouter temizleme (yalnız stdlib)
|
||||||
worker.py transkript → temizleme → pano → yapıştırma
|
worker.py transkript → temizleme → pano → yapıştırma
|
||||||
vad.py kayıtta gerçekten konuşma var mı kararı
|
vad.py kayıtta gerçekten konuşma var mı kararı
|
||||||
filetranscribe.py dosyadan transkript: ffmpeg, parçalama, zaman damgaları
|
filetranscribe.py dosyadan transkript: ffmpeg, parçalama, zaman damgaları
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
"""OpenAI (transcription) and OpenRouter (cleanup) calls, stdlib only."""
|
"""OpenAI and OpenRouter calls, stdlib only.
|
||||||
|
|
||||||
|
Transcription runs on either provider: OpenRouter mirrors OpenAI's
|
||||||
|
/audio/transcriptions endpoint field for field, so one multipart request serves
|
||||||
|
both and only the key, the base URL and the model id change. Cleanup is always
|
||||||
|
OpenRouter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
@@ -9,11 +16,20 @@ import urllib.request
|
|||||||
|
|
||||||
from i18n import t
|
from i18n import t
|
||||||
|
|
||||||
USER_AGENT = "dikte/1.0 (+https://github.com/yusufipk/dikte)"
|
APP_URL = "https://github.com/yusufipk/dikte"
|
||||||
|
USER_AGENT = f"dikte/1.0 (+{APP_URL})"
|
||||||
|
OPENAI_URL = "https://api.openai.com/v1"
|
||||||
OPENROUTER_URL = "https://openrouter.ai/api/v1"
|
OPENROUTER_URL = "https://openrouter.ai/api/v1"
|
||||||
|
|
||||||
# Only whisper-1 returns segment-level timestamps.
|
# Where a transcription request goes; built by config.Config.transcribe_target().
|
||||||
TIMESTAMP_MODEL = "whisper-1"
|
# `service` is the name the user sees in an error, `provider` the one the code
|
||||||
|
# branches on.
|
||||||
|
Target = collections.namedtuple("Target", "provider service api_key base_url model")
|
||||||
|
|
||||||
|
|
||||||
|
def timestamp_model(provider):
|
||||||
|
"""Only whisper-1 returns segment times, and OpenRouter namespaces the id."""
|
||||||
|
return "openai/whisper-1" if provider == "openrouter" else "whisper-1"
|
||||||
|
|
||||||
|
|
||||||
class ApiError(Exception):
|
class ApiError(Exception):
|
||||||
@@ -89,35 +105,44 @@ def _multipart(fields, file_field, file_path):
|
|||||||
return bytes(out), f"multipart/form-data; boundary={boundary}"
|
return bytes(out), f"multipart/form-data; boundary={boundary}"
|
||||||
|
|
||||||
|
|
||||||
def _transcribe_request(wav_path, api_key, model, language, prompt, base_url,
|
def _headers(provider, api_key, content_type=None):
|
||||||
response_format, granularity=None, timeout=300):
|
headers = {"Authorization": f"Bearer {api_key}", "User-Agent": USER_AGENT}
|
||||||
if not api_key:
|
if content_type:
|
||||||
raise ApiError(t("OpenAI API key is empty. Add it in Settings."))
|
headers["Content-Type"] = content_type
|
||||||
fields = [("model", model), ("response_format", response_format)]
|
if provider == "openrouter":
|
||||||
|
# What OpenRouter attributes the calls to on its app leaderboard.
|
||||||
|
headers["HTTP-Referer"] = APP_URL
|
||||||
|
headers["X-Title"] = "Dikte"
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def _transcribe_request(target, wav_path, language, prompt, response_format,
|
||||||
|
granularity=None, timeout=300):
|
||||||
|
if not target.api_key:
|
||||||
|
raise ApiError(t("{service} API key is empty. Add it in Settings.",
|
||||||
|
service=target.service))
|
||||||
|
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 prompt:
|
# 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.
|
||||||
|
if prompt and target.provider == "openai":
|
||||||
fields.append(("prompt", prompt))
|
fields.append(("prompt", prompt))
|
||||||
if granularity:
|
if granularity:
|
||||||
fields.append(("timestamp_granularities[]", granularity))
|
fields.append(("timestamp_granularities[]", granularity))
|
||||||
body, ctype = _multipart(fields, "file", wav_path)
|
body, ctype = _multipart(fields, "file", wav_path)
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": ctype,
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
return _request(
|
return _request(
|
||||||
f"{base_url.rstrip('/')}/audio/transcriptions", body, headers, timeout=timeout
|
f"{target.base_url.rstrip('/')}/audio/transcriptions", body,
|
||||||
|
_headers(target.provider, target.api_key, ctype), timeout=timeout,
|
||||||
)
|
)
|
||||||
except ApiError as exc:
|
except ApiError as exc:
|
||||||
raise explain(exc, "OpenAI") from None
|
raise explain(exc, target.service) from None
|
||||||
|
|
||||||
|
|
||||||
def transcribe(wav_path, api_key, model="gpt-4o-transcribe", language="", prompt="",
|
def transcribe(target, wav_path, language="", prompt="", timeout=300):
|
||||||
base_url="https://api.openai.com/v1", timeout=300):
|
|
||||||
data = _transcribe_request(
|
data = _transcribe_request(
|
||||||
wav_path, api_key, model, language, prompt, base_url, "json", timeout=timeout
|
target, wav_path, language, prompt, "json", timeout=timeout
|
||||||
)
|
)
|
||||||
text = (data.get("text") or "").strip()
|
text = (data.get("text") or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
@@ -125,12 +150,12 @@ def transcribe(wav_path, api_key, model="gpt-4o-transcribe", language="", prompt
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def transcribe_segments(wav_path, api_key, language="", prompt="",
|
def transcribe_segments(target, wav_path, language="", prompt="", timeout=300):
|
||||||
base_url="https://api.openai.com/v1", timeout=300):
|
|
||||||
"""[(start_seconds, text)] using whisper-1's verbose response."""
|
"""[(start_seconds, text)] using whisper-1's verbose response."""
|
||||||
data = _transcribe_request(
|
data = _transcribe_request(
|
||||||
wav_path, api_key, TIMESTAMP_MODEL, language, prompt, base_url,
|
target._replace(model=timestamp_model(target.provider)),
|
||||||
"verbose_json", granularity="segment", timeout=timeout,
|
wav_path, language, prompt, "verbose_json",
|
||||||
|
granularity="segment", timeout=timeout,
|
||||||
)
|
)
|
||||||
segments = data.get("segments") or []
|
segments = data.get("segments") or []
|
||||||
out = []
|
out = []
|
||||||
@@ -148,7 +173,8 @@ def transcribe_segments(wav_path, api_key, language="", prompt="",
|
|||||||
|
|
||||||
def cleanup(text, api_key, model, system_prompt, base_url=OPENROUTER_URL, timeout=180):
|
def cleanup(text, api_key, model, system_prompt, base_url=OPENROUTER_URL, timeout=180):
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise ApiError(t("OpenRouter API key is empty. Add it in Settings."))
|
raise ApiError(t("{service} API key is empty. Add it in Settings.",
|
||||||
|
service="OpenRouter"))
|
||||||
payload = {
|
payload = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"temperature": 0,
|
"temperature": 0,
|
||||||
@@ -157,18 +183,11 @@ def cleanup(text, api_key, model, system_prompt, base_url=OPENROUTER_URL, timeou
|
|||||||
{"role": "user", "content": f"<transcript>\n{text}\n</transcript>"},
|
{"role": "user", "content": f"<transcript>\n{text}\n</transcript>"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
"HTTP-Referer": "https://github.com/yusufipk/dikte",
|
|
||||||
"X-Title": "Dikte",
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
data = _request(
|
data = _request(
|
||||||
f"{base_url.rstrip('/')}/chat/completions",
|
f"{base_url.rstrip('/')}/chat/completions",
|
||||||
json.dumps(payload).encode("utf-8"),
|
json.dumps(payload).encode("utf-8"),
|
||||||
headers,
|
_headers("openrouter", api_key, "application/json"),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
except ApiError as exc:
|
except ApiError as exc:
|
||||||
@@ -199,7 +218,8 @@ def _get_json(url, headers, timeout=20):
|
|||||||
def openrouter_key_status(api_key):
|
def openrouter_key_status(api_key):
|
||||||
"""Check the key against OpenRouter's own /key endpoint."""
|
"""Check the key against OpenRouter's own /key endpoint."""
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise ApiError(t("OpenRouter API key is empty. Add it in Settings."))
|
raise ApiError(t("{service} API key is empty. Add it in Settings.",
|
||||||
|
service="OpenRouter"))
|
||||||
try:
|
try:
|
||||||
data = _get_json(f"{OPENROUTER_URL}/key",
|
data = _get_json(f"{OPENROUTER_URL}/key",
|
||||||
{"Authorization": f"Bearer {api_key}", "User-Agent": USER_AGENT})
|
{"Authorization": f"Bearer {api_key}", "User-Agent": USER_AGENT})
|
||||||
@@ -213,18 +233,32 @@ def openrouter_key_status(api_key):
|
|||||||
usage=round(float(usage or 0), 3), limit=round(float(limit), 3))
|
usage=round(float(usage or 0), 3), limit=round(float(limit), 3))
|
||||||
|
|
||||||
|
|
||||||
def openrouter_models(api_key=""):
|
def openrouter_models(api_key="", transcription=False):
|
||||||
"""Model ids available on OpenRouter (no key required)."""
|
"""Model ids available on OpenRouter (no key required).
|
||||||
|
|
||||||
|
`transcription` narrows the list to the speech-to-text models, the only ones
|
||||||
|
/audio/transcriptions accepts. The filter is applied again on the result,
|
||||||
|
because a query parameter the API stops honouring would otherwise quietly
|
||||||
|
hand back all several hundred models.
|
||||||
|
"""
|
||||||
|
url = f"{OPENROUTER_URL}/models"
|
||||||
|
if transcription:
|
||||||
|
url += "?output_modalities=transcription"
|
||||||
headers = {"User-Agent": USER_AGENT}
|
headers = {"User-Agent": USER_AGENT}
|
||||||
if api_key:
|
if api_key:
|
||||||
headers["Authorization"] = f"Bearer {api_key}"
|
headers["Authorization"] = f"Bearer {api_key}"
|
||||||
data = _get_json(f"{OPENROUTER_URL}/models", headers)
|
models = _get_json(url, headers).get("data", [])
|
||||||
return sorted(m["id"] for m in data.get("data", []) if m.get("id"))
|
if transcription:
|
||||||
|
models = [m for m in models
|
||||||
|
if "transcription" in (m.get("architecture") or {}).get(
|
||||||
|
"output_modalities", [])]
|
||||||
|
return sorted(m["id"] for m in models if m.get("id"))
|
||||||
|
|
||||||
|
|
||||||
def openai_models(api_key, base_url="https://api.openai.com/v1"):
|
def openai_models(api_key, base_url=OPENAI_URL):
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise ApiError(t("OpenAI API key is empty. Add it in Settings."))
|
raise ApiError(t("{service} API key is empty. Add it in Settings.",
|
||||||
|
service="OpenAI"))
|
||||||
try:
|
try:
|
||||||
data = _get_json(
|
data = _get_json(
|
||||||
f"{base_url.rstrip('/')}/models",
|
f"{base_url.rstrip('/')}/models",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
|
import api
|
||||||
import i18n
|
import i18n
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +93,9 @@ DEFAULTS = {
|
|||||||
"openai_base_url": "https://api.openai.com/v1",
|
"openai_base_url": "https://api.openai.com/v1",
|
||||||
"openrouter_api_key": "",
|
"openrouter_api_key": "",
|
||||||
"openrouter_base_url": "https://openrouter.ai/api/v1",
|
"openrouter_base_url": "https://openrouter.ai/api/v1",
|
||||||
"transcribe_model": "gpt-4o-transcribe",
|
"transcribe_provider": "openai", # openai | openrouter
|
||||||
|
"transcribe_model": "gpt-4o-transcribe", # used when provider is openai
|
||||||
|
"openrouter_transcribe_model": "openai/gpt-4o-transcribe",
|
||||||
"language": "tr",
|
"language": "tr",
|
||||||
"transcribe_prompt": "",
|
"transcribe_prompt": "",
|
||||||
"cleanup_enabled": True,
|
"cleanup_enabled": True,
|
||||||
@@ -184,6 +187,15 @@ 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 transcribe_target(self):
|
||||||
|
"""Key, endpoint and model for whichever provider does speech to text."""
|
||||||
|
if self["transcribe_provider"] == "openrouter":
|
||||||
|
return api.Target("openrouter", "OpenRouter", self.openrouter_key(),
|
||||||
|
self["openrouter_base_url"],
|
||||||
|
self["openrouter_transcribe_model"])
|
||||||
|
return api.Target("openai", "OpenAI", self.openai_key(),
|
||||||
|
self["openai_base_url"], self["transcribe_model"])
|
||||||
|
|
||||||
def cleanup_prompt(self, with_timestamps=False):
|
def cleanup_prompt(self, with_timestamps=False):
|
||||||
turkish = i18n.language() == "tr"
|
turkish = i18n.language() == "tr"
|
||||||
prompt = self["cleanup_prompt"].strip() or default_cleanup_prompt()
|
prompt = self["cleanup_prompt"].strip() or default_cleanup_prompt()
|
||||||
|
|||||||
@@ -320,7 +320,9 @@ def main():
|
|||||||
server.newConnection.connect(on_connection)
|
server.newConnection.connect(on_connection)
|
||||||
app.aboutToQuit.connect(dikte.shutdown)
|
app.aboutToQuit.connect(dikte.shutdown)
|
||||||
|
|
||||||
if command == "settings" or not dikte.conf.openai_key():
|
# No key for the chosen transcription provider means nothing can work yet,
|
||||||
|
# so the settings window is the only useful thing to open.
|
||||||
|
if command == "settings" or not dikte.conf.transcribe_target().api_key:
|
||||||
dikte.open_settings()
|
dikte.open_settings()
|
||||||
elif command == "toggle":
|
elif command == "toggle":
|
||||||
QTimer.singleShot(0, dikte.toggle)
|
QTimer.singleShot(0, dikte.toggle)
|
||||||
|
|||||||
+3
-5
@@ -74,6 +74,7 @@ class FileTranscriber(QObject):
|
|||||||
if len(chunks) > 1:
|
if len(chunks) > 1:
|
||||||
self.progress.emit(t("Splitting into {count} chunks…", count=len(chunks)))
|
self.progress.emit(t("Splitting into {count} chunks…", count=len(chunks)))
|
||||||
|
|
||||||
|
target = conf.transcribe_target()
|
||||||
pieces = []
|
pieces = []
|
||||||
for index, (chunk_path, offset) in enumerate(chunks, start=1):
|
for index, (chunk_path, offset) in enumerate(chunks, start=1):
|
||||||
self._check()
|
self._check()
|
||||||
@@ -82,11 +83,10 @@ class FileTranscriber(QObject):
|
|||||||
)
|
)
|
||||||
if timestamps:
|
if timestamps:
|
||||||
segments = api.transcribe_segments(
|
segments = api.transcribe_segments(
|
||||||
|
target,
|
||||||
chunk_path,
|
chunk_path,
|
||||||
conf.openai_key(),
|
|
||||||
language=conf["language"],
|
language=conf["language"],
|
||||||
prompt=conf["transcribe_prompt"],
|
prompt=conf["transcribe_prompt"],
|
||||||
base_url=conf["openai_base_url"],
|
|
||||||
)
|
)
|
||||||
pieces.extend(
|
pieces.extend(
|
||||||
f"[{format_timestamp(start + offset)}] {text}"
|
f"[{format_timestamp(start + offset)}] {text}"
|
||||||
@@ -94,12 +94,10 @@ class FileTranscriber(QObject):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
pieces.append(api.transcribe(
|
pieces.append(api.transcribe(
|
||||||
|
target,
|
||||||
chunk_path,
|
chunk_path,
|
||||||
conf.openai_key(),
|
|
||||||
model=conf["transcribe_model"],
|
|
||||||
language=conf["language"],
|
language=conf["language"],
|
||||||
prompt=conf["transcribe_prompt"],
|
prompt=conf["transcribe_prompt"],
|
||||||
base_url=conf["openai_base_url"],
|
|
||||||
))
|
))
|
||||||
|
|
||||||
text = "\n".join(pieces) if timestamps else " ".join(pieces)
|
text = "\n".join(pieces) if timestamps else " ".join(pieces)
|
||||||
|
|||||||
@@ -93,10 +93,8 @@ TR = {
|
|||||||
"ydotool hatası: {error}\nydotoold çalışıyor mu? (systemctl --user status ydotool)",
|
"ydotool hatası: {error}\nydotoold çalışıyor mu? (systemctl --user status ydotool)",
|
||||||
|
|
||||||
# --- api errors ----------------------------------------------------
|
# --- api errors ----------------------------------------------------
|
||||||
"OpenAI API key is empty. Add it in Settings.":
|
"{service} API key is empty. Add it in Settings.":
|
||||||
"OpenAI API anahtarı boş. Ayarlar'dan gir.",
|
"{service} API anahtarı boş. Ayarlar'dan gir.",
|
||||||
"OpenRouter API key is empty. Add it in Settings.":
|
|
||||||
"OpenRouter API anahtarı boş. Ayarlar'dan gir.",
|
|
||||||
"Transcript came back empty.": "Transkript boş döndü.",
|
"Transcript came back empty.": "Transkript boş döndü.",
|
||||||
"The cleanup model returned an empty reply.": "Temizleme modeli boş yanıt döndü.",
|
"The cleanup model returned an empty reply.": "Temizleme modeli boş yanıt döndü.",
|
||||||
"Could not connect: {reason}": "Bağlantı kurulamadı: {reason}",
|
"Could not connect: {reason}": "Bağlantı kurulamadı: {reason}",
|
||||||
@@ -146,14 +144,23 @@ TR = {
|
|||||||
"Ses kayıtlarını sakla (~/.local/share/dikte/recordings)",
|
"Ses kayıtlarını sakla (~/.local/share/dikte/recordings)",
|
||||||
|
|
||||||
# --- settings: api --------------------------------------------------
|
# --- settings: api --------------------------------------------------
|
||||||
"OpenAI: speech to text": "OpenAI: sesi yazıya çevirme",
|
"Keys": "Anahtarlar",
|
||||||
"OpenRouter: transcript cleanup": "OpenRouter: transkripti temizleme",
|
"Speech to text": "Sesi yazıya çevirme",
|
||||||
|
"Transcript cleanup": "Transkripti temizleme",
|
||||||
"API key": "API anahtarı",
|
"API key": "API anahtarı",
|
||||||
"Model": "Model",
|
"Model": "Model",
|
||||||
|
"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)",
|
||||||
"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 key": "Anahtarı test et",
|
"Test": "Test et",
|
||||||
"Trying…": "Deneniyor…",
|
"Trying…": "Deneniyor…",
|
||||||
|
"Runs on OpenAI, with the key above.":
|
||||||
|
"Yukarıdaki anahtarla OpenAI üzerinde çalışır.",
|
||||||
|
"Runs on OpenRouter, with the key above. Word hints reach the cleanup "
|
||||||
|
"model only.":
|
||||||
|
"Yukarıdaki anahtarla OpenRouter üzerinde çalışır. Kelime ipuçları "
|
||||||
|
"yalnızca temizleme modeline gider.",
|
||||||
|
"Runs on OpenRouter.": "OpenRouter üzerinde çalışır.",
|
||||||
"Connection works. {count} audio models visible.":
|
"Connection works. {count} audio models visible.":
|
||||||
"Bağlantı tamam. {count} ses modeli görünüyor.",
|
"Bağlantı tamam. {count} ses modeli görünüyor.",
|
||||||
"Clean the transcript with a model": "Transkripti bir modelle temizle",
|
"Clean the transcript with a model": "Transkripti bir modelle temizle",
|
||||||
@@ -187,10 +194,10 @@ TR = {
|
|||||||
"Audio and video files": "Ses ve video dosyaları",
|
"Audio and video files": "Ses ve video dosyaları",
|
||||||
"All files": "Tüm dosyalar",
|
"All files": "Tüm dosyalar",
|
||||||
"Add timestamps": "Zaman damgası ekle",
|
"Add timestamps": "Zaman damgası ekle",
|
||||||
"Prefixes every segment with [mm:ss]. Uses whisper-1, the only model that "
|
"Prefixes every segment with [mm:ss]. Uses whisper-1 on whichever provider "
|
||||||
"returns segment times.":
|
"you picked, the only model that returns segment times.":
|
||||||
"Her bölümün başına [dd:ss] koyar. Bölüm zamanı döndüren tek model olan "
|
"Her bölümün başına [dd:ss] koyar. Bölüm zamanı döndüren tek model olan "
|
||||||
"whisper-1 kullanılır.",
|
"whisper-1, seçtiğin sağlayıcı üzerinden kullanılır.",
|
||||||
"Run the cleanup model afterwards": "Sonrasında temizleme modelinden geçir",
|
"Run the cleanup model afterwards": "Sonrasında temizleme modelinden geçir",
|
||||||
"Transcribe": "Yazıya çevir",
|
"Transcribe": "Yazıya çevir",
|
||||||
"Stop": "Durdur",
|
"Stop": "Durdur",
|
||||||
|
|||||||
+140
-46
@@ -25,7 +25,18 @@ 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_MODELS = ["gpt-4o-transcribe", "gpt-4o-mini-transcribe", "whisper-1"]
|
TRANSCRIBE_PROVIDERS = [("OpenAI", "openai"), ("OpenRouter", "openrouter")]
|
||||||
|
# Starting points for the model box; "Fetch model list" replaces them with
|
||||||
|
# whatever the provider offers today.
|
||||||
|
TRANSCRIBE_MODELS = {
|
||||||
|
"openai": ["gpt-4o-transcribe", "gpt-4o-mini-transcribe", "whisper-1"],
|
||||||
|
"openrouter": [
|
||||||
|
"openai/gpt-4o-transcribe", "openai/gpt-4o-mini-transcribe",
|
||||||
|
"openai/whisper-1", "openai/whisper-large-v3",
|
||||||
|
"openai/whisper-large-v3-turbo", "mistralai/voxtral-mini-transcribe",
|
||||||
|
"deepgram/nova-3", "google/chirp-3",
|
||||||
|
],
|
||||||
|
}
|
||||||
CLEANUP_MODELS = [
|
CLEANUP_MODELS = [
|
||||||
"google/gemini-3.5-flash-lite", "google/gemini-3.1-flash-lite",
|
"google/gemini-3.5-flash-lite", "google/gemini-3.1-flash-lite",
|
||||||
"google/gemini-2.5-flash-lite", "anthropic/claude-haiku-4.5",
|
"google/gemini-2.5-flash-lite", "anthropic/claude-haiku-4.5",
|
||||||
@@ -40,6 +51,7 @@ class SettingsWindow(QDialog):
|
|||||||
applied = pyqtSignal()
|
applied = pyqtSignal()
|
||||||
|
|
||||||
_models_loaded = pyqtSignal(list, str)
|
_models_loaded = pyqtSignal(list, str)
|
||||||
|
_transcribe_models_loaded = pyqtSignal(list, str)
|
||||||
_test_done = pyqtSignal(bool, str)
|
_test_done = pyqtSignal(bool, str)
|
||||||
_or_test_done = pyqtSignal(bool, str)
|
_or_test_done = pyqtSignal(bool, str)
|
||||||
|
|
||||||
@@ -47,6 +59,10 @@ class SettingsWindow(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
self.launch_command = launch_command
|
self.launch_command = launch_command
|
||||||
|
# Each provider keeps its own transcription model, so switching the
|
||||||
|
# provider back and forth never overwrites the other one's.
|
||||||
|
self._models = {"openai": "", "openrouter": ""}
|
||||||
|
self._shown_provider = ""
|
||||||
self.transcriber = FileTranscriber(conf, self)
|
self.transcriber = FileTranscriber(conf, self)
|
||||||
self.setWindowTitle(t("Dikte Settings"))
|
self.setWindowTitle(t("Dikte Settings"))
|
||||||
self.resize(680, 640)
|
self.resize(680, 640)
|
||||||
@@ -71,6 +87,7 @@ class SettingsWindow(QDialog):
|
|||||||
layout.addWidget(buttons)
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
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._test_done.connect(self._on_test_done)
|
self._test_done.connect(self._on_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)
|
||||||
@@ -156,56 +173,69 @@ class SettingsWindow(QDialog):
|
|||||||
page = QWidget()
|
page = QWidget()
|
||||||
outer = QVBoxLayout(page)
|
outer = QVBoxLayout(page)
|
||||||
|
|
||||||
oai = QGroupBox(t("OpenAI: speech to text"))
|
# Keys first, then the two jobs, because OpenRouter can now do both of
|
||||||
oai_form = QFormLayout(oai)
|
# them and a key no longer belongs to a single job.
|
||||||
|
keys = QGroupBox(t("Keys"))
|
||||||
|
keys_form = QFormLayout(keys)
|
||||||
self.openai_key = QLineEdit()
|
self.openai_key = QLineEdit()
|
||||||
self.openai_key.setEchoMode(QLineEdit.EchoMode.Password)
|
self.openai_key.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self.openai_key.setPlaceholderText(t("sk-… (falls back to OPENAI_API_KEY)"))
|
self.openai_key.setPlaceholderText(t("sk-… (falls back to OPENAI_API_KEY)"))
|
||||||
oai_form.addRow(t("API key"), self.openai_key)
|
self.test_button = QPushButton(t("Test"))
|
||||||
|
|
||||||
self.transcribe_model = QComboBox()
|
|
||||||
self.transcribe_model.setEditable(True)
|
|
||||||
self.transcribe_model.addItems(TRANSCRIBE_MODELS)
|
|
||||||
oai_form.addRow(t("Model"), self.transcribe_model)
|
|
||||||
|
|
||||||
self.test_button = QPushButton(t("Test key"))
|
|
||||||
self.test_button.clicked.connect(self._test_openai)
|
self.test_button.clicked.connect(self._test_openai)
|
||||||
self.test_label = QLabel("")
|
self.test_label = QLabel("")
|
||||||
self.test_label.setWordWrap(True)
|
self.test_label.setWordWrap(True)
|
||||||
row = QHBoxLayout()
|
keys_form.addRow("OpenAI", self._row(self.openai_key, self.test_button))
|
||||||
row.addWidget(self.test_button)
|
keys_form.addRow("", self.test_label)
|
||||||
row.addWidget(self.test_label, 1)
|
|
||||||
oai_form.addRow("", self._wrap(row))
|
|
||||||
outer.addWidget(oai)
|
|
||||||
|
|
||||||
orr = QGroupBox(t("OpenRouter: transcript cleanup"))
|
|
||||||
orr_form = QFormLayout(orr)
|
|
||||||
self.cleanup_enabled = QCheckBox(t("Clean the transcript with a model"))
|
|
||||||
orr_form.addRow("", self.cleanup_enabled)
|
|
||||||
|
|
||||||
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)"))
|
||||||
orr_form.addRow(t("API key"), self.openrouter_key)
|
self.or_test_button = QPushButton(t("Test"))
|
||||||
|
self.or_test_button.clicked.connect(self._test_openrouter)
|
||||||
|
self.or_test_label = QLabel("")
|
||||||
|
self.or_test_label.setWordWrap(True)
|
||||||
|
keys_form.addRow("OpenRouter", self._row(self.openrouter_key, self.or_test_button))
|
||||||
|
keys_form.addRow("", self.or_test_label)
|
||||||
|
outer.addWidget(keys)
|
||||||
|
|
||||||
|
stt = QGroupBox(t("Speech to text"))
|
||||||
|
stt_form = QFormLayout(stt)
|
||||||
|
self.transcribe_provider = QComboBox()
|
||||||
|
for label, value in TRANSCRIBE_PROVIDERS:
|
||||||
|
self.transcribe_provider.addItem(label, value)
|
||||||
|
stt_form.addRow(t("Provider"), self.transcribe_provider)
|
||||||
|
|
||||||
|
self.transcribe_model = QComboBox()
|
||||||
|
self.transcribe_model.setEditable(True)
|
||||||
|
self.refresh_transcribe_models = QPushButton(t("Fetch model list"))
|
||||||
|
self.refresh_transcribe_models.clicked.connect(self._load_transcribe_models)
|
||||||
|
stt_form.addRow(t("Model"),
|
||||||
|
self._row(self.transcribe_model, self.refresh_transcribe_models))
|
||||||
|
# Spanning rows: in the narrow field column a wrapped label gets a height
|
||||||
|
# that fits one line, and the rest of the text is cut off.
|
||||||
|
self.transcribe_note = QLabel("")
|
||||||
|
self.transcribe_note.setWordWrap(True)
|
||||||
|
stt_form.addRow(self.transcribe_note)
|
||||||
|
self.transcribe_status = QLabel("")
|
||||||
|
self.transcribe_status.setWordWrap(True)
|
||||||
|
stt_form.addRow(self.transcribe_status)
|
||||||
|
self.transcribe_provider.currentIndexChanged.connect(self._provider_changed)
|
||||||
|
outer.addWidget(stt)
|
||||||
|
|
||||||
|
orr = QGroupBox(t("Transcript cleanup"))
|
||||||
|
orr_form = QFormLayout(orr)
|
||||||
|
self.cleanup_enabled = QCheckBox(t("Clean the transcript with a model"))
|
||||||
|
orr_form.addRow("", self.cleanup_enabled)
|
||||||
|
|
||||||
self.cleanup_model = QComboBox()
|
self.cleanup_model = QComboBox()
|
||||||
self.cleanup_model.setEditable(True)
|
self.cleanup_model.setEditable(True)
|
||||||
self.cleanup_model.addItems(CLEANUP_MODELS)
|
self.cleanup_model.addItems(CLEANUP_MODELS)
|
||||||
self.refresh_models = QPushButton(t("Fetch model list"))
|
self.refresh_models = QPushButton(t("Fetch model list"))
|
||||||
self.refresh_models.clicked.connect(self._load_models)
|
self.refresh_models.clicked.connect(self._load_models)
|
||||||
model_row = QHBoxLayout()
|
orr_form.addRow(t("Model"), self._row(self.cleanup_model, self.refresh_models))
|
||||||
model_row.addWidget(self.cleanup_model, 1)
|
self.models_label = QLabel(t("Runs on OpenRouter."))
|
||||||
model_row.addWidget(self.refresh_models)
|
|
||||||
orr_form.addRow(t("Model"), self._wrap(model_row))
|
|
||||||
|
|
||||||
self.or_test_button = QPushButton(t("Test key"))
|
|
||||||
self.or_test_button.clicked.connect(self._test_openrouter)
|
|
||||||
self.models_label = QLabel("")
|
|
||||||
self.models_label.setWordWrap(True)
|
self.models_label.setWordWrap(True)
|
||||||
test_row = QHBoxLayout()
|
orr_form.addRow(self.models_label)
|
||||||
test_row.addWidget(self.or_test_button)
|
|
||||||
test_row.addWidget(self.models_label, 1)
|
|
||||||
orr_form.addRow("", self._wrap(test_row))
|
|
||||||
outer.addWidget(orr)
|
outer.addWidget(orr)
|
||||||
outer.addStretch(1)
|
outer.addStretch(1)
|
||||||
return page
|
return page
|
||||||
@@ -255,8 +285,8 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
self.file_timestamps = QCheckBox(t("Add timestamps"))
|
self.file_timestamps = QCheckBox(t("Add timestamps"))
|
||||||
self.file_timestamps.setToolTip(
|
self.file_timestamps.setToolTip(
|
||||||
t("Prefixes every segment with [mm:ss]. Uses whisper-1, the only model "
|
t("Prefixes every segment with [mm:ss]. Uses whisper-1 on whichever "
|
||||||
"that returns segment times.")
|
"provider you picked, the only model that returns segment times.")
|
||||||
)
|
)
|
||||||
layout.addWidget(self.file_timestamps)
|
layout.addWidget(self.file_timestamps)
|
||||||
|
|
||||||
@@ -385,10 +415,15 @@ class SettingsWindow(QDialog):
|
|||||||
return page
|
return page
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _wrap(layout):
|
def _row(*widgets):
|
||||||
widget = QWidget()
|
"""Widgets side by side in one form row; the first one takes the space."""
|
||||||
widget.setLayout(layout)
|
layout = QHBoxLayout()
|
||||||
return widget
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
for index, widget in enumerate(widgets):
|
||||||
|
layout.addWidget(widget, 1 if index == 0 else 0)
|
||||||
|
holder = QWidget()
|
||||||
|
holder.setLayout(layout)
|
||||||
|
return holder
|
||||||
|
|
||||||
# ---- load / save ----------------------------------------------------
|
# ---- load / save ----------------------------------------------------
|
||||||
|
|
||||||
@@ -408,9 +443,13 @@ 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.transcribe_model.setCurrentText(conf["transcribe_model"])
|
|
||||||
self.cleanup_enabled.setChecked(conf["cleanup_enabled"])
|
|
||||||
self.openrouter_key.setText(conf["openrouter_api_key"])
|
self.openrouter_key.setText(conf["openrouter_api_key"])
|
||||||
|
self._models = {"openai": conf["transcribe_model"],
|
||||||
|
"openrouter": conf["openrouter_transcribe_model"]}
|
||||||
|
self._shown_provider = ""
|
||||||
|
self._select_data(self.transcribe_provider, conf["transcribe_provider"])
|
||||||
|
self._provider_changed() # selecting index 0 fires no signal
|
||||||
|
self.cleanup_enabled.setChecked(conf["cleanup_enabled"])
|
||||||
self.cleanup_model.setCurrentText(conf["cleanup_model"])
|
self.cleanup_model.setCurrentText(conf["cleanup_model"])
|
||||||
self.cleanup_prompt.setPlainText(conf["cleanup_prompt"] or cfg.default_cleanup_prompt())
|
self.cleanup_prompt.setPlainText(conf["cleanup_prompt"] or cfg.default_cleanup_prompt())
|
||||||
self.transcribe_prompt.setPlainText(conf["transcribe_prompt"])
|
self.transcribe_prompt.setPlainText(conf["transcribe_prompt"])
|
||||||
@@ -443,9 +482,16 @@ 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["transcribe_model"] = self.transcribe_model.currentText().strip()
|
|
||||||
conf["cleanup_enabled"] = self.cleanup_enabled.isChecked()
|
|
||||||
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"
|
||||||
|
self._models[provider] = self.transcribe_model.currentText().strip()
|
||||||
|
conf["transcribe_provider"] = provider
|
||||||
|
for key, name in (("openai", "transcribe_model"),
|
||||||
|
("openrouter", "openrouter_transcribe_model")):
|
||||||
|
conf[name] = self._models[key].strip() or cfg.DEFAULTS[name]
|
||||||
|
|
||||||
|
conf["cleanup_enabled"] = self.cleanup_enabled.isChecked()
|
||||||
conf["cleanup_model"] = self.cleanup_model.currentText().strip()
|
conf["cleanup_model"] = self.cleanup_model.currentText().strip()
|
||||||
|
|
||||||
# Store an empty prompt when it matches the default, so switching the
|
# Store an empty prompt when it matches the default, so switching the
|
||||||
@@ -477,6 +523,54 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
# ---- api helpers -----------------------------------------------------
|
# ---- api helpers -----------------------------------------------------
|
||||||
|
|
||||||
|
def _provider_changed(self):
|
||||||
|
"""Swap the model box over to the newly chosen provider's own model."""
|
||||||
|
if self._shown_provider:
|
||||||
|
self._models[self._shown_provider] = self.transcribe_model.currentText().strip()
|
||||||
|
provider = self.transcribe_provider.currentData() or "openai"
|
||||||
|
self._shown_provider = provider
|
||||||
|
self.transcribe_model.clear()
|
||||||
|
self.transcribe_model.addItems(TRANSCRIBE_MODELS[provider])
|
||||||
|
self.transcribe_model.setCurrentText(self._models[provider])
|
||||||
|
self.transcribe_status.setText("")
|
||||||
|
self.transcribe_note.setText(
|
||||||
|
t("Runs on OpenRouter, with the key above. Word hints reach the "
|
||||||
|
"cleanup model only.")
|
||||||
|
if provider == "openrouter" else
|
||||||
|
t("Runs on OpenAI, with the key above.")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _load_transcribe_models(self):
|
||||||
|
"""The model list of whichever provider is selected."""
|
||||||
|
provider = self.transcribe_provider.currentData() or "openai"
|
||||||
|
self.refresh_transcribe_models.setEnabled(False)
|
||||||
|
self.transcribe_status.setText(t("Fetching model list…"))
|
||||||
|
openai_key = self.openai_key.text().strip() or self.conf.openai_key()
|
||||||
|
openrouter_key = self.openrouter_key.text().strip() or self.conf.openrouter_key()
|
||||||
|
base = self.conf["openai_base_url"]
|
||||||
|
|
||||||
|
def work():
|
||||||
|
try:
|
||||||
|
models = (api.openrouter_models(openrouter_key, transcription=True)
|
||||||
|
if provider == "openrouter"
|
||||||
|
else api.openai_models(openai_key, base))
|
||||||
|
self._transcribe_models_loaded.emit(models, "")
|
||||||
|
except api.ApiError as exc:
|
||||||
|
self._transcribe_models_loaded.emit([], str(exc))
|
||||||
|
|
||||||
|
threading.Thread(target=work, daemon=True).start()
|
||||||
|
|
||||||
|
def _on_transcribe_models_loaded(self, models, error):
|
||||||
|
self.refresh_transcribe_models.setEnabled(True)
|
||||||
|
if error:
|
||||||
|
self.transcribe_status.setText(t("Could not fetch the list: {error}", error=error))
|
||||||
|
return
|
||||||
|
current = self.transcribe_model.currentText()
|
||||||
|
self.transcribe_model.clear()
|
||||||
|
self.transcribe_model.addItems(models)
|
||||||
|
self.transcribe_model.setCurrentText(current)
|
||||||
|
self.transcribe_status.setText(t("{count} models loaded.", count=len(models)))
|
||||||
|
|
||||||
def _load_models(self):
|
def _load_models(self):
|
||||||
self.refresh_models.setEnabled(False)
|
self.refresh_models.setEnabled(False)
|
||||||
self.models_label.setText(t("Fetching model list…"))
|
self.models_label.setText(t("Fetching model list…"))
|
||||||
@@ -520,7 +614,7 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
def _test_openrouter(self):
|
def _test_openrouter(self):
|
||||||
self.or_test_button.setEnabled(False)
|
self.or_test_button.setEnabled(False)
|
||||||
self.models_label.setText(t("Trying…"))
|
self.or_test_label.setText(t("Trying…"))
|
||||||
key = self.openrouter_key.text().strip() or self.conf.openrouter_key()
|
key = self.openrouter_key.text().strip() or self.conf.openrouter_key()
|
||||||
|
|
||||||
def work():
|
def work():
|
||||||
@@ -533,7 +627,7 @@ class SettingsWindow(QDialog):
|
|||||||
|
|
||||||
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.models_label.setText(("✓ " if ok else "✗ ") + message)
|
self.or_test_label.setText(("✓ " if ok else "✗ ") + message)
|
||||||
|
|
||||||
def _on_test_done(self, ok, message):
|
def _on_test_done(self, ok, message):
|
||||||
self.test_button.setEnabled(True)
|
self.test_button.setEnabled(True)
|
||||||
|
|||||||
@@ -60,13 +60,12 @@ class Pipeline(QObject):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self.stage.emit(t("Transcribing…"))
|
self.stage.emit(t("Transcribing…"))
|
||||||
|
target = conf.transcribe_target()
|
||||||
raw = api.transcribe(
|
raw = api.transcribe(
|
||||||
|
target,
|
||||||
wav_path,
|
wav_path,
|
||||||
conf.openai_key(),
|
|
||||||
model=conf["transcribe_model"],
|
|
||||||
language=conf["language"],
|
language=conf["language"],
|
||||||
prompt=conf["transcribe_prompt"],
|
prompt=conf["transcribe_prompt"],
|
||||||
base_url=conf["openai_base_url"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if conf["filter_hallucinations"] and vad.looks_like_hallucination(raw, duration):
|
if conf["filter_hallucinations"] and vad.looks_like_hallucination(raw, duration):
|
||||||
@@ -107,7 +106,7 @@ class Pipeline(QObject):
|
|||||||
"ts": time.strftime("%Y-%m-%d %H:%M:%S"),
|
"ts": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"duration": round(duration, 1),
|
"duration": round(duration, 1),
|
||||||
"elapsed": round(time.monotonic() - started, 1),
|
"elapsed": round(time.monotonic() - started, 1),
|
||||||
"model": conf["transcribe_model"],
|
"model": target.model,
|
||||||
"cleanup_model": conf["cleanup_model"] if conf["cleanup_enabled"] else "",
|
"cleanup_model": conf["cleanup_model"] if conf["cleanup_enabled"] else "",
|
||||||
"cleanup_error": warning,
|
"cleanup_error": warning,
|
||||||
"raw": raw,
|
"raw": raw,
|
||||||
|
|||||||
Reference in New Issue
Block a user