Show complete engine disk costs before install (#1728)
Closes #1718. Adds structured pre-install and measured post-install disk costs, complete sidecar preflight accounting, strict authorization for recursive scans, and localized catalogue details with confidence and deduplication context.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<EngineHealthRes
|
||||
return apiJson<EngineHealthResponse>(`/engines/${encodeURIComponent(engineId)}/health`);
|
||||
}
|
||||
|
||||
export async function getEngineDiskUsage(engineId: string): Promise<EngineDiskUsage> {
|
||||
return apiJson<EngineDiskUsage>(`/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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? <Check size={11} /> : <Copy size={11} />}
|
||||
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')}
|
||||
</Button>
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronRight
|
||||
size={10}
|
||||
@@ -1085,7 +1139,7 @@ export default function EngineCompatibilityMatrix({
|
||||
expanded && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
{t('engines.whyUnavailable')}
|
||||
{b.available ? t('engines.diskDetails') : t('engines.whyUnavailable')}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
@@ -1258,7 +1312,9 @@ export default function EngineCompatibilityMatrix({
|
||||
loading={installRunning}
|
||||
leading={!installRunning && <Download size={11} />}
|
||||
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 })}
|
||||
</span>
|
||||
)}
|
||||
{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 (
|
||||
<div
|
||||
className="engine-matrix__disk mt-[4px] grid grid-cols-[max-content_1fr] gap-x-[12px] gap-y-[2px]"
|
||||
data-testid={`disk-usage-${b.id}`}
|
||||
>
|
||||
<span>{t('engines.diskModelDownload')}</span>
|
||||
<strong>{value(estimate.model_download_bytes)}</strong>
|
||||
<span>{t('engines.diskPackageDownload')}</span>
|
||||
<strong>{value(estimate.package_download_bytes)}</strong>
|
||||
<span>{t('engines.diskUniqueInstalled')}</span>
|
||||
<strong>{value(estimate.unique_installed_bytes)}</strong>
|
||||
<span>{t('engines.diskPotentiallyShared')}</span>
|
||||
<strong>{value(estimate.potentially_shared_bytes)}</strong>
|
||||
<span>{t('engines.diskTemporary')}</span>
|
||||
<strong>{value(estimate.temporary_free_bytes)}</strong>
|
||||
<span>{t('engines.diskEstimateConfidence')}</span>
|
||||
<strong>
|
||||
{t(`engines.diskConfidence_${estimate.confidence || 'unknown'}`)}
|
||||
</strong>
|
||||
<span>{t('engines.diskDestination')}</span>
|
||||
<strong>
|
||||
{estimate.destination && estimate.destination !== 'unknown'
|
||||
? t(`engines.diskDestination_${estimate.destination}`)
|
||||
: t('common.unknown')}
|
||||
{estimate.destination_volume &&
|
||||
estimate.destination_volume !== 'unknown' && (
|
||||
<code className="ml-[6px]">{estimate.destination_volume}</code>
|
||||
)}
|
||||
</strong>
|
||||
<span>{t('engines.diskActualModel')}</span>
|
||||
<strong>{value(actual.model_bytes)}</strong>
|
||||
<span>{t('engines.diskActualEnvironment')}</span>
|
||||
<strong>{value(actual.environment_bytes)}</strong>
|
||||
<span>{t('engines.diskActualCache')}</span>
|
||||
<strong>{value(actual.cache_bytes)}</strong>
|
||||
<span>{t('engines.diskActualTotal')}</span>
|
||||
<strong>{value(actual.total_owned_bytes)}</strong>
|
||||
<span>{t('engines.diskActualConfidence')}</span>
|
||||
<strong>
|
||||
{t(`engines.diskConfidence_${actual.confidence || 'unknown'}`)}
|
||||
</strong>
|
||||
{estimate.deduplication && (
|
||||
<span className="col-span-2 mt-[2px] text-[color:var(--chrome-fg-muted,#888)]">
|
||||
{t(`engines.diskDedup_${estimate.deduplication}`)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{/* 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 &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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(
|
||||
<EngineCompatibilityMatrix
|
||||
family="tts"
|
||||
apiListEngines={vi.fn().mockResolvedValue(response)}
|
||||
apiGetEngineHealth={vi.fn()}
|
||||
apiGetDiskUsage={apiGetDiskUsage}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<EngineCompatibilityMatrix
|
||||
family="tts"
|
||||
reloadToken={0}
|
||||
apiListEngines={apiListEngines}
|
||||
apiGetEngineHealth={vi.fn()}
|
||||
apiGetDiskUsage={apiGetDiskUsage}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('OmniVoice (test)');
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Disk details' }));
|
||||
await waitFor(() => expect(apiGetDiskUsage).toHaveBeenCalledOnce());
|
||||
view.rerender(
|
||||
<EngineCompatibilityMatrix
|
||||
family="tts"
|
||||
reloadToken={1}
|
||||
apiListEngines={apiListEngines}
|
||||
apiGetEngineHealth={vi.fn()}
|
||||
apiGetDiskUsage={apiGetDiskUsage}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<EngineCompatibilityMatrix
|
||||
family="tts"
|
||||
@@ -598,7 +713,9 @@ describe('EngineCompatibilityMatrix', () => {
|
||||
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',
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user