diff --git a/CHANGELOG.md b/CHANGELOG.md index 739632f3..f395d36e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ the frozen-backend fallback mirror it for their toolchains. **Highlights** +- Show estimated and measured model, dependency, cache, and temporary disk costs in the engine catalogue (#1718) - CosyVoice setup guidance now separates downloaded model files from the runtime that makes the engine available. ### Changed diff --git a/backend/api/routers/engines.py b/backend/api/routers/engines.py index 42e8e966..3ee3c9fb 100644 --- a/backend/api/routers/engines.py +++ b/backend/api/routers/engines.py @@ -75,6 +75,21 @@ def list_tts_backends(): return _family_payload("tts", tts_backend) +@router.get( + "/engines/{engine_id}/disk-usage", + dependencies=[Depends(require_admin_action)], +) +def engine_disk_usage(engine_id: str): + """Measure owned engine bytes only when a catalogue row is opened.""" + try: + tts_backend.get_backend_class(engine_id) + except ValueError: + raise HTTPException(status_code=404, detail="Unknown TTS engine") + from services.engine_disk_usage import disk_usage_for + + return disk_usage_for(engine_id) + + @router.get("/engines/asr") def list_asr_backends(): return _family_payload("asr", asr_backend) diff --git a/backend/services/engine_disk_usage.py b/backend/services/engine_disk_usage.py new file mode 100644 index 00000000..ebbc5e79 --- /dev/null +++ b/backend/services/engine_disk_usage.py @@ -0,0 +1,230 @@ +"""Structured pre-install and measured disk costs for TTS engines.""" +from __future__ import annotations + +import os +import threading +import time +from functools import lru_cache +from pathlib import Path + +_GIB = 1024**3 +_CACHE_TTL_SECONDS = 10.0 +_measurement_cache: dict[str, tuple[float, dict]] = {} +_measurement_lock = threading.Lock() + +# Catalogue/build estimates. ``None`` is deliberate: unknown costs must stay +# visible instead of being silently treated as zero. +_ESTIMATES: dict[str, dict] = { + "omnivoice": { + "package_download_bytes": None, + "unique_installed_bytes": None, + "potentially_shared_bytes": None, + "temporary_free_bytes": None, + "confidence": "estimated", + "destination": "hf_model_cache", + "deduplication": None, + }, + "kittentts": { + "package_download_bytes": None, + "unique_installed_bytes": None, + "potentially_shared_bytes": None, + "temporary_free_bytes": None, + "confidence": "estimated", + "destination": "hf_model_cache", + "deduplication": None, + }, +} +_MODEL_REPOS = { + "omnivoice": "k2-fsa/OmniVoice", + "kittentts": "KittenML/kitten-tts-mini-0.8", +} + + +def _volume_root(path: Path) -> str: + """Mount point/drive containing a possibly not-yet-created destination.""" + try: + current = path.expanduser().resolve() + while not current.exists() and current.parent != current: + current = current.parent + device = current.stat().st_dev + while current.parent != current and current.parent.stat().st_dev == device: + current = current.parent + return str(current) + except OSError: + return "unknown" + + +def _hf_cache_path() -> Path: + configured = ( + os.environ.get("HF_HUB_CACHE") + or os.environ.get("HUGGINGFACE_HUB_CACHE") + or os.environ.get("HF_HOME") + ) + return Path(configured) if configured else Path.home() / ".cache" / "huggingface" + + +@lru_cache(maxsize=None) +def _catalog_model_bytes(engine_id: str) -> int | None: + """Resolve the weight estimate from config/models.yaml, its source of truth.""" + repo_id = _MODEL_REPOS.get(engine_id) + if repo_id is None: + return None + try: + import yaml + + catalog_path = Path(__file__).resolve().parents[1] / "config" / "models.yaml" + entries = yaml.safe_load(catalog_path.read_text(encoding="utf-8"))["models"] + model = next(item for item in entries if item["repo_id"] == repo_id) + return round(float(model["size_gb"]) * _GIB) + except (OSError, KeyError, StopIteration, TypeError, ValueError): + return None + + +def _dir_size(path: Path) -> int: + total = 0 + try: + for root, _dirs, files in os.walk(path): + for filename in files: + try: + total += os.path.getsize(os.path.join(root, filename)) + except OSError: + continue + except OSError: + return 0 + return total + + +def _sidecar_estimate(engine_id: str) -> dict | None: + try: + from services.sidecar_install import get_spec, managed_root + + spec = get_spec(engine_id) + except Exception: + return None + if spec is None: + return None + model_bytes = spec.weights_bytes + dependency_bytes = spec.dependency_bytes + return { + "model_download_bytes": model_bytes, + "package_download_bytes": dependency_bytes, + "unique_installed_bytes": spec.required_bytes, + "potentially_shared_bytes": spec.potentially_shared_bytes, + "temporary_free_bytes": spec.temporary_free_bytes, + "confidence": spec.disk_confidence, + "destination": "engine_data", + "destination_volume": _volume_root(managed_root(spec)), + "deduplication": "uv_same_volume", + } + + +def estimate_for(engine_id: str) -> dict: + estimate = _sidecar_estimate(engine_id) or _ESTIMATES.get(engine_id) + if estimate is not None: + return { + "model_download_bytes": _catalog_model_bytes(engine_id), + "destination_volume": _volume_root(_hf_cache_path()), + **estimate, + } + return { + "model_download_bytes": None, + "package_download_bytes": None, + "unique_installed_bytes": None, + "potentially_shared_bytes": None, + "temporary_free_bytes": None, + "confidence": "unknown", + "destination": "unknown", + "destination_volume": "unknown", + "deduplication": None, + } + + +def _measure_sidecar(engine_id: str) -> dict | None: + try: + from services.sidecar_install import get_spec, managed_checkout, managed_root + + spec = get_spec(engine_id) + except Exception: + return None + if spec is None: + return None + checkout = managed_checkout(spec) + if not checkout.is_dir(): + return None + model = _dir_size(checkout / spec.weights_subdir) + environment = _dir_size(checkout / ".venv") + total = _dir_size(managed_root(spec)) + shared_cache = _dir_size(managed_root(spec).parent / ".uv-cache") + return { + "model_bytes": model, + "environment_bytes": environment, + "cache_bytes": shared_cache, + "total_owned_bytes": total, + "confidence": "measured", + } + + +def _measure_model_cache(engine_id: str) -> dict | None: + repo_id = _MODEL_REPOS.get(engine_id) + if repo_id is None: + return None + try: + from huggingface_hub import scan_cache_dir + + repo = next((item for item in scan_cache_dir().repos if item.repo_id == repo_id), None) + except Exception: + return None + if repo is None or repo.size_on_disk <= 0: + return None + size = int(repo.size_on_disk) + return { + "model_bytes": size, + # The model lives in this cache; cache overhead is not separately + # attributable without double-counting the same hardlinked blobs. + "environment_bytes": None, + "cache_bytes": 0, + "total_owned_bytes": size, + "confidence": "measured", + } + + +def actual_for(engine_id: str) -> dict: + now = time.monotonic() + cached = _measurement_cache.get(engine_id) + if cached and now - cached[0] < _CACHE_TTL_SECONDS: + return dict(cached[1]) + # A cache miss can recursively walk a sidecar and the shared uv cache. + # Coalesce concurrent requests so callers cannot multiply that work. + with _measurement_lock: + now = time.monotonic() + cached = _measurement_cache.get(engine_id) + if cached and now - cached[0] < _CACHE_TTL_SECONDS: + return dict(cached[1]) + actual = _measure_sidecar(engine_id) or _measure_model_cache(engine_id) or { + "model_bytes": None, + "environment_bytes": None, + "cache_bytes": None, + "total_owned_bytes": None, + "confidence": "unknown", + } + _measurement_cache[engine_id] = (now, actual) + return dict(actual) + + +def disk_usage_for(engine_id: str) -> dict: + """Stable API shape consumed by the engine catalogue.""" + return {"estimate": estimate_for(engine_id), "actual": actual_for(engine_id)} + + +def disk_summary_for(engine_id: str) -> dict: + """Cheap list payload; measurement is deferred until the row is opened.""" + return { + "estimate": estimate_for(engine_id), + "actual": { + "model_bytes": None, + "environment_bytes": None, + "cache_bytes": None, + "total_owned_bytes": None, + "confidence": "unknown", + }, + } diff --git a/backend/services/sidecar_install.py b/backend/services/sidecar_install.py index 57adfdd0..e6296890 100644 --- a/backend/services/sidecar_install.py +++ b/backend/services/sidecar_install.py @@ -116,6 +116,11 @@ class SidecarSpec: weights_config_names: tuple[str, ...] = ("config.yaml",) docs_path: str = "docs/engines" # where the manual-install fallback lives required_bytes: int = 12 * _GIB # conservative source+venv+weights estimate for preflight + weights_bytes: Optional[int] = None + dependency_bytes: Optional[int] = None + potentially_shared_bytes: Optional[int] = None + temporary_free_bytes: Optional[int] = None + disk_confidence: str = "unknown" # Called after a successful install/uninstall so the engine's memoised # venv resolution re-probes (import inside the lambda — never at module load). invalidate: Callable[[], None] = field(default=lambda: None) @@ -157,6 +162,11 @@ SPECS: dict[str, SidecarSpec] = { # ~6 GB weights. Deliberately conservative; the preflight subtracts # whatever a partial install already put on disk. required_bytes=12 * _GIB, + weights_bytes=6 * _GIB, + dependency_bytes=6 * _GIB, + potentially_shared_bytes=None, + temporary_free_bytes=12 * _GIB, + disk_confidence="estimated", invalidate=_indextts_invalidate, installed_probe=_indextts_installed, ), @@ -312,6 +322,25 @@ def _dir_size_bytes(path: Path) -> int: return total +def _preserved_install_bytes(spec: SidecarSpec, checkout: Path) -> tuple[int, int]: + """Return bytes preserved for the final install and dependency peak. + + Resumable weights reduce the final download requirement, but they do not + reduce uv's separate environment-build peak. Only source and a usable + existing venv count against that peak. + """ + if not _source_present(spec, checkout): + return 0, 0 + + weights_dir = checkout / spec.weights_subdir + weights = _dir_size_bytes(weights_dir) if spec.weights_repo_id else 0 + venv_dir = checkout / ".venv" + venv = _dir_size_bytes(venv_dir) + source = max(0, _dir_size_bytes(checkout) - weights - venv) + usable_venv = venv if _venv_python(venv_dir).is_file() else 0 + return source + usable_venv + weights, source + usable_venv + + def disk_free_bytes(path: Path) -> int: """Free bytes on the volume backing *path* (nearest existing ancestor). Never raises; 0 when the volume can't be probed.""" @@ -334,8 +363,17 @@ def disk_space_error(spec: SidecarSpec) -> Optional[str]: root = managed_root(spec) # A preserved predecessor is not a partial copy of the new install: the # upgrade needs its full space until the new sidecar is verified. - already = _dir_size_bytes(managed_checkout(spec)) - remaining = max(0, spec.required_bytes - already) + checkout = managed_checkout(spec) + # Credit only bytes the later steps preserve. An invalid layout or revision + # marker makes _step_fetch_source delete the whole checkout. + preserved, dependency_peak_credit = _preserved_install_bytes(spec, checkout) + remaining = max(0, spec.required_bytes - preserved) + if spec.temporary_free_bytes is not None: + # Resumable model weights are unrelated to uv's dependency-build peak. + remaining = max( + remaining, + max(0, spec.temporary_free_bytes - dependency_peak_credit), + ) free = disk_free_bytes(root) if free <= 0: return None # can't probe → never block on missing information diff --git a/backend/services/tts_backend.py b/backend/services/tts_backend.py index 1fb1c8b7..f84bb406 100644 --- a/backend/services/tts_backend.py +++ b/backend/services/tts_backend.py @@ -2399,6 +2399,7 @@ def list_backends() -> list[dict]: # Routing is host-aware but the host caps are constant per process, so probe # ONCE here and resolve each engine's effective device against the same caps. from core.device_caps import detect_host_caps + from services.engine_disk_usage import disk_summary_for from services.engine_evidence import snapshot as execution_snapshot from services.engine_routing import routing_fields caps = detect_host_caps() @@ -2458,6 +2459,7 @@ def list_backends() -> list[dict]: # in-app (Settings renders an Install button instead of leading # with the manual setup snippet). "one_click_install": bid in installable, + "disk_usage": disk_summary_for(bid), "last_error": _LAST_ERRORS.get(bid), "isolation_mode": isolation, "gpu_compat": list(gpu_compat), diff --git a/docs/engines/disk-usage.md b/docs/engines/disk-usage.md index 291cd0db..cb05475e 100644 --- a/docs/engines/disk-usage.md +++ b/docs/engines/disk-usage.md @@ -1,5 +1,14 @@ # Engine venvs & disk usage +The Model Catalogue now exposes a structured disk breakdown before install: +model-weight download, package download, unique installed bytes, potentially +shared bytes, temporary free-space requirement, destination volume, and the +estimate confidence. Missing package or deduplication measurements are shown as +unknown rather than zero. Opening an installed engine's disk details measures +its model, environment, shared cache, and app-owned total separately. These +values come from `config/models.yaml` and the sidecar installer specification; +the UI does not maintain its own size table. + Most engines run in-process in VoiceStudio's main environment. A few (**IndexTTS2**, **MOSS-TTS-v1.5**, **dots.tts**, and any engine whose dependencies conflict with the parent's `torch`/`transformers` pins) run in a diff --git a/frontend/src/api/engines.ts b/frontend/src/api/engines.ts index 466e025d..1a0a92e7 100644 --- a/frontend/src/api/engines.ts +++ b/frontend/src/api/engines.ts @@ -2,6 +2,7 @@ import { apiJson, apiPost } from './client'; import type { AllEnginesResponse, EngineFamily, + EngineDiskUsage, EngineHealthResponse, EngineSelfTestResponse, SelectEngineResponse, @@ -72,6 +73,10 @@ export async function getEngineHealth(engineId: string): Promise(`/engines/${encodeURIComponent(engineId)}/health`); } +export async function getEngineDiskUsage(engineId: string): Promise { + return apiJson(`/engines/${encodeURIComponent(engineId)}/disk-usage`); +} + /** * Run a bounded, real tiny-synthesis on an AVAILABLE, IN-PROCESS TTS engine — * proves the engine actually emits audio (duration + sample-rate + samples), diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 8238e706..3891b370 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -66,6 +66,32 @@ export interface EngineBackend { // Settings can render a model picker. Absent on every other backend. curated_models?: CuratedModel[]; active_model_id?: string; + disk_usage?: EngineDiskUsage; +} + +export interface EngineDiskEstimate { + model_download_bytes: number | null; + package_download_bytes: number | null; + unique_installed_bytes: number | null; + potentially_shared_bytes: number | null; + temporary_free_bytes: number | null; + confidence: 'exact' | 'measured' | 'estimated' | 'unknown'; + destination: string; + destination_volume: string; + deduplication: string | null; +} + +export interface EngineDiskActual { + model_bytes: number | null; + environment_bytes: number | null; + cache_bytes: number | null; + total_owned_bytes: number | null; + confidence: 'measured' | 'unknown'; +} + +export interface EngineDiskUsage { + estimate: EngineDiskEstimate; + actual: EngineDiskActual; } // #981 — one of mlx-audio's curated models (see backend diff --git a/frontend/src/components/EngineCompatibilityMatrix.jsx b/frontend/src/components/EngineCompatibilityMatrix.jsx index f9d1bfe4..7be7e8e7 100644 --- a/frontend/src/components/EngineCompatibilityMatrix.jsx +++ b/frontend/src/components/EngineCompatibilityMatrix.jsx @@ -21,6 +21,7 @@ import { selfTestEngine, installSidecarEngine, getSidecarInstallStatus, + getEngineDiskUsage, } from '../api/engines'; import { listLoadedModels, unloadLoadedModel } from '../api/system'; import { useAppStore } from '../store'; @@ -49,6 +50,23 @@ function reasonMentionsLicense(reason) { return /license not accepted/i.test(reason); } +export function fmtDiskBytes(value, unknownLabel, locale) { + if (value == null) return unknownLabel; + const [divisor, unit, digits] = + value >= 1024 ** 3 + ? [1024 ** 3, 'gigabyte', 2] + : value >= 1024 ** 2 + ? [1024 ** 2, 'megabyte', 1] + : [1024, 'kilobyte', 0]; + return new Intl.NumberFormat(locale, { + style: 'unit', + unit, + unitDisplay: 'short', + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }).format(value / divisor); +} + /** * Engine Compatibility Matrix (Plan 02-04 / ENGINE-06). * @@ -233,6 +251,7 @@ function normalizeEntry(entry) { // null/absent on every other backend, which never renders a picker. curated_models: Array.isArray(entry.curated_models) ? entry.curated_models : null, active_model_id: entry.active_model_id || null, + disk_usage: entry.disk_usage || null, }; } @@ -269,8 +288,9 @@ export default function EngineCompatibilityMatrix({ // One-click sidecar install layer — same injection story as the rest. apiInstallEngine = installSidecarEngine, apiInstallStatus = getSidecarInstallStatus, + apiGetDiskUsage = getEngineDiskUsage, }) { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [localData, setLocalData] = useState(null); const [localLoading, setLocalLoading] = useState(true); const [localError, setLocalError] = useState(null); @@ -298,6 +318,13 @@ export default function EngineCompatibilityMatrix({ // (one at a time). The panel renders BELOW the row as its own block, so // sibling rows keep their fixed two-line height and stay aligned. const [expandedId, setExpandedId] = useState(null); + const [diskByEngine, setDiskByEngine] = useState({}); + const diskGenerationRef = useRef(0); + + const invalidateDiskUsage = useCallback(() => { + diskGenerationRef.current += 1; + setDiskByEngine({}); + }, []); // Memory residency: engine id → its /model/loaded entry (TTS entries and // sidecars carry engine_id). Advisory — load failures leave it empty and // the matrix renders exactly as before (no residency chips). @@ -323,6 +350,7 @@ export default function EngineCompatibilityMatrix({ }, [apiListLoadedModels]); const reload = useCallback(async () => { + invalidateDiskUsage(); if (sharedRefetch) { const result = await sharedRefetch(); if (result.error) { @@ -343,7 +371,7 @@ export default function EngineCompatibilityMatrix({ } } refreshResidency(); - }, [apiListEngines, refreshResidency, sharedRefetch, t]); + }, [apiListEngines, invalidateDiskUsage, refreshResidency, sharedRefetch, t]); useEffect(() => { if (isShared) { @@ -827,15 +855,17 @@ export default function EngineCompatibilityMatrix({ // Unavailable-row detail material for the expansion panel. // One-click-installable rows always have a panel — it hosts the // install progress and the demoted manual-install fallback. + const hasDiskDetails = activeFamily === 'tts' && !!b.disk_usage; const hasDetails = - !b.available && - !!( - b.reason || - b.install_hint || - b.last_error || - b.setup_snippet || - b.one_click_install - ); + hasDiskDetails || + (!b.available && + !!( + b.reason || + b.install_hint || + b.last_error || + b.setup_snippet || + b.one_click_install + )); const install = installByEngine[b.id] || null; const installJob = install?.job || null; const installRunning = installJob?.state === 'running'; @@ -860,7 +890,9 @@ export default function EngineCompatibilityMatrix({ variant="subtle" onClick={() => copySetup(b.id, b.setup_snippet)} leading={copiedId === b.id ? : } - aria-label={t('engines.copySetup', { engine: b.display_name })} + aria-label={t('engines.copySetup', { + engine: b.display_name, + })} > {copiedId === b.id ? t('engines.copied') : t('engines.copy')} @@ -1024,7 +1056,9 @@ export default function EngineCompatibilityMatrix({ value={b.active_model_id || ''} disabled={!onSelect || !b.available} onChange={(e) => changeModel(b.id, e.target.value)} - aria-label={t('engines.curatedModelAria', { engine: b.display_name })} + aria-label={t('engines.curatedModelAria', { + engine: b.display_name, + })} data-testid={`curated-model-select-${b.id}`} > {b.curated_models.map((m) => ( @@ -1076,7 +1110,27 @@ export default function EngineCompatibilityMatrix({ aria-expanded={expanded} aria-controls={panelId} data-testid={`why-toggle-${b.id}`} - onClick={() => setExpandedId(expanded ? null : b.id)} + onClick={async () => { + if (expanded) { + setExpandedId(null); + return; + } + setExpandedId(b.id); + if (hasDiskDetails && !diskByEngine[b.id]) { + const generation = diskGenerationRef.current; + try { + const usage = await apiGetDiskUsage(b.id); + if (diskGenerationRef.current === generation) { + setDiskByEngine((current) => ({ + ...current, + [b.id]: usage, + })); + } + } catch { + // Estimates remain useful when measurement is unavailable. + } + } + }} > - {t('engines.whyUnavailable')} + {b.available ? t('engines.diskDetails') : t('engines.whyUnavailable')} )} @@ -1258,7 +1312,9 @@ export default function EngineCompatibilityMatrix({ loading={installRunning} leading={!installRunning && } data-testid={`install-${b.id}`} - aria-label={t('engines.installAria', { engine: b.display_name })} + aria-label={t('engines.installAria', { + engine: b.display_name, + })} > {installRunning ? t('engines.installing') @@ -1432,6 +1488,62 @@ export default function EngineCompatibilityMatrix({ {t('engines.lastError', { error: b.last_error })} )} + {hasDiskDetails && + (() => { + const usage = diskByEngine[b.id] || b.disk_usage; + const estimate = usage?.estimate || {}; + const actual = usage?.actual || {}; + const value = (bytes) => + fmtDiskBytes(bytes, t('common.unknown'), i18n.resolvedLanguage); + return ( +
+ {t('engines.diskModelDownload')} + {value(estimate.model_download_bytes)} + {t('engines.diskPackageDownload')} + {value(estimate.package_download_bytes)} + {t('engines.diskUniqueInstalled')} + {value(estimate.unique_installed_bytes)} + {t('engines.diskPotentiallyShared')} + {value(estimate.potentially_shared_bytes)} + {t('engines.diskTemporary')} + {value(estimate.temporary_free_bytes)} + {t('engines.diskEstimateConfidence')} + + {t(`engines.diskConfidence_${estimate.confidence || 'unknown'}`)} + + {t('engines.diskDestination')} + + {estimate.destination && estimate.destination !== 'unknown' + ? t(`engines.diskDestination_${estimate.destination}`) + : t('common.unknown')} + {estimate.destination_volume && + estimate.destination_volume !== 'unknown' && ( + {estimate.destination_volume} + )} + + {t('engines.diskActualModel')} + {value(actual.model_bytes)} + {t('engines.diskActualEnvironment')} + {value(actual.environment_bytes)} + {t('engines.diskActualCache')} + {value(actual.cache_bytes)} + {t('engines.diskActualTotal')} + {value(actual.total_owned_bytes)} + {t('engines.diskActualConfidence')} + + {t(`engines.diskConfidence_${actual.confidence || 'unknown'}`)} + + {estimate.deduplication && ( + + {t(`engines.diskDedup_${estimate.deduplication}`)} + + )} +
+ ); + })()} {/* One-click install progress: per-step states + the live log tail while the provisioner job runs, error + remediation on failure. Poll-driven (1.5 s). */} @@ -1461,7 +1573,9 @@ export default function EngineCompatibilityMatrix({ : s.state === 'error' ? '[!]' : '[ ]'}{' '} - {t(`engines.installStep_${s.id}`, { defaultValue: s.id })} + {t(`engines.installStep_${s.id}`, { + defaultValue: s.id, + })} {s.id === 'fetch_weights' && s.state === 'running' && installJob.weights_progress?.pct != null && diff --git a/frontend/src/components/settings/EnginesTab.test.jsx b/frontend/src/components/settings/EnginesTab.test.jsx index 3edcade2..bd4f7313 100644 --- a/frontend/src/components/settings/EnginesTab.test.jsx +++ b/frontend/src/components/settings/EnginesTab.test.jsx @@ -16,6 +16,7 @@ vi.mock('../../api/engines', () => ({ selfTestEngine: vi.fn(), installSidecarEngine: vi.fn(), getSidecarInstallStatus: vi.fn(), + getEngineDiskUsage: vi.fn(), })); // Residency layer (/model/loaded) — mocked so the matrix never hits the diff --git a/frontend/src/i18n/locales/ar.json b/frontend/src/i18n/locales/ar.json index dbe13a5e..a77b3fac 100644 --- a/frontend/src/i18n/locales/ar.json +++ b/frontend/src/i18n/locales/ar.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} توافق المحرك", "active": "نشط", "whyUnavailable": "ما الذي يحتاجه", + "diskDetails": "تفاصيل القرص", + "diskModelDownload": "تنزيل النموذج", + "diskPackageDownload": "تنزيل الحزم", + "diskUniqueInstalled": "المساحة المثبتة الفريدة", + "diskPotentiallyShared": "المساحة المحتمل مشاركتها", + "diskTemporary": "المساحة الحرة المؤقتة", + "diskDestination": "الوجهة", + "diskDestination_hf_model_cache": "وحدة تخزين ذاكرة نماذج Hugging Face المؤقتة", + "diskDestination_engine_data": "وحدة تخزين بيانات محركات VoiceStudio", + "diskActualModel": "حجم النموذج الفعلي", + "diskActualEnvironment": "حجم البيئة الفعلي", + "diskActualCache": "حجم الذاكرة المؤقتة المشتركة الفعلي", + "diskActualTotal": "إجمالي المساحة المملوكة الفعلي", + "diskEstimateConfidence": "موثوقية التقدير", + "diskActualConfidence": "موثوقية القياس", + "diskConfidence_exact": "دقيق", + "diskConfidence_measured": "مقاس", + "diskConfidence_estimated": "تقديري", + "diskConfidence_unknown": "غير معروف", + "diskDedup_uv_same_volume": "يزيل uv تكرار ملفات wheel المتطابقة فقط عندما تكون ذاكرته المؤقتة وبيئة المحرك على وحدة التخزين نفسها.", "sectionReady": "جاهزة للاستخدام", "sectionMore": "أضف المزيد من المحركات", "lastError": "الخطأ الأخير: {{error}}", diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index af4ecec1..1d963f65 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} Motorkompatibilität", "active": "aktiv", "whyUnavailable": "Was benötigt wird", + "diskDetails": "Speicherplatzdetails", + "diskModelDownload": "Modelldownload", + "diskPackageDownload": "Paketdownload", + "diskUniqueInstalled": "Einzigartig installiert", + "diskPotentiallyShared": "Potenziell gemeinsam genutzt", + "diskTemporary": "Temporärer freier Speicher", + "diskDestination": "Ziel", + "diskDestination_hf_model_cache": "Hugging-Face-Modellcache-Laufwerk", + "diskDestination_engine_data": "VoiceStudio-Engine-Datenlaufwerk", + "diskActualModel": "Tatsächliches Modell", + "diskActualEnvironment": "Tatsächliche Umgebung", + "diskActualCache": "Tatsächlicher gemeinsamer Cache", + "diskActualTotal": "Tatsächlich eigener Gesamtbedarf", + "diskEstimateConfidence": "Schätzgenauigkeit", + "diskActualConfidence": "Messgenauigkeit", + "diskConfidence_exact": "Exakt", + "diskConfidence_measured": "Gemessen", + "diskConfidence_estimated": "Geschätzt", + "diskConfidence_unknown": "Unbekannt", + "diskDedup_uv_same_volume": "uv dedupliziert identische Wheels nur, wenn Cache und Engine-Umgebung auf demselben Laufwerk liegen.", "sectionReady": "Einsatzbereit", "sectionMore": "Weitere Motoren hinzufügen", "lastError": "Letzter Fehler: {{error}}", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 8d9c4f14..0e8ad490 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2049,6 +2049,26 @@ "engineCompatLabel": "{{family}} engine compatibility", "active": "active", "whyUnavailable": "What it needs", + "diskDetails": "Disk details", + "diskModelDownload": "Model download", + "diskPackageDownload": "Package download", + "diskUniqueInstalled": "Unique installed", + "diskPotentiallyShared": "Potentially shared", + "diskTemporary": "Temporary free space", + "diskDestination": "Destination", + "diskDestination_hf_model_cache": "Hugging Face model-cache volume", + "diskDestination_engine_data": "VoiceStudio engine-data volume", + "diskActualModel": "Actual model", + "diskActualEnvironment": "Actual environment", + "diskActualCache": "Actual shared cache", + "diskActualTotal": "Actual owned total", + "diskEstimateConfidence": "Estimate confidence", + "diskActualConfidence": "Measurement confidence", + "diskConfidence_exact": "Exact", + "diskConfidence_measured": "Measured", + "diskConfidence_estimated": "Estimated", + "diskConfidence_unknown": "Unknown", + "diskDedup_uv_same_volume": "uv deduplicates identical wheels only when its cache and the engine environment are on the same volume.", "sectionReady": "Ready to use", "sectionMore": "Add more engines", "lastError": "Last error: {{error}}", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index d9a7fd87..fc0923a5 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} compatibilidad del motor", "active": "activo", "whyUnavailable": "Qué necesita", + "diskDetails": "Detalles de disco", + "diskModelDownload": "Descarga del modelo", + "diskPackageDownload": "Descarga de paquetes", + "diskUniqueInstalled": "Instalación exclusiva", + "diskPotentiallyShared": "Posible espacio compartido", + "diskTemporary": "Espacio temporal libre", + "diskDestination": "Destino", + "diskDestination_hf_model_cache": "Volumen de caché de modelos de Hugging Face", + "diskDestination_engine_data": "Volumen de datos de motores de VoiceStudio", + "diskActualModel": "Modelo real", + "diskActualEnvironment": "Entorno real", + "diskActualCache": "Caché compartida real", + "diskActualTotal": "Total propio real", + "diskEstimateConfidence": "Confianza de la estimación", + "diskActualConfidence": "Confianza de la medición", + "diskConfidence_exact": "Exacta", + "diskConfidence_measured": "Medida", + "diskConfidence_estimated": "Estimada", + "diskConfidence_unknown": "Desconocida", + "diskDedup_uv_same_volume": "uv deduplica wheels idénticos solo si su caché y el entorno del motor están en el mismo volumen.", "sectionReady": "Listos para usar", "sectionMore": "Añadir más motores", "lastError": "Último error: {{error}}", diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 08982478..16fb88b5 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -743,6 +743,26 @@ "engineCompatLabel": "Compatibilité moteur {{family}}", "active": "actif", "whyUnavailable": "Ce qu'il lui faut", + "diskDetails": "Détails du disque", + "diskModelDownload": "Téléchargement du modèle", + "diskPackageDownload": "Téléchargement des paquets", + "diskUniqueInstalled": "Installation unique", + "diskPotentiallyShared": "Potentiellement partagé", + "diskTemporary": "Espace libre temporaire", + "diskDestination": "Destination", + "diskDestination_hf_model_cache": "Volume du cache de modèles Hugging Face", + "diskDestination_engine_data": "Volume de données des moteurs VoiceStudio", + "diskActualModel": "Modèle réel", + "diskActualEnvironment": "Environnement réel", + "diskActualCache": "Cache partagé réel", + "diskActualTotal": "Total réellement détenu", + "diskEstimateConfidence": "Fiabilité de l’estimation", + "diskActualConfidence": "Fiabilité de la mesure", + "diskConfidence_exact": "Exacte", + "diskConfidence_measured": "Mesurée", + "diskConfidence_estimated": "Estimée", + "diskConfidence_unknown": "Inconnue", + "diskDedup_uv_same_volume": "uv déduplique les wheels identiques uniquement si son cache et l’environnement du moteur sont sur le même volume.", "sectionReady": "Prêts à l'emploi", "sectionMore": "Ajouter d'autres moteurs", "lastError": "Dernière erreur : {{error}}", diff --git a/frontend/src/i18n/locales/hi.json b/frontend/src/i18n/locales/hi.json index fcf3f2a7..acf5015f 100644 --- a/frontend/src/i18n/locales/hi.json +++ b/frontend/src/i18n/locales/hi.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} इंजन अनुकूलता", "active": "सक्रिय", "whyUnavailable": "इसे क्या चाहिए", + "diskDetails": "डिस्क विवरण", + "diskModelDownload": "मॉडल डाउनलोड", + "diskPackageDownload": "पैकेज डाउनलोड", + "diskUniqueInstalled": "अद्वितीय स्थापित आकार", + "diskPotentiallyShared": "संभावित साझा आकार", + "diskTemporary": "अस्थायी खाली स्थान", + "diskDestination": "गंतव्य", + "diskDestination_hf_model_cache": "Hugging Face मॉडल कैश वॉल्यूम", + "diskDestination_engine_data": "VoiceStudio इंजन डेटा वॉल्यूम", + "diskActualModel": "वास्तविक मॉडल", + "diskActualEnvironment": "वास्तविक परिवेश", + "diskActualCache": "वास्तविक साझा कैश", + "diskActualTotal": "वास्तविक स्वामित्व कुल", + "diskEstimateConfidence": "अनुमान की विश्वसनीयता", + "diskActualConfidence": "माप की विश्वसनीयता", + "diskConfidence_exact": "सटीक", + "diskConfidence_measured": "मापा गया", + "diskConfidence_estimated": "अनुमानित", + "diskConfidence_unknown": "अज्ञात", + "diskDedup_uv_same_volume": "uv समान wheels की प्रतियां तभी हटाता है जब उसका कैश और इंजन परिवेश एक ही वॉल्यूम पर हों।", "sectionReady": "उपयोग के लिए तैयार", "sectionMore": "और इंजन जोड़ें", "lastError": "अंतिम त्रुटि: {{error}}", diff --git a/frontend/src/i18n/locales/id.json b/frontend/src/i18n/locales/id.json index c43aa41b..a20cd9a5 100644 --- a/frontend/src/i18n/locales/id.json +++ b/frontend/src/i18n/locales/id.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} kompatibilitas mesin", "active": "aktif", "whyUnavailable": "Apa yang dibutuhkan", + "diskDetails": "Rincian disk", + "diskModelDownload": "Unduhan model", + "diskPackageDownload": "Unduhan paket", + "diskUniqueInstalled": "Terpasang unik", + "diskPotentiallyShared": "Berpotensi dibagikan", + "diskTemporary": "Ruang kosong sementara", + "diskDestination": "Tujuan", + "diskDestination_hf_model_cache": "Volume cache model Hugging Face", + "diskDestination_engine_data": "Volume data mesin VoiceStudio", + "diskActualModel": "Model aktual", + "diskActualEnvironment": "Lingkungan aktual", + "diskActualCache": "Cache bersama aktual", + "diskActualTotal": "Total milik aktual", + "diskEstimateConfidence": "Keyakinan estimasi", + "diskActualConfidence": "Keyakinan pengukuran", + "diskConfidence_exact": "Tepat", + "diskConfidence_measured": "Terukur", + "diskConfidence_estimated": "Perkiraan", + "diskConfidence_unknown": "Tidak diketahui", + "diskDedup_uv_same_volume": "uv mendeduplikasi wheel identik hanya jika cache dan lingkungan mesin berada pada volume yang sama.", "sectionReady": "Siap digunakan", "sectionMore": "Tambahkan mesin lainnya", "lastError": "Kesalahan terakhir: {{error}}", diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json index 1eb01d40..5a49c7b6 100644 --- a/frontend/src/i18n/locales/it.json +++ b/frontend/src/i18n/locales/it.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} compatibilità motore", "active": "attivo", "whyUnavailable": "Cosa serve", + "diskDetails": "Dettagli disco", + "diskModelDownload": "Download del modello", + "diskPackageDownload": "Download dei pacchetti", + "diskUniqueInstalled": "Installazione univoca", + "diskPotentiallyShared": "Potenzialmente condiviso", + "diskTemporary": "Spazio libero temporaneo", + "diskDestination": "Destinazione", + "diskDestination_hf_model_cache": "Volume della cache modelli Hugging Face", + "diskDestination_engine_data": "Volume dei dati dei motori VoiceStudio", + "diskActualModel": "Modello effettivo", + "diskActualEnvironment": "Ambiente effettivo", + "diskActualCache": "Cache condivisa effettiva", + "diskActualTotal": "Totale effettivamente occupato", + "diskEstimateConfidence": "Affidabilità della stima", + "diskActualConfidence": "Affidabilità della misura", + "diskConfidence_exact": "Esatta", + "diskConfidence_measured": "Misurata", + "diskConfidence_estimated": "Stimata", + "diskConfidence_unknown": "Sconosciuta", + "diskDedup_uv_same_volume": "uv deduplica wheel identiche solo quando la cache e l’ambiente del motore sono sullo stesso volume.", "sectionReady": "Pronti all'uso", "sectionMore": "Aggiungi altri motori", "lastError": "Ultimo errore: {{error}}", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 78a6bc38..2eaf7a7e 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} エンジンの互換性", "active": "アクティブな", "whyUnavailable": "必要なもの", + "diskDetails": "ディスクの詳細", + "diskModelDownload": "モデルのダウンロード", + "diskPackageDownload": "パッケージのダウンロード", + "diskUniqueInstalled": "固有のインストール容量", + "diskPotentiallyShared": "共有可能な容量", + "diskTemporary": "一時的な空き容量", + "diskDestination": "保存先", + "diskDestination_hf_model_cache": "Hugging Face モデルキャッシュのボリューム", + "diskDestination_engine_data": "VoiceStudio エンジンデータのボリューム", + "diskActualModel": "実際のモデル", + "diskActualEnvironment": "実際の環境", + "diskActualCache": "実際の共有キャッシュ", + "diskActualTotal": "実際の専有合計", + "diskEstimateConfidence": "推定の信頼度", + "diskActualConfidence": "測定の信頼度", + "diskConfidence_exact": "正確", + "diskConfidence_measured": "測定済み", + "diskConfidence_estimated": "推定", + "diskConfidence_unknown": "不明", + "diskDedup_uv_same_volume": "uv は、キャッシュとエンジン環境が同じボリュームにある場合のみ、同一の wheel を重複排除します。", "sectionReady": "すぐに使える", "sectionMore": "エンジンを追加", "lastError": "最後のエラー: {{error}}", diff --git a/frontend/src/i18n/locales/ko.json b/frontend/src/i18n/locales/ko.json index 5c819a40..2ae9fa58 100644 --- a/frontend/src/i18n/locales/ko.json +++ b/frontend/src/i18n/locales/ko.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} 엔진 호환성", "active": "활성", "whyUnavailable": "필요한 것", + "diskDetails": "디스크 세부 정보", + "diskModelDownload": "모델 다운로드", + "diskPackageDownload": "패키지 다운로드", + "diskUniqueInstalled": "고유 설치 용량", + "diskPotentiallyShared": "공유 가능 용량", + "diskTemporary": "임시 여유 공간", + "diskDestination": "대상", + "diskDestination_hf_model_cache": "Hugging Face 모델 캐시 볼륨", + "diskDestination_engine_data": "VoiceStudio 엔진 데이터 볼륨", + "diskActualModel": "실제 모델", + "diskActualEnvironment": "실제 환경", + "diskActualCache": "실제 공유 캐시", + "diskActualTotal": "실제 소유 합계", + "diskEstimateConfidence": "예상 신뢰도", + "diskActualConfidence": "측정 신뢰도", + "diskConfidence_exact": "정확함", + "diskConfidence_measured": "측정됨", + "diskConfidence_estimated": "예상됨", + "diskConfidence_unknown": "알 수 없음", + "diskDedup_uv_same_volume": "uv는 캐시와 엔진 환경이 같은 볼륨에 있을 때만 동일한 wheel을 중복 제거합니다.", "sectionReady": "바로 사용 가능", "sectionMore": "엔진 추가", "lastError": "마지막 오류: {{error}}", diff --git a/frontend/src/i18n/locales/nl.json b/frontend/src/i18n/locales/nl.json index 07b09fe2..8056e396 100644 --- a/frontend/src/i18n/locales/nl.json +++ b/frontend/src/i18n/locales/nl.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} motorcompatibiliteit", "active": "actief", "whyUnavailable": "Wat het nodig heeft", + "diskDetails": "Schijfdetails", + "diskModelDownload": "Modeldownload", + "diskPackageDownload": "Pakketdownload", + "diskUniqueInstalled": "Uniek geïnstalleerd", + "diskPotentiallyShared": "Mogelijk gedeeld", + "diskTemporary": "Tijdelijke vrije ruimte", + "diskDestination": "Bestemming", + "diskDestination_hf_model_cache": "Volume voor Hugging Face-modelcache", + "diskDestination_engine_data": "Volume voor VoiceStudio-enginegegevens", + "diskActualModel": "Werkelijk model", + "diskActualEnvironment": "Werkelijke omgeving", + "diskActualCache": "Werkelijke gedeelde cache", + "diskActualTotal": "Werkelijk eigen totaal", + "diskEstimateConfidence": "Betrouwbaarheid schatting", + "diskActualConfidence": "Betrouwbaarheid meting", + "diskConfidence_exact": "Exact", + "diskConfidence_measured": "Gemeten", + "diskConfidence_estimated": "Geschat", + "diskConfidence_unknown": "Onbekend", + "diskDedup_uv_same_volume": "uv dedupliceert identieke wheels alleen als de cache en engineomgeving op hetzelfde volume staan.", "sectionReady": "Klaar voor gebruik", "sectionMore": "Meer motoren toevoegen", "lastError": "Laatste fout: {{error}}", diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json index 54f92339..5d59bf05 100644 --- a/frontend/src/i18n/locales/pl.json +++ b/frontend/src/i18n/locales/pl.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} kompatybilność silnika", "active": "aktywny", "whyUnavailable": "Czego potrzebuje", + "diskDetails": "Szczegóły dysku", + "diskModelDownload": "Pobieranie modelu", + "diskPackageDownload": "Pobieranie pakietów", + "diskUniqueInstalled": "Unikalnie zainstalowane", + "diskPotentiallyShared": "Potencjalnie współdzielone", + "diskTemporary": "Tymczasowe wolne miejsce", + "diskDestination": "Miejsce docelowe", + "diskDestination_hf_model_cache": "Wolumin pamięci modeli Hugging Face", + "diskDestination_engine_data": "Wolumin danych silników VoiceStudio", + "diskActualModel": "Rzeczywisty model", + "diskActualEnvironment": "Rzeczywiste środowisko", + "diskActualCache": "Rzeczywista współdzielona pamięć podręczna", + "diskActualTotal": "Rzeczywista suma własna", + "diskEstimateConfidence": "Wiarygodność szacunku", + "diskActualConfidence": "Wiarygodność pomiaru", + "diskConfidence_exact": "Dokładne", + "diskConfidence_measured": "Zmierzone", + "diskConfidence_estimated": "Szacowane", + "diskConfidence_unknown": "Nieznane", + "diskDedup_uv_same_volume": "uv deduplikuje identyczne wheels tylko wtedy, gdy pamięć podręczna i środowisko silnika są na tym samym woluminie.", "sectionReady": "Gotowe do użycia", "sectionMore": "Dodaj więcej silników", "lastError": "Ostatni błąd: {{error}}", diff --git a/frontend/src/i18n/locales/pt.json b/frontend/src/i18n/locales/pt.json index b38589b1..edc8d29c 100644 --- a/frontend/src/i18n/locales/pt.json +++ b/frontend/src/i18n/locales/pt.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} compatibilidade do motor", "active": "ativo", "whyUnavailable": "O que falta", + "diskDetails": "Detalhes do disco", + "diskModelDownload": "Download do modelo", + "diskPackageDownload": "Download de pacotes", + "diskUniqueInstalled": "Instalação exclusiva", + "diskPotentiallyShared": "Possivelmente compartilhado", + "diskTemporary": "Espaço livre temporário", + "diskDestination": "Destino", + "diskDestination_hf_model_cache": "Volume de cache de modelos do Hugging Face", + "diskDestination_engine_data": "Volume de dados dos mecanismos do VoiceStudio", + "diskActualModel": "Modelo real", + "diskActualEnvironment": "Ambiente real", + "diskActualCache": "Cache compartilhado real", + "diskActualTotal": "Total próprio real", + "diskEstimateConfidence": "Confiança da estimativa", + "diskActualConfidence": "Confiança da medição", + "diskConfidence_exact": "Exata", + "diskConfidence_measured": "Medida", + "diskConfidence_estimated": "Estimada", + "diskConfidence_unknown": "Desconhecida", + "diskDedup_uv_same_volume": "O uv deduplica wheels idênticos apenas quando o cache e o ambiente do mecanismo estão no mesmo volume.", "sectionReady": "Prontos para usar", "sectionMore": "Adicionar mais motores", "lastError": "Último erro: {{error}}", diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index 455f6b00..ac25998d 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -743,6 +743,26 @@ "engineCompatLabel": "Совместимость с двигателем {{family}}", "active": "активный", "whyUnavailable": "Что нужно", + "diskDetails": "Сведения о диске", + "diskModelDownload": "Загрузка модели", + "diskPackageDownload": "Загрузка пакетов", + "diskUniqueInstalled": "Уникально установлено", + "diskPotentiallyShared": "Возможно совместное использование", + "diskTemporary": "Временное свободное место", + "diskDestination": "Назначение", + "diskDestination_hf_model_cache": "Том кэша моделей Hugging Face", + "diskDestination_engine_data": "Том данных движков VoiceStudio", + "diskActualModel": "Фактическая модель", + "diskActualEnvironment": "Фактическое окружение", + "diskActualCache": "Фактический общий кэш", + "diskActualTotal": "Фактический собственный итог", + "diskEstimateConfidence": "Достоверность оценки", + "diskActualConfidence": "Достоверность измерения", + "diskConfidence_exact": "Точно", + "diskConfidence_measured": "Измерено", + "diskConfidence_estimated": "Оценочно", + "diskConfidence_unknown": "Неизвестно", + "diskDedup_uv_same_volume": "uv дедуплицирует одинаковые wheels, только если кэш и окружение движка находятся на одном томе.", "sectionReady": "Готовы к использованию", "sectionMore": "Добавить больше двигателей", "lastError": "Последняя ошибка: {{error}}", diff --git a/frontend/src/i18n/locales/sv.json b/frontend/src/i18n/locales/sv.json index cf24b8d4..b125323d 100644 --- a/frontend/src/i18n/locales/sv.json +++ b/frontend/src/i18n/locales/sv.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} motorkompatibilitet", "active": "aktiv", "whyUnavailable": "Vad som krävs", + "diskDetails": "Diskdetaljer", + "diskModelDownload": "Modellhämtning", + "diskPackageDownload": "Pakethämtning", + "diskUniqueInstalled": "Unikt installerat", + "diskPotentiallyShared": "Möjligen delat", + "diskTemporary": "Tillfälligt ledigt utrymme", + "diskDestination": "Mål", + "diskDestination_hf_model_cache": "Volym för Hugging Face-modellcache", + "diskDestination_engine_data": "Volym för VoiceStudio-motordata", + "diskActualModel": "Faktisk modell", + "diskActualEnvironment": "Faktisk miljö", + "diskActualCache": "Faktisk delad cache", + "diskActualTotal": "Faktisk egen totalsumma", + "diskEstimateConfidence": "Uppskattningens tillförlitlighet", + "diskActualConfidence": "Mätningens tillförlitlighet", + "diskConfidence_exact": "Exakt", + "diskConfidence_measured": "Uppmätt", + "diskConfidence_estimated": "Uppskattad", + "diskConfidence_unknown": "Okänd", + "diskDedup_uv_same_volume": "uv deduplicerar identiska wheels endast när cachen och motormiljön finns på samma volym.", "sectionReady": "Redo att använda", "sectionMore": "Lägg till fler motorer", "lastError": "Senaste fel: {{error}}", diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json index 8021d4fc..5fad6930 100644 --- a/frontend/src/i18n/locales/th.json +++ b/frontend/src/i18n/locales/th.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} ความเข้ากันได้ของเครื่องยนต์", "active": "ใช้งานอยู่", "whyUnavailable": "สิ่งที่ต้องมี", + "diskDetails": "รายละเอียดดิสก์", + "diskModelDownload": "ดาวน์โหลดโมเดล", + "diskPackageDownload": "ดาวน์โหลดแพ็กเกจ", + "diskUniqueInstalled": "พื้นที่ติดตั้งเฉพาะ", + "diskPotentiallyShared": "พื้นที่ที่อาจใช้ร่วมกัน", + "diskTemporary": "พื้นที่ว่างชั่วคราว", + "diskDestination": "ปลายทาง", + "diskDestination_hf_model_cache": "โวลุ่มแคชโมเดล Hugging Face", + "diskDestination_engine_data": "โวลุ่มข้อมูลเอนจิน VoiceStudio", + "diskActualModel": "โมเดลจริง", + "diskActualEnvironment": "สภาพแวดล้อมจริง", + "diskActualCache": "แคชที่ใช้ร่วมกันจริง", + "diskActualTotal": "ยอดรวมที่เป็นเจ้าของจริง", + "diskEstimateConfidence": "ความน่าเชื่อถือของค่าประมาณ", + "diskActualConfidence": "ความน่าเชื่อถือของการวัด", + "diskConfidence_exact": "แม่นยำ", + "diskConfidence_measured": "วัดแล้ว", + "diskConfidence_estimated": "โดยประมาณ", + "diskConfidence_unknown": "ไม่ทราบ", + "diskDedup_uv_same_volume": "uv จะลดข้อมูล wheel ที่ซ้ำกันเฉพาะเมื่อแคชและสภาพแวดล้อมของเอนจินอยู่ในโวลุ่มเดียวกัน", "sectionReady": "พร้อมใช้งาน", "sectionMore": "เพิ่มเครื่องยนต์อื่น ๆ", "lastError": "ข้อผิดพลาดครั้งล่าสุด: {{error}}", diff --git a/frontend/src/i18n/locales/tr.json b/frontend/src/i18n/locales/tr.json index 84cd2a59..46bd6346 100644 --- a/frontend/src/i18n/locales/tr.json +++ b/frontend/src/i18n/locales/tr.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} motor uyumluluğu", "active": "aktif", "whyUnavailable": "Neye ihtiyacı var", + "diskDetails": "Disk ayrıntıları", + "diskModelDownload": "Model indirmesi", + "diskPackageDownload": "Paket indirmesi", + "diskUniqueInstalled": "Benzersiz kurulum", + "diskPotentiallyShared": "Olası paylaşılan alan", + "diskTemporary": "Geçici boş alan", + "diskDestination": "Hedef", + "diskDestination_hf_model_cache": "Hugging Face model önbelleği birimi", + "diskDestination_engine_data": "VoiceStudio motor verileri birimi", + "diskActualModel": "Gerçek model", + "diskActualEnvironment": "Gerçek ortam", + "diskActualCache": "Gerçek paylaşılan önbellek", + "diskActualTotal": "Gerçek sahip olunan toplam", + "diskEstimateConfidence": "Tahmin güvenilirliği", + "diskActualConfidence": "Ölçüm güvenilirliği", + "diskConfidence_exact": "Kesin", + "diskConfidence_measured": "Ölçüldü", + "diskConfidence_estimated": "Tahmini", + "diskConfidence_unknown": "Bilinmiyor", + "diskDedup_uv_same_volume": "uv, aynı wheel dosyalarını yalnızca önbelleği ve motor ortamı aynı birimdeyse tekilleştirir.", "sectionReady": "Kullanıma hazır", "sectionMore": "Daha fazla motor ekle", "lastError": "Son hata: {{error}}", diff --git a/frontend/src/i18n/locales/uk.json b/frontend/src/i18n/locales/uk.json index 1a3644f7..90624251 100644 --- a/frontend/src/i18n/locales/uk.json +++ b/frontend/src/i18n/locales/uk.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} сумісність з двигуном", "active": "активний", "whyUnavailable": "Що потрібно", + "diskDetails": "Відомості про диск", + "diskModelDownload": "Завантаження моделі", + "diskPackageDownload": "Завантаження пакетів", + "diskUniqueInstalled": "Унікально встановлено", + "diskPotentiallyShared": "Можливо спільне", + "diskTemporary": "Тимчасове вільне місце", + "diskDestination": "Призначення", + "diskDestination_hf_model_cache": "Том кешу моделей Hugging Face", + "diskDestination_engine_data": "Том даних рушіїв VoiceStudio", + "diskActualModel": "Фактична модель", + "diskActualEnvironment": "Фактичне середовище", + "diskActualCache": "Фактичний спільний кеш", + "diskActualTotal": "Фактичний власний підсумок", + "diskEstimateConfidence": "Достовірність оцінки", + "diskActualConfidence": "Достовірність вимірювання", + "diskConfidence_exact": "Точно", + "diskConfidence_measured": "Виміряно", + "diskConfidence_estimated": "Оцінено", + "diskConfidence_unknown": "Невідомо", + "diskDedup_uv_same_volume": "uv дедуплікує однакові wheels, лише якщо кеш і середовище рушія розташовані на одному томі.", "sectionReady": "Готові до використання", "sectionMore": "Додати більше двигунів", "lastError": "Остання помилка: {{error}}", diff --git a/frontend/src/i18n/locales/vi.json b/frontend/src/i18n/locales/vi.json index 464b7ffe..5c69f1d3 100644 --- a/frontend/src/i18n/locales/vi.json +++ b/frontend/src/i18n/locales/vi.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} khả năng tương thích động cơ", "active": "hoạt động", "whyUnavailable": "Cần những gì", + "diskDetails": "Chi tiết ổ đĩa", + "diskModelDownload": "Tải mô hình", + "diskPackageDownload": "Tải gói", + "diskUniqueInstalled": "Dung lượng cài đặt riêng", + "diskPotentiallyShared": "Có thể dùng chung", + "diskTemporary": "Dung lượng trống tạm thời", + "diskDestination": "Đích", + "diskDestination_hf_model_cache": "Ổ đĩa bộ nhớ đệm mô hình Hugging Face", + "diskDestination_engine_data": "Ổ đĩa dữ liệu động cơ VoiceStudio", + "diskActualModel": "Mô hình thực tế", + "diskActualEnvironment": "Môi trường thực tế", + "diskActualCache": "Bộ nhớ đệm dùng chung thực tế", + "diskActualTotal": "Tổng dung lượng sở hữu thực tế", + "diskEstimateConfidence": "Độ tin cậy của ước tính", + "diskActualConfidence": "Độ tin cậy của phép đo", + "diskConfidence_exact": "Chính xác", + "diskConfidence_measured": "Đã đo", + "diskConfidence_estimated": "Ước tính", + "diskConfidence_unknown": "Không xác định", + "diskDedup_uv_same_volume": "uv chỉ khử trùng lặp các wheel giống nhau khi bộ nhớ đệm và môi trường động cơ nằm trên cùng một ổ đĩa.", "sectionReady": "Sẵn sàng sử dụng", "sectionMore": "Thêm động cơ khác", "lastError": "Lỗi cuối cùng: {{error}}", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index c8ffdb85..e4afdccd 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -700,6 +700,26 @@ "engineCompatLabel": "{{family}} 引擎兼容性", "active": "当前", "whyUnavailable": "所需条件", + "diskDetails": "磁盘详情", + "diskModelDownload": "模型下载", + "diskPackageDownload": "软件包下载", + "diskUniqueInstalled": "独占安装空间", + "diskPotentiallyShared": "可能共享的空间", + "diskTemporary": "临时可用空间", + "diskDestination": "目标位置", + "diskDestination_hf_model_cache": "Hugging Face 模型缓存卷", + "diskDestination_engine_data": "VoiceStudio 引擎数据卷", + "diskActualModel": "实际模型空间", + "diskActualEnvironment": "实际环境空间", + "diskActualCache": "实际共享缓存", + "diskActualTotal": "实际独占总量", + "diskEstimateConfidence": "估算置信度", + "diskActualConfidence": "测量置信度", + "diskConfidence_exact": "精确", + "diskConfidence_measured": "已测量", + "diskConfidence_estimated": "估算", + "diskConfidence_unknown": "未知", + "diskDedup_uv_same_volume": "仅当 uv 缓存和引擎环境位于同一卷时,uv 才能对相同的 wheel 去重。", "sectionReady": "即可使用", "sectionMore": "添加更多引擎", "lastError": "最后一个错误:{{error}}", diff --git a/frontend/src/i18n/locales/zh-TW.json b/frontend/src/i18n/locales/zh-TW.json index 6ef9791d..8f8b94fa 100644 --- a/frontend/src/i18n/locales/zh-TW.json +++ b/frontend/src/i18n/locales/zh-TW.json @@ -743,6 +743,26 @@ "engineCompatLabel": "{{family}} 引擎相容性", "active": "活躍的", "whyUnavailable": "所需條件", + "diskDetails": "磁碟詳細資料", + "diskModelDownload": "模型下載", + "diskPackageDownload": "套件下載", + "diskUniqueInstalled": "獨占安裝空間", + "diskPotentiallyShared": "可能共用的空間", + "diskTemporary": "暫時可用空間", + "diskDestination": "目的地", + "diskDestination_hf_model_cache": "Hugging Face 模型快取磁碟區", + "diskDestination_engine_data": "VoiceStudio 引擎資料磁碟區", + "diskActualModel": "實際模型空間", + "diskActualEnvironment": "實際環境空間", + "diskActualCache": "實際共用快取", + "diskActualTotal": "實際獨占總量", + "diskEstimateConfidence": "估算可信度", + "diskActualConfidence": "測量可信度", + "diskConfidence_exact": "精確", + "diskConfidence_measured": "已測量", + "diskConfidence_estimated": "估算", + "diskConfidence_unknown": "未知", + "diskDedup_uv_same_volume": "只有在 uv 快取與引擎環境位於同一磁碟區時,uv 才能對相同的 wheel 去除重複。", "sectionReady": "隨時可用", "sectionMore": "新增更多引擎", "lastError": "最後一個錯誤:{{error}}", diff --git a/frontend/src/test/EngineCompatibilityMatrix.test.jsx b/frontend/src/test/EngineCompatibilityMatrix.test.jsx index a668c8f8..6d6ea5a7 100644 --- a/frontend/src/test/EngineCompatibilityMatrix.test.jsx +++ b/frontend/src/test/EngineCompatibilityMatrix.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; // Mock the toast import the component depends on — keeps the test free // of side-effect side-channels (toast() schedules timers we don't want). @@ -19,6 +19,7 @@ vi.mock('../api/system', () => ({ import EngineCompatibilityMatrix, { FORCE_WAIT_TIMEOUT_MS, + fmtDiskBytes, } from '../components/EngineCompatibilityMatrix'; /** Build a minimal AllEnginesResponse with the three rows the plan calls for. */ @@ -69,6 +70,10 @@ describe('EngineCompatibilityMatrix', () => { vi.useRealTimers(); }); + it('formats disk sizes with the active locale', () => { + expect(fmtDiskBytes(1.5 * 1024 ** 3, 'unknown', 'de-DE')).toMatch(/^1,50\sGB$/u); + }); + it('lists available engines first, keeping registration order inside each group', async () => { // The fixture is deliberately interleaved (available, UNavailable, // available). A matrix that renders it in payload order buries a usable @@ -139,6 +144,100 @@ describe('EngineCompatibilityMatrix', () => { ]); }); + it('shows separate estimates and measured disk categories on demand', async () => { + const response = makeEnginesResponse(); + response.tts.backends[0].disk_usage = { + estimate: { + model_download_bytes: 2 * 1024 ** 3, + package_download_bytes: null, + unique_installed_bytes: 3 * 1024 ** 3, + potentially_shared_bytes: null, + temporary_free_bytes: 4 * 1024 ** 3, + destination: 'hf_model_cache', + confidence: 'estimated', + deduplication: null, + }, + actual: {}, + }; + const apiGetDiskUsage = vi.fn().mockResolvedValue({ + estimate: response.tts.backends[0].disk_usage.estimate, + actual: { + model_bytes: 1024 ** 3, + environment_bytes: null, + cache_bytes: 0, + total_owned_bytes: 1024 ** 3, + confidence: 'measured', + }, + }); + render( + , + ); + + await screen.findByText('OmniVoice (test)'); + fireEvent.click(screen.getByRole('button', { name: 'Disk details' })); + await waitFor(() => expect(apiGetDiskUsage).toHaveBeenCalledWith('omnivoice')); + const details = await screen.findByTestId('disk-usage-omnivoice'); + expect(within(details).getByText('Model download')).toBeInTheDocument(); + expect(within(details).getByText('Package download')).toBeInTheDocument(); + expect(within(details).getByText('Actual environment')).toBeInTheDocument(); + expect(within(details).getByText('Estimate confidence')).toBeInTheDocument(); + expect(within(details).getByText('Estimated')).toBeInTheDocument(); + expect(within(details).getByText('Measurement confidence')).toBeInTheDocument(); + expect(within(details).getByText('Measured')).toBeInTheDocument(); + expect(within(details).getAllByText('1.00 GB')).toHaveLength(2); + expect(within(details).getAllByText('unknown').length).toBeGreaterThan(0); + }); + + it('ignores a disk measurement that finishes after engine data reloads', async () => { + const response = makeEnginesResponse(); + response.tts.backends[0].disk_usage = { + estimate: { + model_download_bytes: 2 * 1024 ** 3, + confidence: 'estimated', + }, + actual: {}, + }; + let resolveMeasurement; + const apiGetDiskUsage = vi.fn(() => new Promise((resolve) => (resolveMeasurement = resolve))); + const apiListEngines = vi.fn().mockResolvedValue(response); + const view = render( + , + ); + + await screen.findByText('OmniVoice (test)'); + fireEvent.click(screen.getByRole('button', { name: 'Disk details' })); + await waitFor(() => expect(apiGetDiskUsage).toHaveBeenCalledOnce()); + view.rerender( + , + ); + await waitFor(() => expect(apiListEngines).toHaveBeenCalledTimes(2)); + await act(async () => { + resolveMeasurement({ + estimate: response.tts.backends[0].disk_usage.estimate, + actual: { model_bytes: 9 * 1024 ** 3, confidence: 'measured' }, + }); + }); + + expect(screen.queryByText('9.00 GB')).not.toBeInTheDocument(); + }); + it('shows isolation_mode badge per row (subprocess for IndexTTS, in-process for the others)', async () => { const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); render( @@ -247,7 +346,9 @@ describe('EngineCompatibilityMatrix', () => { await waitFor(() => screen.getByText('IndexTTS2 (test)')); const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]'); - const testBtn = within(indexRow).getByRole('button', { name: /test indextts2/i }); + const testBtn = within(indexRow).getByRole('button', { + name: /test indextts2/i, + }); fireEvent.click(testBtn); await waitFor(() => { @@ -279,7 +380,9 @@ describe('EngineCompatibilityMatrix', () => { await waitFor(() => screen.getByText('IndexTTS2 (test)')); const indexRow = screen.getByText('IndexTTS2 (test)').closest('[role="row"]'); - const testBtn = within(indexRow).getByRole('button', { name: /test indextts2/i }); + const testBtn = within(indexRow).getByRole('button', { + name: /test indextts2/i, + }); fireEvent.click(testBtn); await waitFor(() => { @@ -291,7 +394,12 @@ describe('EngineCompatibilityMatrix', () => { expect(apiGetEngineHealth).toHaveBeenCalledTimes(1); // Release the promise so the test doesn't leak a pending microtask. - resolveHealth({ id: 'indextts2', ok: true, message: 'pong', latency_ms: 50 }); + resolveHealth({ + id: 'indextts2', + ok: true, + message: 'pong', + latency_ms: 50, + }); }); // ── #21 routing display ──────────────────────────────────────────────── @@ -336,7 +444,11 @@ describe('EngineCompatibilityMatrix', () => { routing_reason: 'requires cuda; this host has cpu', }), // Legacy payload: no routing_* keys → render exactly as before. - base({ id: 'legacy', display_name: 'Legacy TTS', gpu_compat: ['cpu'] }), + base({ + id: 'legacy', + display_name: 'Legacy TTS', + gpu_compat: ['cpu'], + }), ], }, asr: { active: '', backends: [] }, @@ -482,9 +594,12 @@ describe('EngineCompatibilityMatrix', () => { // ── P3-B: in-process health check reads as a liveness/deps check ──────── it('labels an in-process health check "deps OK" while subprocess shows real ms', async () => { const apiListEngines = vi.fn().mockResolvedValue(makeEnginesResponse()); - const apiGetEngineHealth = vi - .fn() - .mockResolvedValue({ id: 'omnivoice', ok: true, message: 'import ok', latency_ms: 0 }); + const apiGetEngineHealth = vi.fn().mockResolvedValue({ + id: 'omnivoice', + ok: true, + message: 'import ok', + latency_ms: 0, + }); render( { expect(screen.queryByText('Supertonic-3 — License Acceptance')).not.toBeInTheDocument(); fireEvent.click( - screen.getByRole('button', { name: /review and accept supertonic-3 license/i }), + screen.getByRole('button', { + name: /review and accept supertonic-3 license/i, + }), ); await waitFor(() => { expect(screen.getByText('Supertonic-3 — License Acceptance')).toBeInTheDocument(); @@ -634,7 +751,11 @@ describe('EngineCompatibilityMatrix', () => { ); await waitFor(() => screen.getByText('PocketTTS')); - fireEvent.click(screen.getByRole('button', { name: /review and accept pockettts license/i })); + fireEvent.click( + screen.getByRole('button', { + name: /review and accept pockettts license/i, + }), + ); await waitFor(() => { expect(screen.getByText('PocketTTS License Acceptance')).toBeInTheDocument(); expect(screen.getByText('Review the access conditions')).toBeInTheDocument(); @@ -829,7 +950,11 @@ describe('EngineCompatibilityMatrix', () => { label: 'Kokoro (default, fast)', repo_id: 'mlx-community/Kokoro-82M-bf16', }, - { key: 'csm', label: 'CSM (voice cloning)', repo_id: 'mlx-community/csm-1b-8bit' }, + { + key: 'csm', + label: 'CSM (voice cloning)', + repo_id: 'mlx-community/csm-1b-8bit', + }, { key: 'outetts', label: 'OuteTTS', diff --git a/tests/backend/services/test_tts_backend_registry.py b/tests/backend/services/test_tts_backend_registry.py index 798e35f7..3981d3a9 100644 --- a/tests/backend/services/test_tts_backend_registry.py +++ b/tests/backend/services/test_tts_backend_registry.py @@ -173,6 +173,9 @@ def test_list_backends_shape(registry_sandbox): # below the floor gets a caveat in `routing_reason` BEFORE it spends # the full compute budget finding out its card is too small. "min_vram_gb", + # Structured estimated/measured storage costs for pre-install + # decisions and post-install accounting (#1718). + "disk_usage", # Sanitized actual-vs-declared provider/device evidence (#1717). "execution_evidence", } diff --git a/tests/fixtures/api_routes.txt b/tests/fixtures/api_routes.txt index 7a1dc9d6..a8c1ced9 100644 --- a/tests/fixtures/api_routes.txt +++ b/tests/fixtures/api_routes.txt @@ -89,6 +89,7 @@ GET /engines/sidecar/{engine_id}/install/status GET /engines/sonitranslate/status GET /engines/translation GET /engines/tts +GET /engines/{engine_id}/disk-usage GET /engines/{engine_id}/health GET /export/history GET /gallery/categories diff --git a/tests/test_admin_route_policy.py b/tests/test_admin_route_policy.py index b41b98ed..22670755 100644 --- a/tests/test_admin_route_policy.py +++ b/tests/test_admin_route_policy.py @@ -125,6 +125,7 @@ def test_managed_sidecar_install_stays_desktop_only(): @pytest.mark.parametrize( ("filename", "function_name"), [ + ("engines.py", "engine_disk_usage"), ("engines.py", "engine_health"), ("settings.py", "list_llm_provider_models"), ("system.py", "system_diagnose"), diff --git a/tests/test_engine_disk_usage.py b/tests/test_engine_disk_usage.py new file mode 100644 index 00000000..885318f9 --- /dev/null +++ b/tests/test_engine_disk_usage.py @@ -0,0 +1,113 @@ +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor +from threading import Event + +import pytest + + +@pytest.fixture +def disk_modules(): + from services import engine_disk_usage + from services.sidecar_install import SPECS + + return engine_disk_usage, SPECS + + +def test_in_process_model_exposes_unknown_dependency_cost_explicitly(monkeypatch, disk_modules): + engine_disk_usage, _ = disk_modules + monkeypatch.setattr(engine_disk_usage, "_measure_model_cache", lambda _engine_id: None) + usage = engine_disk_usage.disk_usage_for("omnivoice") + assert usage["estimate"]["model_download_bytes"] > 0 + assert usage["estimate"]["package_download_bytes"] is None + assert usage["estimate"]["confidence"] == "estimated" + assert usage["estimate"]["destination_volume"] + + +def test_lightweight_optional_engine_has_weight_estimate_not_fake_package_zero(monkeypatch, disk_modules): + engine_disk_usage, _ = disk_modules + monkeypatch.setattr(engine_disk_usage, "_measure_model_cache", lambda _engine_id: None) + usage = engine_disk_usage.disk_usage_for("kittentts") + assert usage["estimate"]["model_download_bytes"] == round(0.08 * 1024**3) + assert usage["estimate"]["package_download_bytes"] is None + + +def test_separate_torch_sidecar_uses_installer_build_metadata(monkeypatch, tmp_path, disk_modules): + engine_disk_usage, SPECS = disk_modules + spec = SPECS["indextts2"] + monkeypatch.setattr("services.sidecar_install.DATA_DIR", tmp_path) + usage = engine_disk_usage.disk_usage_for(spec.engine_id) + assert usage["estimate"]["model_download_bytes"] == spec.weights_bytes + assert usage["estimate"]["package_download_bytes"] == spec.dependency_bytes + assert usage["estimate"]["unique_installed_bytes"] == spec.required_bytes + assert usage["estimate"]["temporary_free_bytes"] == spec.temporary_free_bytes + assert usage["estimate"]["deduplication"] == "uv_same_volume" + + +def test_installed_sidecar_reports_separate_measured_categories(monkeypatch, tmp_path, disk_modules): + engine_disk_usage, SPECS = disk_modules + spec = SPECS["indextts2"] + monkeypatch.setattr("services.sidecar_install.DATA_DIR", tmp_path) + checkout = tmp_path / "engines" / spec.engine_id / spec.checkout_dirname + for relative, payload in ( + (Path(spec.weights_subdir) / "model.bin", b"weights"), + (Path(".venv") / "package.py", b"environment"), + (Path("source.py"), b"source"), + ): + path = checkout / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(payload) + cache = tmp_path / "engines" / ".uv-cache" / "wheel" + cache.parent.mkdir(parents=True) + cache.write_bytes(b"shared") + + engine_disk_usage._measurement_cache.clear() + actual = engine_disk_usage.actual_for(spec.engine_id) + assert actual["model_bytes"] == len(b"weights") + assert actual["environment_bytes"] == len(b"environment") + assert actual["cache_bytes"] == len(b"shared") + assert actual["total_owned_bytes"] == len(b"weights") + len(b"environment") + len(b"source") + assert actual["confidence"] == "measured" + + +def test_disk_measurement_route_rejects_unknown_engine(monkeypatch): + from api.routers import engines + from fastapi import HTTPException + + def unknown_backend(_engine_id): + raise ValueError("unknown") + + monkeypatch.setattr( + engines.tts_backend, + "get_backend_class", + unknown_backend, + ) + with pytest.raises(HTTPException) as caught: + engines.engine_disk_usage("unknown") + assert caught.value.status_code == 404 + + +def test_concurrent_disk_measurements_are_coalesced(monkeypatch, disk_modules): + engine_disk_usage, _ = disk_modules + calls = 0 + entered = Event() + release = Event() + + def measure(_engine_id): + nonlocal calls + calls += 1 + entered.set() + assert release.wait(timeout=1) + return {"total_owned_bytes": 7, "confidence": "measured"} + + engine_disk_usage._measurement_cache.clear() + monkeypatch.setattr(engine_disk_usage, "_measure_sidecar", measure) + with ThreadPoolExecutor(max_workers=2) as pool: + first = pool.submit(engine_disk_usage.actual_for, "coalesce") + assert entered.wait(timeout=1) + second = pool.submit(engine_disk_usage.actual_for, "coalesce") + assert not second.done() + release.set() + results = [first.result(), second.result()] + + assert calls == 1 + assert [result["total_owned_bytes"] for result in results] == [7, 7] diff --git a/tests/test_sidecar_install.py b/tests/test_sidecar_install.py index c438aa70..7bfa4653 100644 --- a/tests/test_sidecar_install.py +++ b/tests/test_sidecar_install.py @@ -195,12 +195,62 @@ def test_disk_preflight_subtracts_partial_install(monkeypatch, tmp_path): # only the remainder (+headroom) must fit, so a resume isn't blocked. spec = _mk_spec(required_bytes=1 * _GIB) root = si.managed_root(spec) + checkout = si.managed_checkout(spec) root.mkdir(parents=True) - monkeypatch.setattr(si, "_dir_size_bytes", lambda p: int(0.9 * _GIB)) + monkeypatch.setattr(si, "_source_present", lambda _spec, _checkout: True) + monkeypatch.setattr( + si, + "_dir_size_bytes", + lambda path: int(0.9 * _GIB) if path == checkout else 0, + ) monkeypatch.setattr(si, "disk_free_bytes", lambda p: (si.MIN_FREE_GB + 1) * _GIB) assert si.disk_space_error(spec) is None +def test_disk_preflight_subtracts_partial_install_from_peak_requirement(monkeypatch): + spec = _mk_spec(required_bytes=1 * _GIB, temporary_free_bytes=4 * _GIB) + checkout = si.managed_checkout(spec) + monkeypatch.setattr(si, "_source_present", lambda _spec, _checkout: True) + monkeypatch.setattr( + si, + "_dir_size_bytes", + lambda path: int(0.9 * _GIB) if path == checkout else 0, + ) + monkeypatch.setattr(si, "disk_free_bytes", lambda _path: (si.MIN_FREE_GB + 2) * _GIB) + assert "3.1 GB" in si.disk_space_error(spec) + + +def test_disk_preflight_does_not_credit_weights_against_dependency_peak(monkeypatch): + spec = _mk_spec( + required_bytes=12 * _GIB, + temporary_free_bytes=12 * _GIB, + weights_repo_id="Example/Weights", + ) + checkout = si.managed_checkout(spec) + weights = checkout / spec.weights_subdir + monkeypatch.setattr(si, "_source_present", lambda _spec, _checkout: True) + monkeypatch.setattr( + si, + "_dir_size_bytes", + lambda path: 7 * _GIB if path == checkout else 6 * _GIB if path == weights else 0, + ) + monkeypatch.setattr(si, "disk_free_bytes", lambda _path: (si.MIN_FREE_GB + 10) * _GIB) + + assert "11.0 GB" in si.disk_space_error(spec) + + +def test_disk_preflight_does_not_credit_checkout_that_fetch_will_delete(monkeypatch): + spec = _mk_spec(required_bytes=4 * _GIB, source_revision="new-revision") + checkout = si.managed_checkout(spec) + checkout.mkdir(parents=True) + (checkout / "pyproject.toml").write_text("[project]\nname='fake'\n") + (checkout / si._SOURCE_REVISION_MARKER).write_text("stale-revision\n") + monkeypatch.setattr(si, "_dir_size_bytes", lambda _path: 3 * _GIB) + monkeypatch.setattr(si, "disk_free_bytes", lambda _path: (si.MIN_FREE_GB + 2) * _GIB) + + assert "4.0 GB" in si.disk_space_error(spec) + + def test_missing_uv_is_actionable(monkeypatch): spec = _mk_spec() monkeypatch.setattr(si, "_locate_uv", lambda: None)