Changing the model download location in Settings had no effect: after the prompted restart, new downloads still went to the old folder and "Effective location" stayed stuck on it. Two stores hold the models dir. The in-app Settings panel writes the new path to the durable per-user env file (`~/.config/omnivoice/env`, OMNIVOICE_CACHE_DIR), but the desktop launcher injects the OLD value from its own Tauri config into the backend's environment before startup — and main.py loaded the per-user file with `override=False`, so the launcher's stale value always won. main.py then maps OMNIVOICE_CACHE_DIR → HF_HOME/HF_HUB_CACHE/TORCH_HOME, pointing downloads at the old dir; `_effective_models_dir()` reads that live env, so the UI faithfully reported the old path as if the change had failed. Fix: load the per-user env file with override so it beats launcher-injected defaults — restoring this file's documented "values written here take effect on the next backend launch" contract. Centralized as `user_env.load_into_environ()` (the file is the in-app Settings source of truth) and called from main.py. Both keys this file can hold (OMNIVOICE_CACHE_DIR, HF_ENDPOINT) are the user's explicit Settings choice and should beat the launcher default, so the override is correct for both (this also fixes the same latent bug for a Settings-set HF mirror). HF_TOKEN isn't launcher-injected, so its behavior is unchanged. Follow-up (separate PR): add a Tauri `set_models_dir` command so the launcher's config.json stays in sync, covering the reset-to-default edge too. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
"""Durable per-user environment file (`~/.config/omnivoice/env`).
|
|
|
|
`main.py` loads this file at startup (via dotenv) before importing torch/HF, so
|
|
values written here take effect on the next backend launch. Used by the
|
|
configurable models directory (#64): the Settings endpoint upserts
|
|
``OMNIVOICE_CACHE_DIR`` here, which main.py then maps to
|
|
``HF_HOME`` / ``HF_HUB_CACHE`` / ``TORCH_HOME``.
|
|
|
|
Format is dotenv-style ``KEY=value`` lines. Upsert preserves other keys (e.g. a
|
|
persisted ``HF_TOKEN``) and writes the file ``0600`` (it can hold secrets).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Optional
|
|
|
|
USER_ENV_PATH = os.path.expanduser("~/.config/omnivoice/env")
|
|
|
|
|
|
def _read_lines(path: str) -> list[str]:
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return f.read().splitlines()
|
|
except FileNotFoundError:
|
|
return []
|
|
# Any *other* OSError (permission, I/O error) propagates: collapsing it to
|
|
# an empty baseline would make a subsequent upsert silently drop existing
|
|
# keys (e.g. a persisted HF_TOKEN) when it rewrites the file.
|
|
|
|
|
|
def _opener_0600(path: str, flags: int) -> int:
|
|
# Create the file with 0600 from the start (no world-readable window before
|
|
# a follow-up chmod) — it can hold secrets like HF_TOKEN. The mode is
|
|
# masked by umask but only ever *more* restrictive; non-POSIX platforms
|
|
# ignore the mode bits.
|
|
return os.open(path, flags, 0o600)
|
|
|
|
|
|
def _write_lines(path: str, lines: list[str]) -> None:
|
|
parent = os.path.dirname(path)
|
|
if parent: # bare filename (e.g. an OMNIVOICE_ENV_FILE override) has no parent
|
|
os.makedirs(parent, exist_ok=True)
|
|
body = "\n".join(lines)
|
|
if body and not body.endswith("\n"):
|
|
body += "\n"
|
|
with open(path, "w", encoding="utf-8", opener=_opener_0600) as f:
|
|
f.write(body)
|
|
try:
|
|
os.chmod(path, 0o600) # tighten an existing file that predates the opener
|
|
except OSError:
|
|
pass # best-effort; some filesystems/Windows don't support chmod
|
|
|
|
|
|
def get_user_env(key: str, path: Optional[str] = None) -> Optional[str]:
|
|
path = path or os.environ.get("OMNIVOICE_ENV_FILE") or USER_ENV_PATH # resolved at call time so tests can monkeypatch
|
|
prefix = f"{key}="
|
|
for line in _read_lines(path):
|
|
if line.startswith(prefix):
|
|
return line[len(prefix):]
|
|
return None
|
|
|
|
|
|
def set_user_env(key: str, value: str, path: Optional[str] = None) -> None:
|
|
"""Upsert ``KEY=value``, preserving all other lines."""
|
|
path = path or os.environ.get("OMNIVOICE_ENV_FILE") or USER_ENV_PATH
|
|
prefix = f"{key}="
|
|
lines = _read_lines(path)
|
|
replaced = False
|
|
for i, line in enumerate(lines):
|
|
if line.startswith(prefix):
|
|
lines[i] = f"{key}={value}"
|
|
replaced = True
|
|
break
|
|
if not replaced:
|
|
lines.append(f"{key}={value}")
|
|
_write_lines(path, lines)
|
|
|
|
|
|
def unset_user_env(key: str, path: Optional[str] = None) -> None:
|
|
"""Remove ``KEY=...`` if present, preserving all other lines."""
|
|
path = path or os.environ.get("OMNIVOICE_ENV_FILE") or USER_ENV_PATH
|
|
prefix = f"{key}="
|
|
lines = [ln for ln in _read_lines(path) if not ln.startswith(prefix)]
|
|
_write_lines(path, lines)
|
|
|
|
|
|
def load_into_environ(path: Optional[str] = None) -> bool:
|
|
"""Load the durable per-user env file into ``os.environ``, **overriding**
|
|
any value a launcher already injected. Returns True if a file was loaded.
|
|
|
|
This file is the in-app Settings source of truth. The desktop launcher
|
|
(Tauri) injects defaults like ``OMNIVOICE_CACHE_DIR`` (and ``HF_ENDPOINT``)
|
|
from its *own* config into the backend's environment *before* startup, so
|
|
loading this file with ``override=False`` meant a models directory the user
|
|
changed in Settings was silently ignored on every launch — the effective
|
|
location stayed on the old one no matter how many restarts (#480). Both keys
|
|
this file can hold are the user's explicit Settings choice and should beat
|
|
the launcher's default, so we override. Restores this file's documented
|
|
"values written here take effect on the next backend launch" contract.
|
|
"""
|
|
path = path or os.environ.get("OMNIVOICE_ENV_FILE") or USER_ENV_PATH
|
|
if not os.path.isfile(path):
|
|
return False
|
|
try:
|
|
import dotenv
|
|
except ImportError:
|
|
return False
|
|
dotenv.load_dotenv(path, override=True)
|
|
return True
|