Skip to content

Commit 7096b1d

Browse files
committed
feat(setup): add Force CPU toggle with guard-rail warning
User request: a toggle to force CPU mode (disable GPU acceleration for all ASR backends), in the same style as the FP32/INT8 quant toggle. Plus a guard-rail label that tells the user what they lose/gain based on detected GPU VRAM. UI added: new "Performance" QGroupBox between Parakeet and Sortformer: ┌─ Performance ───────────────────────────────────┐ │ GPU [━━●] CPU │ ← centered ToggleSwitch │ ⚠ CPU mode forced — losing GPU acceleration │ ← dynamic warning │ (8.0 GB VRAM available). Parakeet FP32 will be │ │ ~6× slower; Canary will be unusably slow. │ └─────────────────────────────────────────────────┘ Guard-rail (label updated live by ToggleSwitch.toggled signal): - Toggle OFF + GPU ≥ 4 GB → "✓ GPU acceleration enabled" (green) - Toggle OFF + GPU < 4 GB → "⚠ may OOM, consider INT8" (orange) - Toggle OFF + no GPU → "ℹ No GPU detected — running on CPU regardless" - Toggle ON + GPU ≥ 4 GB → "⚠ losing GPU acceleration, Parakeet 6× slower" - Toggle ON + GPU < 4 GB → "ℹ FP32 likely OOM anyway, int8 reasonable" - Toggle ON + no GPU → "ℹ no-op, CPU is already used" Plumbing: - save_config gets a new force_cpu param. Writes DICTEE_FORCE_CPU="1" or "0" so the daemon-restart detection sees the change. - DICTEE_FORCE_CPU added to _ASR_KEYS and _new_asr in _on_apply, so toggling triggers a synchronous dictee.service restart (15 s timeout). - dictee.conf.example: new "GPU / CPU mode (advanced)" section documenting DICTEE_FORCE_CPU values and use cases. - pkg/dictee/usr/share/dictee/dictee.conf.example synced. Requires the companion Rust fix (commit e8c8de8) for explicit value parsing — otherwise writing "0" would still be interpreted as "force CPU".
1 parent e8c8de8 commit 7096b1d

2 files changed

Lines changed: 111 additions & 4 deletions

File tree

