mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Merge master: the shortcut table, local models, and a Mac still in them
Four files disagreed, and all four the same way: master had turned things the Mac branch wrote out by hand into one list to read from. Shortcuts are the whole of it. master gave every binding a row in hotkey.SHORTCUTS, so the Mac's DESKTOP_IDS is gone and CarbonHotkey reads the desktop id off that row, which also gives the new cancel key a status line on a Mac. Settings builds its four rows through master's _shortcut_row, and that one now asks _install_buttons for Install and Remove, so macOS gets a combination box and nothing to press, and everywhere else the button says the desktop's own name. dikte.py starts the listener from the same table, on macOS whatever the setting says: there is nothing installed for it to be a fallback to. The rest is two imports and a paste list that lives in paste.Desktop now.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Settings storage, in the place this system keeps a program's settings."""
|
||||
|
||||
import collections
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
@@ -7,8 +8,10 @@ import pathlib
|
||||
import sys
|
||||
|
||||
import api
|
||||
import ggml
|
||||
import i18n
|
||||
import paste
|
||||
from i18n import t
|
||||
|
||||
|
||||
def _xdg(var, default):
|
||||
@@ -377,16 +380,53 @@ DEFAULTS = {
|
||||
"ui_language": "auto", # auto | tr | en
|
||||
"openai_api_key": "",
|
||||
"openai_base_url": "https://api.openai.com/v1",
|
||||
"groq_api_key": "",
|
||||
"groq_base_url": "https://api.groq.com/openai/v1",
|
||||
"openrouter_api_key": "",
|
||||
"openrouter_base_url": "https://openrouter.ai/api/v1",
|
||||
"transcribe_provider": "openai", # openai | openrouter
|
||||
"transcribe_provider": "local", # "local", or a key of TRANSCRIBERS
|
||||
"transcribe_model": "gpt-4o-transcribe", # used when provider is openai
|
||||
"groq_transcribe_model": "whisper-large-v3-turbo",
|
||||
"openrouter_transcribe_model": "openai/gpt-4o-transcribe",
|
||||
"language": "tr",
|
||||
"transcribe_prompt": "",
|
||||
|
||||
# --- whisper.cpp, on this machine ---------------------------------------
|
||||
# The program and the model are both fetched from Settings; empty means
|
||||
# nothing has been downloaded yet, which is what opens Settings on a first
|
||||
# run.
|
||||
# Pointed at the suggestion rather than at nothing, so the settings window
|
||||
# opens with the Download button already on the right model.
|
||||
"local_model": ggml.SUGGESTED_WHISPER,
|
||||
"local_threads": 0, # 0 -> whisper.cpp picks
|
||||
"local_gpu": True,
|
||||
"local_preload": True, # load the model while Dikte starts, rather
|
||||
# than on the first dictation
|
||||
"local_binary": "", # empty -> whichever copy ggml.py finds
|
||||
|
||||
"cleanup_enabled": True,
|
||||
"cleanup_provider": "openrouter", # a name in cleanup.PROVIDERS
|
||||
"cleanup_model": "google/gemini-3.5-flash-lite",
|
||||
"cleanup_claude_model": "haiku", # Claude Code: an alias, or a full model id
|
||||
"cleanup_codex_model": "", # empty -> whatever Codex is set to
|
||||
"cleanup_reasoning": "", # empty -> whatever the model does by default
|
||||
|
||||
# --- llama.cpp, on this machine -----------------------------------------
|
||||
# Kept apart from the meeting settings on purpose. Cleanup is punctuation
|
||||
# and filler words, which a small model does in a moment; the minutes are a
|
||||
# summary of an hour, which it does not.
|
||||
"local_llm_model": "", # a file name, e.g. gemma-3-4b-it-Q4_K_M.gguf
|
||||
# Where the model list is read from; the settings window offers the
|
||||
# publishers ggml.py knows of and takes any other one that is typed in.
|
||||
"local_llm_repo": ggml.SUGGESTED_LLM[0],
|
||||
"local_llm_threads": 0,
|
||||
"local_llm_gpu": True,
|
||||
"local_llm_context": 8192,
|
||||
"local_llm_binary": "",
|
||||
"local_llm_preload": False, # heavier than whisper, so only when asked
|
||||
# Off rather than empty: a model trained to think will, and 300 tokens of
|
||||
# reasoning about a comma is 300 tokens of waiting.
|
||||
"local_llm_reasoning": "none",
|
||||
"cleanup_prompt": "", # empty -> language-specific default
|
||||
"auto_paste": True,
|
||||
"paste_shortcut": paste.desktop().shortcuts[0], # cmd+v on a Mac
|
||||
@@ -399,6 +439,10 @@ DEFAULTS = {
|
||||
"min_voiced_seconds": 0.3,
|
||||
"filter_hallucinations": True,
|
||||
"shortcut": "Ctrl+Space",
|
||||
# Ctrl+Alt+Space rather than Escape: the combination the recording started
|
||||
# with, one modifier along. Escape belongs to whatever window has focus, and
|
||||
# while you are dictating something else usually has it.
|
||||
"cancel_shortcut": "Ctrl+Alt+Space",
|
||||
"evdev_hotkey": False,
|
||||
"overlay_corner": "bottom-left",
|
||||
"keep_audio": False,
|
||||
@@ -453,6 +497,22 @@ LEGACY_PROMPTS = {
|
||||
"154fc5aca1166f00eebda705f848f0391bfbf5fe", # 1.2 English
|
||||
}
|
||||
|
||||
# Every provider speech to text can run on, and the four settings that describe
|
||||
# one. A fifth is a row here rather than another branch in transcribe_target(),
|
||||
# another key row in the settings window and another line in save and load. The
|
||||
# order is the order the provider box offers them in. `service` is the name the
|
||||
# user sees; the environment variable that stands in for an empty key is the
|
||||
# name of its setting, shouted.
|
||||
Transcriber = collections.namedtuple("Transcriber", "service key url model")
|
||||
TRANSCRIBERS = {
|
||||
"openai": Transcriber("OpenAI", "openai_api_key", "openai_base_url",
|
||||
"transcribe_model"),
|
||||
"groq": Transcriber("Groq", "groq_api_key", "groq_base_url",
|
||||
"groq_transcribe_model"),
|
||||
"openrouter": Transcriber("OpenRouter", "openrouter_api_key",
|
||||
"openrouter_base_url", "openrouter_transcribe_model"),
|
||||
}
|
||||
|
||||
# Corners used to be stored with Turkish names.
|
||||
_CORNER_MIGRATION = {
|
||||
"sol-alt": "bottom-left", "sağ-alt": "bottom-right",
|
||||
@@ -501,21 +561,76 @@ class Config:
|
||||
def get(self, key, default=None):
|
||||
return self.data.get(key, DEFAULTS.get(key, default))
|
||||
|
||||
def api_key(self, setting):
|
||||
"""A stored key, or the environment variable that shares its name."""
|
||||
return self[setting].strip() or os.environ.get(setting.upper(), "").strip()
|
||||
|
||||
def openai_key(self):
|
||||
"""Fall back to the environment when no key is stored."""
|
||||
return self["openai_api_key"].strip() or os.environ.get("OPENAI_API_KEY", "").strip()
|
||||
return self.api_key("openai_api_key")
|
||||
|
||||
def groq_key(self):
|
||||
return self.api_key("groq_api_key")
|
||||
|
||||
def openrouter_key(self):
|
||||
return self["openrouter_api_key"].strip() or os.environ.get("OPENROUTER_API_KEY", "").strip()
|
||||
return self.api_key("openrouter_api_key")
|
||||
|
||||
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"])
|
||||
"""Key, endpoint and model for whichever provider does speech to text.
|
||||
|
||||
The local one is not in the table and leaves its base URL empty on
|
||||
purpose: the server picks a port when it starts, and reading a setting
|
||||
must not be what launches a process. api.py fills the address in when it
|
||||
is about to send the request, which is the moment the server is needed
|
||||
anyway.
|
||||
"""
|
||||
name = self["transcribe_provider"]
|
||||
if name == "local":
|
||||
return api.Target("local", t("Local whisper"), "", "",
|
||||
self["local_model"])
|
||||
if name not in TRANSCRIBERS:
|
||||
# A config written by a fork, or by a version that dropped one. The
|
||||
# shipped default is not in the table, so this names the hosted one
|
||||
# to land on rather than reading it from there.
|
||||
name = "openai"
|
||||
who = TRANSCRIBERS[name]
|
||||
return api.Target(name, who.service, self.api_key(who.key),
|
||||
self[who.url], self[who.model])
|
||||
|
||||
def transcribe_ready(self):
|
||||
"""Whether speech to text could run right now, without opening Settings."""
|
||||
if self["transcribe_provider"] == "local":
|
||||
return self.local_whisper_ready()
|
||||
return bool(self.transcribe_target().api_key)
|
||||
|
||||
def local_whisper_ready(self):
|
||||
return bool(ggml.program_path(ggml.WHISPER, self["local_binary"])
|
||||
and self["local_model"]
|
||||
and ggml.have_model(ggml.whisper_model_path(self["local_model"])))
|
||||
|
||||
def local_llm_ready(self):
|
||||
return bool(ggml.program_path(ggml.LLAMA, self["local_llm_binary"])
|
||||
and self["local_llm_model"]
|
||||
and ggml.have_model(ggml.llm_model_path(self["local_llm_model"])))
|
||||
|
||||
def apply_local(self):
|
||||
"""Hand the local settings to the servers, restarting what they change."""
|
||||
ggml.whisper.configure(
|
||||
model=self["local_model"],
|
||||
threads=int(self["local_threads"]),
|
||||
gpu=bool(self["local_gpu"]),
|
||||
binary=self["local_binary"],
|
||||
)
|
||||
ggml.llm.configure(
|
||||
model=self["local_llm_model"],
|
||||
threads=int(self["local_llm_threads"]),
|
||||
gpu=bool(self["local_llm_gpu"]),
|
||||
binary=self["local_llm_binary"],
|
||||
context=int(self["local_llm_context"]),
|
||||
)
|
||||
|
||||
def uses_local_llm(self):
|
||||
"""Whether anything is set to run the local cleanup model."""
|
||||
return self["cleanup_provider"] == "local"
|
||||
|
||||
def cleanup_prompt(self, with_timestamps=False, with_speakers=False,
|
||||
subtitles=False):
|
||||
|
||||
Reference in New Issue
Block a user