diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e6dd900..6dcd5b8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,8 @@ the frozen-backend fallback mirror it for their toolchains.
- Correct the Windows Rustup installation command in tooling and documentation (#2066) — thanks @Rukhaam!
+- Show local setup guidance when remote native engine installation is unavailable (#2166)
+
## [0.5.3] — 2026-09-17
**Highlights**
diff --git a/backend/api/routers/engines.py b/backend/api/routers/engines.py
index 7d4547bd..cd689cc4 100644
--- a/backend/api/routers/engines.py
+++ b/backend/api/routers/engines.py
@@ -21,12 +21,12 @@ import os
import threading
from time import perf_counter
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Request
from huggingface_hub import utils as hf_utils
from huggingface_hub.errors import HFValidationError
from pydantic import BaseModel, Field
-from api.dependencies import require_admin, require_admin_action, require_desktop
+from api.dependencies import require_admin, require_admin_action, require_desktop, is_loopback
from core import prefs
from core.engine_licenses import LICENSE_GATED_ENGINES
from services import tts_backend, asr_backend, llm_backend, translation_engines
@@ -116,18 +116,29 @@ def _is_hf_repo_id(value: str) -> bool:
return True
+def _request_install_capability(payload, request):
+ allowed = bool(request and request.client and is_loopback(request.client.host))
+ result = dict(payload)
+ result["backends"] = [dict(entry) for entry in payload["backends"]]
+ for entry in result["backends"]:
+ if entry.get("one_click_install") and not allowed:
+ entry["one_click_install"] = False
+ entry["local_install_required"] = True
+ return result
+
+
@router.get("/engines")
-def list_all_engines():
+def list_all_engines(request: Request):
return {
- "tts": _family_payload("tts", tts_backend),
+ "tts": _request_install_capability(_family_payload("tts", tts_backend), request),
"asr": _family_payload("asr", asr_backend),
"llm": _family_payload("llm", llm_backend),
}
@router.get("/engines/tts")
-def list_tts_backends():
- return _family_payload("tts", tts_backend)
+def list_tts_backends(request: Request):
+ return _request_install_capability(_family_payload("tts", tts_backend), request)
@router.get(
@@ -409,10 +420,10 @@ async def uninstall_translation_engine(engine_id: str):
"/engines/audiocpp/runtime/install/status",
dependencies=[Depends(require_admin)],
)
-def audiocpp_runtime_install_status():
+def audiocpp_runtime_install_status(request: Request):
from services import audiocpp_runtime_install
- return audiocpp_runtime_install.status()
+ return {**audiocpp_runtime_install.status(), "install_allowed": bool(request.client and is_loopback(request.client.host))}
@router.post(
@@ -485,7 +496,7 @@ def install_sidecar_engine(engine_id: str):
"/engines/sidecar/{engine_id}/install/status",
dependencies=[Depends(require_admin)],
)
-def sidecar_install_status(engine_id: str):
+def sidecar_install_status(engine_id: str, request: Request = None):
"""Step-by-step status of the sidecar install job (poll while running).
Shape: ``{engine_id, installed, managed, install_dir, job}`` where job is
@@ -494,7 +505,7 @@ def sidecar_install_status(engine_id: str):
"""
from services import sidecar_install
try:
- return sidecar_install.get_status(engine_id)
+ return {**sidecar_install.get_status(engine_id), "install_allowed": bool(request and request.client and is_loopback(request.client.host))}
except KeyError:
raise HTTPException(
status_code=404,
diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md
index e25807eb..b7238399 100644
--- a/docs/install/troubleshooting.md
+++ b/docs/install/troubleshooting.md
@@ -1193,3 +1193,7 @@ Electron and web imports accept UTF-8, UTF-16 with a byte-order mark, and Window
### Compile fallback after startup
An architecture accepted by the torch.compile preflight may still encounter independent Dynamo, Inductor, Triton, or CUDA-graph runtime errors. VoiceStudio distinguishes those from GPU memory exhaustion and retries with eager execution; architecture support alone does not guarantee compilation succeeds.
+
+### Native engine installation from a remote client
+
+Sidecar and audio.cpp runtime installation is restricted to requests from the backend computer's loopback interface. An API key does not bypass this restriction. The catalogue now shows local setup guidance instead of offering a remote install that will be rejected. Open the backend through `localhost` on that computer, or follow the engine's setup guide there. In Docker, bridge-network requests may not be loopback even when the browser runs on the host; use the documented container setup rather than weakening the native-install gate.
diff --git a/electron/src/renderer/src/features/settings/engine-install.tsx b/electron/src/renderer/src/features/settings/engine-install.tsx
index abcda4ed..5860c3cc 100644
--- a/electron/src/renderer/src/features/settings/engine-install.tsx
+++ b/electron/src/renderer/src/features/settings/engine-install.tsx
@@ -18,6 +18,7 @@ export function EngineInstall({ id }: { id: string }) {
queryFn: () =>
apiJson<{
installed: boolean;
+ install_allowed?: boolean;
job: null | {
state: string;
steps: { name?: string; state: string }[];
@@ -36,8 +37,15 @@ export function EngineInstall({ id }: { id: string }) {
{
+ if (status.data?.install_allowed === false) return;
setStarting(true);
setFailed(null);
setDismissedFailure(false);
@@ -54,6 +62,11 @@ export function EngineInstall({ id }: { id: string }) {
{t(running ? 'modelMaintenance.installing' : 'modelMaintenance.install')}
+ {status.data?.install_allowed === false && (
+
+ {t('engines.localInstallRequired')}
+
+ )}
{!dismissedFailure && (failed || status.isError || status.data?.job?.state === 'failed') && (
- expect(mock.toast.success).toHaveBeenCalledWith('settings.engine_switched'),
+ await waitFor(() => expect(mock.toast.success).toHaveBeenCalledWith('settings.engine_switched'));
+});
+
+it('explains remote native installation restrictions before a POST', async () => {
+ mock.api.mockImplementation((path: string) =>
+ Promise.resolve(
+ path === '/engines/diarisation'
+ ? {
+ active: 'pyannote',
+ options: [
+ {
+ id: 'audiocpp-sortformer',
+ label: 'Sortformer',
+ model_installed: true,
+ runtime_installed: false,
+ installed: false,
+ },
+ ],
+ }
+ : {
+ installed: false,
+ supported: true,
+ install_allowed: false,
+ job: { state: 'idle', progress: 0 },
+ },
+ ),
);
+ render(
+
+
+ ,
+ );
+ expect(await screen.findByText('engines.localInstallRequired')).toBeInTheDocument();
+ const button = screen.getByRole('button', { name: 'modelMaintenance.install' });
+ expect(button).toBeDisabled();
+ fireEvent.click(button);
+ expect(mock.api.mock.calls.some(([, init]) => init?.method === 'POST')).toBe(false);
});
diff --git a/electron/src/renderer/src/features/settings/model-settings.tsx b/electron/src/renderer/src/features/settings/model-settings.tsx
index 4504ae26..4e9517c7 100644
--- a/electron/src/renderer/src/features/settings/model-settings.tsx
+++ b/electron/src/renderer/src/features/settings/model-settings.tsx
@@ -61,6 +61,7 @@ export function DiarisationSettings() {
apiJson<{
installed: boolean;
supported: boolean;
+ install_allowed?: boolean;
job: { state: string; progress: number; error?: string | null };
}>('/engines/audiocpp/runtime/install/status'),
refetchInterval: (state) => (state.state.data?.job.state === 'running' ? 1_500 : 10_000),
@@ -75,6 +76,7 @@ export function DiarisationSettings() {
}, [client, runtime.data?.installed]);
const runtimeRunning = busy || runtime.data?.job.state === 'running';
const installRuntime = async () => {
+ if (runtime.data?.install_allowed === false) return;
setBusy(true);
setFailed(null);
try {
@@ -152,7 +154,12 @@ export function DiarisationSettings() {
void installRuntime()}
>
@@ -163,6 +170,13 @@ export function DiarisationSettings() {
: t('modelMaintenance.install')}
)}
+ {option.id === 'audiocpp-sortformer' &&
+ !option.runtime_installed &&
+ runtime.data?.install_allowed === false && (
+
+ {t('engines.localInstallRequired')}
+
+ )}
{option.installed && (
)}
+ {b.local_install_required && !b.available && (
+ {t('engines.localInstallRequired')}
+ )}
{/* Setup snippet: top-level on plain path-gated rows; a collapsed
"Manual install" fallback on one-click rows (open when the install
failed — the snippet IS the recovery path then). */}
diff --git a/frontend/src/components/engines/useEngineInventory.js b/frontend/src/components/engines/useEngineInventory.js
index de326708..09fa6281 100644
--- a/frontend/src/components/engines/useEngineInventory.js
+++ b/frontend/src/components/engines/useEngineInventory.js
@@ -60,6 +60,7 @@ export function normalizeEntry(entry) {
docs_url: entry.docs_url || null,
// The backend's sidecar provisioner can install this engine in-app.
one_click_install: entry.one_click_install === true,
+ local_install_required: entry.local_install_required === true,
// Routing (#21) — may be absent on a legacy/older backend payload.
effective_device: entry.effective_device || null,
routing_status: entry.routing_status || null,
diff --git a/frontend/src/i18n/locales/ar.json b/frontend/src/i18n/locales/ar.json
index 900f60b7..441c26f8 100644
--- a/frontend/src/i18n/locales/ar.json
+++ b/frontend/src/i18n/locales/ar.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "يحوّل الصوت إلى نص للنسخ والدبلجة والتسميات التوضيحية والإملاء.",
"familyDesc_llm": "مساعد نصي اختياري للترجمة وإعادة الصياغة. يؤدي إيقافه إلى تخطي هذه الخطوات.",
"mpsLongText": "يعمل {{engine}} على شريحة Apple Silicon (MPS) هنا، وهذا النص طويل — قد يتجاوز التوليد الحد الزمني. النص الأقصر، أو تقسيمه إلى عدة مقاطع، سيكون أسرع بكثير.",
- "inMemory": "في الذاكرة"
+ "inMemory": "في الذاكرة",
+ "localInstallRequired": "ثبّت هذا المحرك على جهاز الخادم باستخدام localhost أو دليل الإعداد. التثبيت عن بُعد معطّل."
},
"capture": {
"desc": "تعمل مفاتيح التشغيل السريع العامة فقط في تطبيق سطح المكتب. تستخدم واجهة مستخدم الويب اختصارًا <1>Ctrl+Shift+Space1> داخل الصفحة أثناء التركيز على النافذة.",
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index 3ae3dc42..8d56f6d8 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Transkribiert Audio für Synchronisation, Untertitel und Diktat.",
"familyDesc_llm": "Optionaler Texthelfer für Übersetzungen und Überarbeitungen. „Aus“ überspringt diese Schritte.",
"mpsLongText": "{{engine}} läuft hier auf Apple Silicon (MPS) und dieser Text ist lang — die Generierung überschreitet möglicherweise das Zeitbudget. Kürzerer Text oder eine Aufteilung in mehrere Takes ist deutlich schneller.",
- "inMemory": "Im Speicher"
+ "inMemory": "Im Speicher",
+ "localInstallRequired": "Installiere diese Engine auf dem Backend-Rechner über localhost oder die Einrichtungsanleitung. Die Ferninstallation ist deaktiviert."
},
"capture": {
"desc": "Globale Hotkeys funktionieren nur in der Desktop-App. Die Web-Benutzeroberfläche verwendet eine In-Page-Verknüpfung <1>Strg+Umschalt+Leertaste1>, während das Fenster den Fokus hat.",
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index 6b7750d0..3b2a500b 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -2237,7 +2237,8 @@
"generateCaveat": "{{engine}} may be slow on this machine — {{reason}}",
"cpuLongText": "{{engine}} runs on the CPU here, and this text is long — the generation may exceed the time budget. Shorter text, or a CPU-tuned engine (OmniVoice GGUF, Supertonic-3), will be much faster.",
"cpuLongTextTuned": "{{engine}} runs on the CPU here, and this text is long — the generation may exceed the time budget. Shorter text, or splitting it into a few takes, will be much faster.",
- "mpsLongText": "{{engine}} runs on Apple Silicon (MPS) here, and this text is long — the generation may exceed the time budget. Shorter text, or splitting it into a few takes, will be much faster."
+ "mpsLongText": "{{engine}} runs on Apple Silicon (MPS) here, and this text is long — the generation may exceed the time budget. Shorter text, or splitting it into a few takes, will be much faster.",
+ "localInstallRequired": "Install this engine on the backend computer using localhost or its setup guide. Remote installation is disabled."
},
"errors": {
"title": "This tab hit a snag.",
diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json
index a66442d3..da811cf3 100644
--- a/frontend/src/i18n/locales/es.json
+++ b/frontend/src/i18n/locales/es.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Convierte audio en texto para transcripciones, doblaje, subtítulos y dictado.",
"familyDesc_llm": "Asistente de texto opcional para traducciones y reescrituras. «Desactivado» omite esos pasos.",
"mpsLongText": "{{engine}} se ejecuta en Apple Silicon (MPS) aquí y este texto es largo: la generación puede superar el límite de tiempo. Un texto más corto, o dividirlo en varias tomas, será mucho más rápido.",
- "inMemory": "En memoria"
+ "inMemory": "En memoria",
+ "localInstallRequired": "Instala este motor en el equipo del servidor mediante localhost o su guía de configuración. La instalación remota está desactivada."
},
"capture": {
"desc": "Las teclas de acceso rápido globales solo funcionan en la aplicación de escritorio. La interfaz de usuario web utiliza un acceso directo <1>Ctrl+Shift+Espacio1> en la página mientras la ventana tiene el foco.",
diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json
index 29ef9fb7..84216656 100644
--- a/frontend/src/i18n/locales/fr.json
+++ b/frontend/src/i18n/locales/fr.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Transforme l’audio en texte pour la transcription, le doublage, les sous-titres et la dictée.",
"familyDesc_llm": "Assistant textuel facultatif pour la traduction et la réécriture. « Désactivé » ignore ces étapes.",
"mpsLongText": "{{engine}} fonctionne ici sur Apple Silicon (MPS) et ce texte est long — la génération risque de dépasser le budget de temps. Un texte plus court, ou découpé en plusieurs prises, sera bien plus rapide.",
- "inMemory": "En mémoire"
+ "inMemory": "En mémoire",
+ "localInstallRequired": "Installez ce moteur sur l’ordinateur du serveur via localhost ou son guide de configuration. L’installation à distance est désactivée."
},
"capture": {
"desc": "Les raccourcis clavier globaux ne fonctionnent que dans l'application de bureau. L'interface utilisateur Web utilise un raccourci <1>Ctrl+Shift+Espace1> sur la page lorsque la fenêtre a le focus.",
diff --git a/frontend/src/i18n/locales/hi.json b/frontend/src/i18n/locales/hi.json
index 6e524df3..614701fe 100644
--- a/frontend/src/i18n/locales/hi.json
+++ b/frontend/src/i18n/locales/hi.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "डबिंग, कैप्शन और डिक्टेशन के लिए ऑडियो को टेक्स्ट में बदलता है।",
"familyDesc_llm": "अनुवाद और पुनर्लेखन के लिए वैकल्पिक टेक्स्ट सहायक। “बंद” होने पर ये चरण छोड़ दिए जाते हैं।",
"mpsLongText": "यहाँ {{engine}} Apple Silicon (MPS) पर चलता है और यह टेक्स्ट लंबा है — जनरेशन समय सीमा पार कर सकता है। छोटा टेक्स्ट, या इसे कुछ हिस्सों में बाँटना, कहीं तेज़ रहेगा।",
- "inMemory": "मेमोरी में"
+ "inMemory": "मेमोरी में",
+ "localInstallRequired": "इस इंजन को बैकएंड कंप्यूटर पर localhost या सेटअप गाइड से इंस्टॉल करें। रिमोट इंस्टॉलेशन बंद है।"
},
"capture": {
"desc": "ग्लोबल हॉटकीज़ केवल डेस्कटॉप ऐप में काम करती हैं। वेब यूआई इन-पेज <1>Ctrl+Shift+Space1> शॉर्टकट का उपयोग करता है जबकि विंडो पर फोकस होता है।",
diff --git a/frontend/src/i18n/locales/id.json b/frontend/src/i18n/locales/id.json
index 81442e46..4093daaf 100644
--- a/frontend/src/i18n/locales/id.json
+++ b/frontend/src/i18n/locales/id.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Mengubah audio menjadi teks untuk transkripsi, sulih suara, teks layar, dan dikte.",
"familyDesc_llm": "Pembantu teks opsional untuk terjemahan dan penulisan ulang. “Nonaktif” akan melewati langkah tersebut.",
"mpsLongText": "{{engine}} berjalan di Apple Silicon (MPS) di sini dan teks ini panjang — proses mungkin melampaui batas waktu. Teks lebih pendek, atau membaginya menjadi beberapa bagian, akan jauh lebih cepat.",
- "inMemory": "Dalam memori"
+ "inMemory": "Dalam memori",
+ "localInstallRequired": "Instal mesin ini di komputer backend melalui localhost atau panduan penyiapannya. Instalasi jarak jauh dinonaktifkan."
},
"capture": {
"desc": "Tombol pintas global hanya berfungsi di aplikasi desktop. UI web menggunakan pintasan <1>Ctrl+Shift+Space1> dalam halaman saat jendela memiliki fokus.",
diff --git a/frontend/src/i18n/locales/it.json b/frontend/src/i18n/locales/it.json
index 233d224b..d8bb7dff 100644
--- a/frontend/src/i18n/locales/it.json
+++ b/frontend/src/i18n/locales/it.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Trasforma l’audio in testo per trascrizione, doppiaggio, sottotitoli e dettatura.",
"familyDesc_llm": "Assistente di testo opzionale per traduzioni e riscritture. “Disattivato” salta questi passaggi.",
"mpsLongText": "{{engine}} qui viene eseguito su Apple Silicon (MPS) e questo testo è lungo: la generazione potrebbe superare il limite di tempo. Un testo più breve, o diviso in più riprese, sarà molto più veloce.",
- "inMemory": "In memoria"
+ "inMemory": "In memoria",
+ "localInstallRequired": "Installa questo motore sul computer del backend tramite localhost o la guida alla configurazione. L’installazione remota è disabilitata."
},
"capture": {
"desc": "I tasti di scelta rapida globali funzionano solo nell'app desktop. L'interfaccia utente Web utilizza una scorciatoia <1>Ctrl+Shift+Spazio1> nella pagina mentre la finestra è attiva.",
diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json
index bb15f522..92e68af0 100644
--- a/frontend/src/i18n/locales/ja.json
+++ b/frontend/src/i18n/locales/ja.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "音声をテキストに変換し、ダビング、字幕、音声入力の文字起こしに使用します。",
"familyDesc_llm": "翻訳や書き換え用の任意のテキスト補助です。「オフ」にするとこれらの手順を省略します。",
"mpsLongText": "ここでは {{engine}} が Apple Silicon(MPS)で実行され、このテキストは長いため、生成が時間制限を超える可能性があります。テキストを短くするか、数回に分けたほうが大幅に高速です。",
- "inMemory": "メモリに読み込み済み"
+ "inMemory": "メモリに読み込み済み",
+ "localInstallRequired": "バックエンドのコンピューターで localhost またはセットアップガイドを使ってこのエンジンをインストールしてください。リモートインストールは無効です。"
},
"capture": {
"desc": "グローバル ホットキーはデスクトップ アプリでのみ機能します。 Web UI は、ウィンドウにフォーカスがある間、ページ内 <1>Ctrl+Shift+Space1> ショートカットを使用します。",
diff --git a/frontend/src/i18n/locales/ko.json b/frontend/src/i18n/locales/ko.json
index b63dda01..5a89bc8e 100644
--- a/frontend/src/i18n/locales/ko.json
+++ b/frontend/src/i18n/locales/ko.json
@@ -1141,7 +1141,8 @@
"unload": "언로드",
"unloadFailed": "언로드하지 못했습니다: {{message}}",
"unloading": "언로드 중…",
- "mpsLongText": "여기서는 {{engine}}이(가) Apple Silicon(MPS)에서 실행되며 이 텍스트는 길어 생성이 시간 제한을 초과할 수 있습니다. 텍스트를 줄이거나 여러 번에 나누면 훨씬 빠릅니다."
+ "mpsLongText": "여기서는 {{engine}}이(가) Apple Silicon(MPS)에서 실행되며 이 텍스트는 길어 생성이 시간 제한을 초과할 수 있습니다. 텍스트를 줄이거나 여러 번에 나누면 훨씬 빠릅니다.",
+ "localInstallRequired": "백엔드 컴퓨터에서 localhost 또는 설정 가이드를 사용하여 이 엔진을 설치하세요. 원격 설치는 비활성화되어 있습니다."
},
"capture": {
"desc": "전역 단축키는 데스크톱 앱에서만 작동합니다. 웹 UI는 창에 포커스가 있는 동안 페이지 내 <1>Ctrl+Shift+Space1> 단축키를 사용합니다.",
diff --git a/frontend/src/i18n/locales/nl.json b/frontend/src/i18n/locales/nl.json
index 6e4e6cf9..a5b5c962 100644
--- a/frontend/src/i18n/locales/nl.json
+++ b/frontend/src/i18n/locales/nl.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Zet audio om in tekst voor transcriptie, nasynchronisatie, ondertiteling en dicteren.",
"familyDesc_llm": "Optionele teksthulp voor vertalen en herschrijven. ‘Uit’ slaat deze stappen over.",
"mpsLongText": "{{engine}} draait hier op Apple Silicon (MPS) en deze tekst is lang — de generatie overschrijdt mogelijk het tijdsbudget. Kortere tekst, of opsplitsen in meerdere takes, is veel sneller.",
- "inMemory": "In het geheugen"
+ "inMemory": "In het geheugen",
+ "localInstallRequired": "Installeer deze engine op de backendcomputer via localhost of de installatiehandleiding. Installatie op afstand is uitgeschakeld."
},
"capture": {
"desc": "Globale sneltoetsen werken alleen in de desktop-app. De webinterface gebruikt een sneltoets <1>Ctrl+Shift+Spatie1> op de pagina terwijl het venster focus heeft.",
diff --git a/frontend/src/i18n/locales/pl.json b/frontend/src/i18n/locales/pl.json
index 95dc968c..bf8235a1 100644
--- a/frontend/src/i18n/locales/pl.json
+++ b/frontend/src/i18n/locales/pl.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Zamienia dźwięk na tekst do transkrypcji, dubbingu, napisów i dyktowania.",
"familyDesc_llm": "Opcjonalny pomocnik tekstowy do tłumaczeń i przeredagowywania. „Wył.” pomija te kroki.",
"mpsLongText": "{{engine}} działa tutaj na Apple Silicon (MPS), a ten tekst jest długi — generowanie może przekroczyć limit czasu. Krótszy tekst lub podzielenie go na kilka części będzie znacznie szybsze.",
- "inMemory": "W pamięci"
+ "inMemory": "W pamięci",
+ "localInstallRequired": "Zainstaluj ten silnik na komputerze zaplecza przez localhost lub zgodnie z instrukcją konfiguracji. Instalacja zdalna jest wyłączona."
},
"capture": {
"desc": "Globalne skróty klawiszowe działają tylko w aplikacji komputerowej. Interfejs WWW korzysta ze skrótu <1>Ctrl+Shift+Spacja1> znajdującego się na stronie, gdy okno jest aktywne.",
diff --git a/frontend/src/i18n/locales/pt.json b/frontend/src/i18n/locales/pt.json
index 82e27ec5..aa06b40a 100644
--- a/frontend/src/i18n/locales/pt.json
+++ b/frontend/src/i18n/locales/pt.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Transforma áudio em texto para transcrição, dublagem, legendas e ditado.",
"familyDesc_llm": "Assistente de texto opcional para tradução e reescrita. “Desativado” ignora essas etapas.",
"mpsLongText": "{{engine}} é executado em Apple Silicon (MPS) aqui e este texto é longo — a geração pode ultrapassar o limite de tempo. Um texto mais curto, ou dividido em algumas partes, será bem mais rápido.",
- "inMemory": "Na memória"
+ "inMemory": "Na memória",
+ "localInstallRequired": "Instale este motor no computador do backend pelo localhost ou pelo guia de configuração. A instalação remota está desativada."
},
"capture": {
"desc": "As teclas de atalho globais funcionam apenas no aplicativo de desktop. A IU da web usa um atalho <1>Ctrl+Shift+Space1> na página enquanto a janela está em foco.",
diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json
index 3dd6e082..5e71f9c3 100644
--- a/frontend/src/i18n/locales/ru.json
+++ b/frontend/src/i18n/locales/ru.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Преобразует аудио в текст для транскрипции, дубляжа, субтитров и диктовки.",
"familyDesc_llm": "Необязательный текстовый помощник для перевода и перефразирования. Режим «Выкл.» пропускает эти этапы.",
"mpsLongText": "Здесь {{engine}} работает на Apple Silicon (MPS), а этот текст длинный — генерация может превысить лимит времени. Более короткий текст или разбиение на несколько частей будет намного быстрее.",
- "inMemory": "В памяти"
+ "inMemory": "В памяти",
+ "localInstallRequired": "Установите этот движок на компьютере сервера через localhost или по инструкции. Удалённая установка отключена."
},
"capture": {
"desc": "Глобальные горячие клавиши работают только в настольном приложении. Веб-интерфейс использует внутристраничный ярлык <1>Ctrl+Shift+Space1>, пока окно находится в фокусе.",
diff --git a/frontend/src/i18n/locales/sv.json b/frontend/src/i18n/locales/sv.json
index 9d28b472..003705db 100644
--- a/frontend/src/i18n/locales/sv.json
+++ b/frontend/src/i18n/locales/sv.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Gör om ljud till text för transkribering, dubbning, undertexter och diktering.",
"familyDesc_llm": "Valfri texthjälp för översättning och omskrivning. ”Av” hoppar över dessa steg.",
"mpsLongText": "{{engine}} körs på Apple Silicon (MPS) här och den här texten är lång — genereringen kan överskrida tidsbudgeten. Kortare text, eller uppdelad i flera tagningar, går betydligt snabbare.",
- "inMemory": "I minnet"
+ "inMemory": "I minnet",
+ "localInstallRequired": "Installera motorn på backenddatorn via localhost eller installationsguiden. Fjärrinstallation är avstängd."
},
"capture": {
"desc": "Globala snabbtangenter fungerar bara i skrivbordsappen. Webbgränssnittet använder en genväg <1>Ctrl+Skift+Mellanslag1> på sidan medan fönstret har fokus.",
diff --git a/frontend/src/i18n/locales/th.json b/frontend/src/i18n/locales/th.json
index c439c7cd..dfed115b 100644
--- a/frontend/src/i18n/locales/th.json
+++ b/frontend/src/i18n/locales/th.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "แปลงเสียงเป็นข้อความสำหรับการถอดเสียง การพากย์ คำบรรยาย และการป้อนตามเสียง",
"familyDesc_llm": "ผู้ช่วยข้อความเสริมสำหรับการแปลและเขียนใหม่ เมื่อ “ปิด” จะข้ามขั้นตอนเหล่านี้",
"mpsLongText": "{{engine}} ทำงานบน Apple Silicon (MPS) ที่นี่ และข้อความนี้ยาว — การสร้างอาจเกินงบประมาณเวลา ข้อความที่สั้นลง หรือแบ่งเป็นหลายส่วน จะเร็วกว่ามาก",
- "inMemory": "อยู่ในหน่วยความจำ"
+ "inMemory": "อยู่ในหน่วยความจำ",
+ "localInstallRequired": "ติดตั้งเอนจินนี้บนคอมพิวเตอร์แบ็กเอนด์ผ่าน localhost หรือคู่มือการตั้งค่า การติดตั้งจากระยะไกลถูกปิดใช้งาน"
},
"capture": {
"desc": "ปุ่มลัดส่วนกลางใช้งานได้ในแอปเดสก์ท็อปเท่านั้น UI ของเว็บใช้ทางลัด <1>Ctrl+Shift+Space1> ในเพจในขณะที่หน้าต่างมีโฟกัส",
diff --git a/frontend/src/i18n/locales/tr.json b/frontend/src/i18n/locales/tr.json
index 1f92c630..559e0381 100644
--- a/frontend/src/i18n/locales/tr.json
+++ b/frontend/src/i18n/locales/tr.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Sesi; transkripsiyon, dublaj, altyazı ve dikte için metne dönüştürür.",
"familyDesc_llm": "Çeviri ve yeniden yazma için isteğe bağlı metin yardımcısıdır. “Kapalı” bu adımları atlar.",
"mpsLongText": "{{engine}} burada Apple Silicon (MPS) üzerinde çalışıyor ve bu metin uzun — üretim süre bütçesini aşabilir. Daha kısa bir metin ya da birkaç parçaya bölmek çok daha hızlı olacaktır.",
- "inMemory": "Bellekte"
+ "inMemory": "Bellekte",
+ "localInstallRequired": "Bu motoru arka uç bilgisayarında localhost veya kurulum kılavuzu üzerinden yükleyin. Uzaktan kurulum devre dışıdır."
},
"capture": {
"desc": "Genel kısayol tuşları yalnızca masaüstü uygulamasında çalışır. Web kullanıcı arayüzü, pencere odaktayken sayfa içi <1>Ctrl+Shift+Space1> kısayolunu kullanır.",
diff --git a/frontend/src/i18n/locales/uk.json b/frontend/src/i18n/locales/uk.json
index ecc60d36..0536c08f 100644
--- a/frontend/src/i18n/locales/uk.json
+++ b/frontend/src/i18n/locales/uk.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Перетворює аудіо на текст для транскрипції, дубляжу, субтитрів і диктування.",
"familyDesc_llm": "Необов’язковий текстовий помічник для перекладу й переписування. «Вимкнено» пропускає ці кроки.",
"mpsLongText": "Тут {{engine}} працює на Apple Silicon (MPS), а цей текст довгий — генерація може перевищити ліміт часу. Коротший текст або розбиття на кілька частин буде значно швидшим.",
- "inMemory": "У пам’яті"
+ "inMemory": "У пам’яті",
+ "localInstallRequired": "Установіть цей рушій на комп’ютері сервера через localhost або за інструкцією. Віддалене встановлення вимкнено."
},
"capture": {
"desc": "Глобальні гарячі клавіші працюють лише в настільній програмі. Веб-інтерфейс використовує ярлик <1>Ctrl+Shift+Пробіл1> на сторінці, коли вікно має фокус.",
diff --git a/frontend/src/i18n/locales/vi.json b/frontend/src/i18n/locales/vi.json
index e054c5b3..733de77b 100644
--- a/frontend/src/i18n/locales/vi.json
+++ b/frontend/src/i18n/locales/vi.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "Chuyển âm thanh thành văn bản để phiên âm, lồng tiếng, tạo phụ đề và đọc chính tả.",
"familyDesc_llm": "Trợ lý văn bản tùy chọn cho dịch thuật và viết lại. “Tắt” sẽ bỏ qua các bước này.",
"mpsLongText": "{{engine}} chạy trên Apple Silicon (MPS) ở đây và văn bản này dài — quá trình tạo có thể vượt quá giới hạn thời gian. Văn bản ngắn hơn, hoặc chia thành vài phần, sẽ nhanh hơn nhiều.",
- "inMemory": "Trong bộ nhớ"
+ "inMemory": "Trong bộ nhớ",
+ "localInstallRequired": "Cài đặt công cụ này trên máy chủ qua localhost hoặc hướng dẫn thiết lập. Cài đặt từ xa đã bị tắt."
},
"capture": {
"desc": "Phím nóng chung chỉ hoạt động trong ứng dụng máy tính để bàn. Giao diện người dùng web sử dụng phím tắt <1>Ctrl+Shift+Space1> trong trang trong khi cửa sổ đang có tiêu điểm.",
diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json
index ebcecdd5..0b93b4dd 100644
--- a/frontend/src/i18n/locales/zh-CN.json
+++ b/frontend/src/i18n/locales/zh-CN.json
@@ -1093,7 +1093,8 @@
"installStep_fetch_weights": "正在下载模型权重",
"installStep_persist": "正在保存配置",
"installAria": "安装 {{engine}}",
- "mpsLongText": "{{engine}} 在本机上使用 Apple Silicon(MPS)运行,而这段文本较长——生成可能超出时间预算。缩短文本,或分成几次生成会快得多。"
+ "mpsLongText": "{{engine}} 在本机上使用 Apple Silicon(MPS)运行,而这段文本较长——生成可能超出时间预算。缩短文本,或分成几次生成会快得多。",
+ "localInstallRequired": "请在后端计算机上通过 localhost 或安装指南安装此引擎。远程安装已禁用。"
},
"capture": {
"desc": "全局热键仅在桌面应用中有效。网页界面使用页面内快捷键 <1>Ctrl+Shift+Space1>(窗口聚焦时可用)。",
diff --git a/frontend/src/i18n/locales/zh-TW.json b/frontend/src/i18n/locales/zh-TW.json
index 9c5f41a4..72a98f1d 100644
--- a/frontend/src/i18n/locales/zh-TW.json
+++ b/frontend/src/i18n/locales/zh-TW.json
@@ -864,7 +864,8 @@
"familyDesc_asr": "將音訊轉換為文字,用於轉錄、配音、字幕和聽寫。",
"familyDesc_llm": "用於翻譯和改寫的選用文字助理。「關閉」會略過這些步驟。",
"mpsLongText": "{{engine}} 在此機器上使用 Apple Silicon(MPS)執行,而這段文字較長 — 生成可能超出時間預算。縮短文字或分成幾次生成會快得多。",
- "inMemory": "已載入記憶體"
+ "inMemory": "已載入記憶體",
+ "localInstallRequired": "請在後端電腦上透過 localhost 或安裝指南安裝此引擎。遠端安裝已停用。"
},
"capture": {
"desc": "全域熱鍵僅適用於桌面應用程式。當視窗具有焦點時,Web UI 使用頁內 <1>Ctrl+Shift+Space1> 快速鍵。",
diff --git a/tests/test_engine_install_capability.py b/tests/test_engine_install_capability.py
new file mode 100644
index 00000000..1da68f58
--- /dev/null
+++ b/tests/test_engine_install_capability.py
@@ -0,0 +1,32 @@
+import httpx
+import pytest
+from fastapi import FastAPI
+from api.dependencies import require_admin
+from api.routers import engines
+from services import sidecar_install, audiocpp_runtime_install
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('host,allowed', [('127.0.0.1', True), ('::1', True), ('192.0.2.10', False)])
+async def test_catalogue_matches_native_install_boundary(host, allowed, monkeypatch):
+ payload = {'backends': [{'id': 'example', 'one_click_install': True, 'available': False}]}
+ monkeypatch.setattr(engines, '_family_payload', lambda *args: payload)
+ monkeypatch.setattr(sidecar_install, 'get_status', lambda engine: {'installed': False, 'job': None})
+ monkeypatch.setattr(audiocpp_runtime_install, 'status', lambda: {'installed': False, 'supported': True})
+ calls = []
+ monkeypatch.setattr(sidecar_install, 'start_install', lambda engine: calls.append(engine) or {'status': 'started'})
+ monkeypatch.setattr(audiocpp_runtime_install, 'start_install', lambda: calls.append('native') or {'status': 'started'})
+ app = FastAPI()
+ app.include_router(engines.router)
+ app.dependency_overrides[require_admin] = lambda: None
+ async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, client=(host, 1234)), base_url='http://test') as client:
+ listing = (await client.get('/engines')).json()['tts']['backends'][0]
+ assert listing['one_click_install'] is allowed
+ assert listing.get('local_install_required', False) is (not allowed)
+ assert payload['backends'][0]['one_click_install'] is True # no cached registry mutation
+ for path in ['/engines/sidecar/example/install', '/engines/audiocpp/runtime/install']:
+ status = (await client.get(path + '/status')).json()
+ assert status['install_allowed'] is allowed
+ response = await client.post(path)
+ assert response.status_code == (200 if allowed else 403)
+ assert len(calls) == (2 if allowed else 0)