diff --git a/CHANGELOG.md b/CHANGELOG.md index ed94ec2e..8d4f153f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ the frozen-backend fallback mirror it for their toolchains. - The OmniVoice guide now covers combining style attributes with a reference clip (consistent instruct stabilizes cloning; the reference wins conflicts), inline pronunciation control (pinyin / CMU phonemes), and corrects the claim that the default engine can't do voice design — it can, from attributes (#1565) ### Fixed +- The setup wizard's RAM check no longer blocks 8 GB machines whose OS reports ~7.8 GB usable — the thresholds now tolerate reserved memory, and `OMNIVOICE_RAM_PREFLIGHT=0` turns a genuine block into a warning for those who accept the OOM risk (#1618) - Invisible watermarking now runs eagerly instead of through `torch.compile` — AudioSeal's lazy compile sent the first embed of every session into Inductor's C++ codegen, which failed outright on macOS hosts whose toolchain couldn't serve it and shipped the audio unmarked after a 30-40s wait; first embed drops from 9.70s to 0.26s (#1615) — thanks @paoloantinori! - The macOS Accessibility blocker now rechecks while visible and closes as soon as the grant is enabled instead of keeping a stale permission prompt on screen (#1609) - The dubbing editor's video and transcript columns can now be resized by pointer or keyboard, and the chosen split persists across launches (#1571) — thanks @invio-a11y! diff --git a/backend/api/routers/setup/wizard.py b/backend/api/routers/setup/wizard.py index 82ac722f..b80dd2ed 100644 --- a/backend/api/routers/setup/wizard.py +++ b/backend/api/routers/setup/wizard.py @@ -62,6 +62,11 @@ def setup_status(): _MIN_NVIDIA_DRIVER = 555 _RAM_FAIL_GB = 8 _RAM_WARN_GB = 12 +# Installed DIMMs never fully reach the OS: firmware, integrated graphics and +# kernel reservations shave off up to ~7% (an "8 GB" Windows laptop reports +# ~7.8 GB usable). Thresholds are compared with this allowance applied so the +# machines a threshold is meant to admit aren't blocked by that gap (#1618). +_RAM_RESERVED_ALLOWANCE = 0.93 def _run_cmd(args: list[str], timeout: float = 2.0) -> tuple[int, str]: @@ -352,17 +357,28 @@ def preflight(): # ── RAM ram = _ram_gb() + # Escape hatch (#1618): a preflight should inform, not brick setup — + # OMNIVOICE_RAM_PREFLIGHT=0 downgrades the hard block to a warning for + # users who accept the OOM risk. Same opt-out shape as + # OMNIVOICE_ASR_VRAM_PREFLIGHT. + ram_gate = os.environ.get( + "OMNIVOICE_RAM_PREFLIGHT", "1" + ).strip().lower() not in ("0", "false", "no") if ram == 0: ram_status, ram_detail, ram_fix = ( "warn", "Could not detect system RAM.", "Install psutil in the backend environment or ignore this warning.", ) - elif ram < _RAM_FAIL_GB: + elif ram < _RAM_FAIL_GB * _RAM_RESERVED_ALLOWANCE: ram_status, ram_detail, ram_fix = ( - "fail", f"{ram:.1f} GB total (need ≥ {_RAM_FAIL_GB} GB)", - "The app will OOM on first dub. Close other apps or upgrade RAM.", + "fail" if ram_gate else "warn", + f"{ram:.1f} GB total (need ≥ {_RAM_FAIL_GB} GB)", + "The app will OOM on first dub. Close other apps or upgrade RAM." + if ram_gate else + "RAM check disabled via OMNIVOICE_RAM_PREFLIGHT=0 — dubbing may " + "OOM on this machine.", ) - elif ram < _RAM_WARN_GB: + elif ram < _RAM_WARN_GB * _RAM_RESERVED_ALLOWANCE: ram_status, ram_detail, ram_fix = ( "warn", f"{ram:.1f} GB total ({_RAM_WARN_GB}+ GB recommended)", "Long videos may hit swap. Keep other apps closed during dubbing.", diff --git a/backend/tests/test_ram_preflight_1618.py b/backend/tests/test_ram_preflight_1618.py new file mode 100644 index 00000000..fe8951e0 --- /dev/null +++ b/backend/tests/test_ram_preflight_1618.py @@ -0,0 +1,76 @@ +"""#1618 — RAM preflight must not hard-block the machines it means to admit. + +An "8 GB" machine reports ~7.8 GB usable (firmware/iGPU/kernel reservations), +so comparing reported RAM against the marketing-size threshold blocked exactly +the boundary hardware the ≥8 GB rule intends to allow. The check now applies +``_RAM_RESERVED_ALLOWANCE`` to both thresholds, and +``OMNIVOICE_RAM_PREFLIGHT=0`` downgrades a genuine fail to a warning. +""" + +import pytest + +from api.routers.setup import wizard + + +def _ram_check(monkeypatch, ram_gb: float, env: str | None = None) -> dict: + # Keep the preflight hermetic: stub the probes that hit the network or + # auto-acquire media tools, so each RAM assertion stays fast and offline. + monkeypatch.setattr(wizard, "_network_check", lambda: { + "id": "network", "label": "Network", "status": "pass", + "detail": "stubbed", "fix": None, "mirror_reachable": True, + }) + import services.media_tools as media_tools + monkeypatch.setattr(media_tools, "summary", lambda auto_acquire=True: None) + monkeypatch.setattr(wizard, "_ram_gb", lambda: ram_gb) + if env is None: + monkeypatch.delenv("OMNIVOICE_RAM_PREFLIGHT", raising=False) + else: + monkeypatch.setenv("OMNIVOICE_RAM_PREFLIGHT", env) + resp = wizard.preflight() + checks = resp["checks"] if isinstance(resp, dict) else resp.checks + for c in checks: + c = c if isinstance(c, dict) else c.model_dump() + if c["id"] == "ram": + return c + raise AssertionError("no ram check in preflight response") + + +def test_8gb_installed_reporting_7_84_usable_is_not_blocked(monkeypatch): + """The #1618 report: 7.84 GB usable on an 8 GB laptop was a hard fail.""" + check = _ram_check(monkeypatch, 7.84) + assert check["status"] != "fail" + + +def test_boundary_at_allowance_passes_the_fail_gate(monkeypatch): + check = _ram_check( + monkeypatch, wizard._RAM_FAIL_GB * wizard._RAM_RESERVED_ALLOWANCE + ) + assert check["status"] != "fail" + + +def test_genuinely_low_ram_still_fails(monkeypatch): + check = _ram_check(monkeypatch, 6.0) + assert check["status"] == "fail" + + +@pytest.mark.parametrize("env", ["0", "false", "no"]) +def test_escape_hatch_downgrades_fail_to_warn(monkeypatch, env): + check = _ram_check(monkeypatch, 6.0, env=env) + assert check["status"] == "warn" + assert "OMNIVOICE_RAM_PREFLIGHT" in (check["fix"] or "") + + +def test_escape_hatch_not_triggered_by_other_values(monkeypatch): + check = _ram_check(monkeypatch, 6.0, env="1") + assert check["status"] == "fail" + + +def test_12gb_installed_reporting_11_8_usable_passes_clean(monkeypatch): + """Same reservation gap at the warn threshold: 12 GB installed ≈ 11.8.""" + check = _ram_check(monkeypatch, 11.8) + assert check["status"] == "pass" + + +def test_warn_band_between_thresholds(monkeypatch): + check = _ram_check(monkeypatch, 9.0) + assert check["status"] == "warn" diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md index 328855af..d5a80154 100644 --- a/docs/install/troubleshooting.md +++ b/docs/install/troubleshooting.md @@ -138,6 +138,28 @@ failing outright. **Linked issue:** [#1185](https://github.com/debpalash/VoiceStudio/issues/1185) +## 1c. Setup blocked: "System RAM … The app will OOM on first dub" + +**Symptom:** the setup wizard's System Check shows **System RAM** in red and +"Resolve blockers to continue" stays disabled — often on an 8 GB machine that +reports ~7.8 GB usable (firmware and integrated graphics reserve a slice of +installed RAM). + +**Cause:** the preflight compares OS-reported RAM against the 8 GB minimum. +Since [#1618](https://github.com/debpalash/VoiceStudio/issues/1618) the check +tolerates that reserved-memory gap, so 8 GB-installed machines pass. + +**Fix:** update to the latest release. If your machine is genuinely below the +minimum and you accept the out-of-memory risk (long dubs may crash), set +`OMNIVOICE_RAM_PREFLIGHT=0` before launching — the hard block becomes a +warning. On Windows run PowerShell +`[Environment]::SetEnvironmentVariable('OMNIVOICE_RAM_PREFLIGHT','0','User')` +and relaunch; on macOS/Linux export it in the shell that starts the app. (The +in-app Settings panel can't help here — this blocker appears before setup +completes.) + +**Linked issues:** [#1618](https://github.com/debpalash/VoiceStudio/issues/1618) + ## 2. HF 401 / pyannote license not accepted **Symptom:** dubbing fails with `HfHubHTTPError: 401 Client Error: Unauthorized