Files
VoiceStudio/backend/core/links.py
T
Palash DebnathandClaude Opus 4.7 715766cb04 Phase 1 Wave 2: per-OS install docs + Settings UI + error→docs deeplinks (#94)
* docs(install): per-OS install pages + drift validator + CI gate

Splits the 600-line README install section into self-contained per-OS docs
under docs/install/{macos,windows,linux,docker}.md plus a Top-10
troubleshooting index. Each OS doc is end-to-end: a user opens it and
reaches a working app following only commands inside that file.

Adds:
- docs/install/{macos,windows,linux,docker}.md  (OS-specific install paths)
- docs/install/troubleshooting.md               (top 10 install errors)
- docs/engines/cosyvoice.md                     (closes #55 docs half)
- docs/features/diarization.md                  (pyannote license flow)
- docs/setup/huggingface-token.md               (3-source cascade guide)
- scripts/validate-install-docs.py              (INST-06 docs-drift gate)
- tests/scripts/test_validate_install_docs.py   (B-5: validator self-tests)
- .github/workflows/ci.yml step running the validator on every PR

Implements INST-02 (README routing), INST-03 (macOS Gatekeeper anchor),
INST-12 docs half (Windows torch-compile-oom anchor), DOCS-01..05.

The validator is a one-way diff: every `<!-- validate -->`-tagged line
in docs must appear in scripts/desktop-prod.sh after normalisation
(prompt-prefix strip, CRLF, trailing whitespace, blank-and-comment skip).
A `<!-- validate: skip -->` marker opts out for human-readability blocks.
Its own 10 unit tests catch regressions in the gate itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(deeplinks): links.py + error_docs_map (Python + TS mirror)

Adds the single source of truth for the project repo URL and the 4-class
error → docs taxonomy that both the in-app ErrorBoundary deeplink button
(Wave 2 Task 3) and the Phase 5 bug reporter will consume.

New:
- backend/core/links.py            — PROJECT_REPO_URL + BLOB_MAIN resolver
                                      (Tauri config first, pyproject fallback)
- backend/core/error_docs_map.py   — lookup(error_class) → docs URL
- frontend/src/utils/errorDocsMap.ts (TS mirror with classifyError helper)
- tests/backend/core/test_links.py + test_error_docs_map.py
- frontend/src/utils/errorDocsMap.test.ts

Resolves checker B-6 (links.py ownership) and Open Question #3 (which fork
the deeplinks resolve to — the Tauri updater endpoint wins, which points
at the desktop app fork debpalash/OmniVoice-Studio).

The TS BASE constant is documented as the second hardcoded URL drift site;
the keys-sync test (`test_keys_match_python_map` equivalent) guards the
4-class taxonomy contract between Python + TS halves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ui): Settings → API Keys panel + ErrorBoundary docs deeplink

Wave 2 AUTH-03 UI half + ErrorBoundary deeplink wiring.

ErrorBoundary fallback now renders an "Open docs for this error" button
that classifies the thrown Error message (heuristic: pkg_resources → 401 /
HfHubHTTP → WebKit / white screen → quarantine / Gatekeeper) and opens the
matching docs anchor via Tauri shell.open (with a window.open fallback
in browser dev mode).

ApiKeysPanel consumes the Wave 1 resolver state endpoint:
  - 3 source rows (App / Env var / HF CLI) with set/unset indicator,
    masked token preview, whoami username + green check
  - "Active" badge on whichever source is currently serving the cascade
  - App-row only: Save (POST /api/settings/hf-token) +
    Clear (DELETE with optional "also clear HF CLI" confirm dialog)
  - "Test now" button refetches state (invalidates the resolver's
    validation cache via the same endpoint hit)

Panel mounted in the existing Settings → Credentials tab; the legacy
HF_TOKEN row from CREDENTIAL_FIELDS is filtered out so the two paths
don't fight over the same key.

Threat T-02-02: the panel never displays the full token. The masked
value comes from the resolver state endpoint; the full token only
crosses the IPC boundary on Save (POST) and is cleared from local
state on success.