dictee-setup.py

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ def save_config(backend, lang_source, lang_target, clipboard=True,
383383
trpp_states=None, trpp_short_text_max=3,
384384
cheatsheet_mod="", cheatsheet_key_seq="",
385385
parakeet_quant=None,
386+
force_cpu=None,
386387
mark_setup_done=True):
387388
"""Update dictee.conf preserving comments and structure.
388389

@@ -411,6 +412,11 @@ def save_config(backend, lang_source, lang_target, clipboard=True,
411412
# untouched (preserve existing user value or comment).
412413
**({"DICTEE_PARAKEET_QUANT": parakeet_quant}
413414
if parakeet_quant in ("fp32", "int8") else {}),
415+
# Force CPU mode (disables GPU acceleration for all ASR backends).
416+
# We write "1" or "0" explicitly so the daemon restart detects the
417+
# change. Rust side parses both truthy ("1"/"true"/"yes") and falsy.
418+
**({"DICTEE_FORCE_CPU": "1" if force_cpu else "0"}
419+
if force_cpu is not None else {}),
414420
"DICTEE_PTT_MODE": ptt_mode,
415421
"DICTEE_PTT_KEY": str(ptt_key),
416422
"DICTEE_POSTPROCESS": "true" if postprocess else "false",
@@ -8182,6 +8188,52 @@ def _make_model_label_html(self, model, active_quant, recommended_quant):
81828188
f"</p>"
81838189
)
81848190

8191+
def _refresh_force_cpu_warning(self):
8192+
"""Update the warning label under the Force CPU toggle.
8193+
Guard-rail: tell the user what they lose / gain by switching modes,
8194+
based on detected GPU VRAM. Called by ToggleSwitch.toggled and once
8195+
at build time."""
8196+
if not hasattr(self, '_lbl_force_cpu_warning') or not hasattr(self, 'tgl_force_cpu'):
8197+
return
8198+
forcing_cpu = self.tgl_force_cpu.isChecked()
8199+
total_vram, _free = get_gpu_vram_gb()
8200+
8201+
if forcing_cpu:
8202+
# User wants CPU. Warn if GPU was capable.
8203+
if total_vram >= 4:
8204+
msg = ("⚠ " + _("CPU mode forced — losing GPU acceleration "
8205+
"({:.1f} GB VRAM available). Parakeet FP32 will "
8206+
"be ~6× slower; Canary will be unusably slow.")
8207+
.format(total_vram))
8208+
color = "#d8a000" # orange warning
8209+
elif total_vram > 0:
8210+
msg = ("ℹ " + _("CPU mode forced — GPU has only {:.1f} GB VRAM, "
8211+
"FP32 likely OOM anyway. int8 on CPU is a "
8212+
"reasonable fallback.").format(total_vram))
8213+
color = "#7a7a7a" # neutral grey
8214+
else:
8215+
msg = ("ℹ " + _("No GPU detected — CPU is the only option "
8216+
"(this toggle is a no-op)."))
8217+
color = "#7a7a7a"
8218+
else:
8219+
# User wants GPU (or default).
8220+
if total_vram >= 4:
8221+
msg = ("✓ " + _("GPU acceleration enabled "
8222+
"({:.1f} GB VRAM detected).").format(total_vram))
8223+
color = "#3a7a3a" # green ok
8224+
elif total_vram > 0:
8225+
msg = ("⚠ " + _("GPU has only {:.1f} GB VRAM — Parakeet FP32 "
8226+
"may OOM at load. Consider toggling CPU "
8227+
"or using INT8.").format(total_vram))
8228+
color = "#d8a000"
8229+
else:
8230+
msg = ("ℹ " + _("No GPU detected — running on CPU regardless "
8231+
"of this toggle."))
8232+
color = "#7a7a7a"
8233+
8234+
self._lbl_force_cpu_warning.setText(
8235+
f"<p style='font-size: 9pt; color: {color};'>{msg}</p>")
8236+
81858237
def _apply_block_opacity(self, widget, opacity):
81868238
"""Apply or remove QGraphicsOpacityEffect on a widget.
81878239
opacity >= 1.0 → restore full opacity (effect removed).
@@ -8379,6 +8431,38 @@ def _build_model_row(layout, model):
83798431

83808432
lay_outer.addWidget(parakeet_box)
83818433

8434+
# === Performance group box (Force CPU toggle, applies to all backends) ===
8435+
perf_box = QGroupBox(_("Performance"))
8436+
perf_lay = QVBoxLayout(perf_box)
8437+
perf_lay.setContentsMargins(12, 12, 12, 10)
8438+
perf_lay.setSpacing(6)
8439+
8440+
force_cpu_active = (self.conf.get("DICTEE_FORCE_CPU", "0").lower()
8441+
in ("1", "true", "yes"))
8442+
8443+
# Centered GPU [toggle] CPU row
8444+
force_row = QHBoxLayout()
8445+
force_row.setContentsMargins(0, 0, 0, 0)
8446+
force_row.addStretch()
8447+
force_row.addWidget(QLabel("GPU"))
8448+
self.tgl_force_cpu = ToggleSwitch("")
8449+
self.tgl_force_cpu.setChecked(force_cpu_active)
8450+
self.tgl_force_cpu.toggled.connect(self._refresh_force_cpu_warning)
8451+
force_row.addWidget(self.tgl_force_cpu)
8452+
force_row.addWidget(QLabel("CPU"))
8453+
force_row.addStretch()
8454+
perf_lay.addLayout(force_row)
8455+
8456+
# Dynamic warning label below the toggle (guard-rail)
8457+
self._lbl_force_cpu_warning = QLabel()
8458+
self._lbl_force_cpu_warning.setWordWrap(True)
8459+
self._lbl_force_cpu_warning.setStyleSheet("QLabel { padding-left: 4px; }")
8460+
perf_lay.addWidget(self._lbl_force_cpu_warning)
8461+
# Initial render
8462+
self._refresh_force_cpu_warning()
8463+
8464+
lay_outer.addWidget(perf_box)
8465+
83828466
# === Sortformer group box (separate, simple title) ===
83838467
sortformer_box = QGroupBox(_("Sortformer"))
83848468
sortformer_lay = QVBoxLayout(sortformer_box)
@@ -17695,7 +17779,8 @@ def _on_apply(self, show_message=True, mark_setup_done=True):
1769517779
_old_asr = {}
1769617780
_ASR_KEYS = ("DICTEE_ASR_BACKEND", "DICTEE_WHISPER_MODEL",
1769717781
"DICTEE_WHISPER_LANG", "DICTEE_VOSK_MODEL",
17698-
"DICTEE_AUDIO_SOURCE", "DICTEE_PARAKEET_QUANT")
17782+
"DICTEE_AUDIO_SOURCE", "DICTEE_PARAKEET_QUANT",
17783+
"DICTEE_FORCE_CPU")
1769917784
try:
1770017785
if os.path.isfile(CONF_PATH):
1770117786
with open(CONF_PATH) as _f:
@@ -17938,6 +18023,9 @@ def _on_apply(self, show_message=True, mark_setup_done=True):
1793818023
# correct variant on restart.
1793918024
("int8" if self.tgl_quant.isChecked() else "fp32")
1794018025
if hasattr(self, 'tgl_quant') else None),
18026+
force_cpu=(
18027+
self.tgl_force_cpu.isChecked()
18028+
if hasattr(self, 'tgl_force_cpu') else None),
1794118029
mark_setup_done=mark_setup_done)
1794218030

1794318031
# Register the cheatsheet shortcut.
@@ -18052,20 +18140,25 @@ def _on_apply(self, show_message=True, mark_setup_done=True):
1805218140
svc_error = _("Warning: could not enable {svc} at boot.\n{err}").format(
1805318141
svc=active_svc, err=en.stderr.strip())
1805418142

18055-
# Include DICTEE_PARAKEET_QUANT so toggling FP32 ↔ int8 triggers a
18056-
# daemon restart (otherwise the daemon keeps the old model in VRAM/RAM
18057-
# and the env var change has no visible effect).
18143+
# Include DICTEE_PARAKEET_QUANT and DICTEE_FORCE_CPU so toggling either
18144+
# triggers a daemon restart (otherwise the daemon keeps the old model
18145+
# / mode and the env var change has no visible effect).
1805818146
_new_parakeet_quant = (
1805918147
("int8" if self.tgl_quant.isChecked() else "fp32")
1806018148
if hasattr(self, 'tgl_quant') else ""
1806118149
)
18150+
_new_force_cpu = (
18151+
("1" if self.tgl_force_cpu.isChecked() else "0")
18152+
if hasattr(self, 'tgl_force_cpu') else ""
18153+
)
1806218154
_new_asr = {
1806318155
"DICTEE_ASR_BACKEND": asr_backend,
1806418156
"DICTEE_WHISPER_MODEL": whisper_model,
1806518157
"DICTEE_WHISPER_LANG": whisper_lang,
1806618158
"DICTEE_VOSK_MODEL": vosk_model,
1806718159
"DICTEE_AUDIO_SOURCE": str(audio_source),
1806818160
"DICTEE_PARAKEET_QUANT": _new_parakeet_quant,
18161+
"DICTEE_FORCE_CPU": _new_force_cpu,
1806918162
}
1807018163
_old_asr_normalized = {k: _old_asr.get(k, "") for k in _new_asr}
1807118164
_asr_changed = _new_asr != _old_asr_normalized

dictee.conf.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,20 @@ DICTEE_AUDIO_CONTEXT=true
149149
# Max buffer duration in seconds (beyond this, the buffer is cleared)
150150
DICTEE_AUDIO_CONTEXT_TIMEOUT=30
151151

152+
# ============================================================================
153+
# GPU / CPU mode (advanced)
154+
# ============================================================================
155+
156+
# Force CPU mode for all ASR backends, even if a GPU is detected.
157+
# Truthy values: 1, true, yes (case-insensitive).
158+
# Anything else (0, false, no, empty, unset) → GPU used if available.
159+
# Use cases:
160+
# - laptop on battery (CPU mode is more power-efficient)
161+
# - GPU shared with other workloads (Ollama, games)
162+
# - debugging GPU-specific issues
163+
# WARNING: Canary 1B is very slow on CPU (~5 s for 16 s audio, vs 0.65 s GPU).
164+
#DICTEE_FORCE_CPU=0
165+
152166
# ============================================================================
153167
# Parakeet quantization (advanced)
154168
# ============================================================================

0 commit comments

Comments
 (0)