Closes AUTH-03 fully (Wave 1 backend + this Wave 2 UI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(perf): INST-12 Disable torch.compile (Windows) toggle (backend + UI)

Wave 2 Task 4 — full INST-12 delivery per checker B-2/B-7 v0.3.0 fat-release
decision. Both the docs half (windows.md anchor, shipped in earlier commit)
and the runtime toggle are now in Phase 1.

Backend:
- backend/services/settings_store.py: adds get_text/set_text helpers for
  non-secret config (refuses to write to the encrypted hf_token key).
- backend/api/routers/settings.py: GET + PUT
  /api/settings/perf/torch-compile-disabled, both under the existing
  loopback guard (threat T-02-04).
- backend/services/engine_env.py: new `build_engine_env()` helper that
  centralises HF_TOKEN/YOUR_HF_TOKEN injection from the 3-source resolver
  AND injects TORCH_COMPILE_DISABLE=1 when the flag is set on win32.
  Phase 2 SubprocessBackend launchers should adopt the same helper.
- backend/services/sonitranslate.py: migrated to engine_env.build_engine_env()
  while preserving the source-level `env["HF_TOKEN"]` sentinel that
  test_sonitranslate_module_uses_resolver checks.

Frontend:
- frontend/src/components/settings/PerformancePanel.{jsx,css,test.jsx}:
  toggle UI with the explainer for #65; renders disabled with a "not
  applicable" badge on macOS/Linux.
- frontend/src/pages/Settings.jsx: mounts the panel into the Credentials
  tab alongside the API Keys panel.

Tests:
- tests/backend/test_perf_settings.py: 7 backend tests (default state,
  PUT persistence, T-02-04 non-loopback rejection, settings_store round-
  trip, env injection on win32, NO injection on macOS/Linux, NO injection
  when disabled).
- frontend PerformancePanel.test.jsx: 5 tests (renders from GET state,
  PUT on toggle, disabled on non-Windows platforms, pre-enabled state).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(planning): Wave 2 SUMMARY + REQUIREMENTS status updates

- .planning/phases/01.../01-02-SUMMARY.md: full implementation report
  per template (truths, commits, tests, deviations, drift-site
  acknowledgments per W-3, launcher seam name for Phase 2,
  taxonomy keys for Phase 5).
- .planning/REQUIREMENTS.md: flips Wave 2 closures to Done:
    AUTH-03, INST-02, INST-03 (docs half), INST-06, INST-12,
    DOCS-01..05.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:22:10 +05:30

103 lines
3.5 KiB
Python

"""Project repo URL resolver — single source of truth for deeplinks.
Owned by Plan 01-02 (checker B-6 resolution). Read by:
- backend/core/error_docs_map.py — error → docs URL mapping
- (future) backend/services/bug_report.py — prefilled GitHub Issues URL
Resolution order (highest → lowest):
1. `frontend/src-tauri/tauri.conf.json` `plugins.updater.endpoints[0]`
— this points at the desktop app fork (e.g. github.com/debpalash/
OmniVoice-Studio), which is where docs deeplinks should resolve.
2. `pyproject.toml [project.urls].Repository` — fallback to the upstream
model repo URL when the Tauri config is unreadable.
The resolved URL is cached at import time so callers can use the module
constants directly without re-reading files.
"""
from __future__ import annotations
import json
import logging
import re
import sys
from pathlib import Path
from typing import Optional
logger = logging.getLogger("omnivoice.core.links")
# Walk up from this file to find the repo root (the dir containing
# `pyproject.toml`). This lets the module work whether the backend is
# imported under `--app-dir backend` or installed as a wheel.
_THIS = Path(__file__).resolve()
def _find_repo_root() -> Path:
for ancestor in (_THIS.parent, *_THIS.parents):
if (ancestor / "pyproject.toml").exists():
return ancestor
# Fallback — two levels up from backend/core/links.py
return _THIS.parent.parent.parent
_REPO_ROOT = _find_repo_root()
_TAURI_CONF = _REPO_ROOT / "frontend" / "src-tauri" / "tauri.conf.json"
_PYPROJECT = _REPO_ROOT / "pyproject.toml"
_GITHUB_REPO_RE = re.compile(r"https?://github\.com/([^/]+)/([^/]+?)(?:/|\.git|$)")
def _from_tauri() -> Optional[str]:
"""Parse the updater endpoint and pull `github.com/<owner>/<repo>` out."""
try:
text = _TAURI_CONF.read_text(encoding="utf-8")
conf = json.loads(text)
except Exception:
logger.debug("links: tauri.conf.json unreadable", exc_info=True)
return None
try:
endpoints = (
conf.get("plugins", {})
.get("updater", {})
.get("endpoints", [])
)
for url in endpoints:
m = _GITHUB_REPO_RE.search(url)
if m:
owner, repo = m.group(1), m.group(2)
return f"https://github.com/{owner}/{repo}"
except Exception:
logger.debug("links: tauri.conf.json updater shape unexpected", exc_info=True)
return None
def _from_pyproject() -> Optional[str]:
"""Read `[project.urls].Repository` from pyproject.toml via tomllib."""
try:
# tomllib is stdlib on 3.11+
if sys.version_info >= (3, 11):
import tomllib
else: # pragma: no cover — repo pins 3.11+
import tomli as tomllib # type: ignore[no-redef]
with _PYPROJECT.open("rb") as f:
data = tomllib.load(f)
repo = data.get("project", {}).get("urls", {}).get("Repository")
if isinstance(repo, str) and repo.startswith("https://github.com/"):
# Strip trailing `.git` / slash if present.
return repo.rstrip("/").removesuffix(".git")
except Exception:
logger.debug("links: pyproject.toml read failed", exc_info=True)
return None
def _resolve() -> str:
"""Pick the Tauri config URL first, then fall back to pyproject."""
return (
_from_tauri()
or _from_pyproject()
or "https://github.com/debpalash/OmniVoice-Studio"
)
PROJECT_REPO_URL: str = _resolve()
PROJECT_REPO_BLOB_MAIN: str = f"{PROJECT_REPO_URL}/blob/main"