feat(settings): LLM provider testing pass — latency + classified errors, model discovery, full i18n, router tests (#887)

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-07-02 17:03:13 +05:30
committed by GitHub
co-authored by mergetest Claude Fable 5
parent bb492086c9
commit da9315815d
27 changed files with 1204 additions and 45 deletions
+11
View File
@@ -8,6 +8,17 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
## [Unreleased]
### Added
- **LLM Providers: one-click connection testing with real diagnostics.** The
Test button in Settings → LLM Providers now measures round-trip latency and
turns failures into plain-language guidance — bad key (401/403), wrong
model or URL (404), rate-limited (429), or unreachable server — instead of
a raw exception dump. A new "Fetch models" button lists every model your
key can access so you pick from real names instead of guessing. The whole
panel is now translated into all 21 languages, provider error messages
never echo your API key, and the settings API gained full test coverage.
### Changed
- **The app now always opens maximized (not fullscreen).** Window size and
+91 -6
View File
@@ -293,13 +293,58 @@ def set_active_llm_provider(body: _LLMActiveBody):
return list_llm_providers()
def _scrub_llm_detail(e: Exception, api_key: str | None) -> str:
"""Scrubbed, UI-safe failure text. scrub_text() covers env secrets and
home paths — but a STORE-persisted key isn't in the env, and some
providers echo the key in error bodies, so redact the exact resolved key
explicitly before the generic pass."""
from core.scrub import scrub_text
detail = f"{type(e).__name__}: {e}"
if api_key and api_key != "local" and len(api_key) >= 8:
detail = detail.replace(api_key, "•••")
return scrub_text(detail)
def _classify_llm_error(e: Exception) -> str:
"""Map a provider-call failure to an actionable kind the UI can localize.
Kinds: auth (bad/missing key), not_found (model or endpoint path),
rate_limit, network (DNS/conn/timeout), error (everything else).
Status codes win when the OpenAI SDK provides one; exception-family
names catch the non-HTTP failures (DNS, refused, TLS, timeout).
"""
status = getattr(e, "status_code", None)
if status in (401, 403):
return "auth"
if status == 404:
return "not_found"
if status == 429:
return "rate_limit"
name = type(e).__name__
if name in ("APIConnectionError", "APITimeoutError", "ConnectError",
"ConnectTimeout", "TimeoutError"):
return "network"
if name == "AuthenticationError":
return "auth"
if name == "NotFoundError":
return "not_found"
if name == "RateLimitError":
return "rate_limit"
return "error"
@router.post("/llm-providers/{provider_id}/test")
def test_llm_provider(provider_id: str):
"""One cheap round-trip against a provider to prove the key/URL work.
Temporarily activates the provider for the probe by resolving its config
directly (does not change the persisted active selection).
directly (does not change the persisted active selection). Returns
latency_ms plus, on failure, a classified ``kind`` (config / auth /
not_found / rate_limit / network / error) so the UI shows an actionable,
localizable message instead of a raw exception string.
"""
import time as _time
from services import llm_providers
p = llm_providers.get_provider(provider_id)
if p is None:
@@ -307,9 +352,10 @@ def test_llm_provider(provider_id: str):
base_url = llm_providers.resolve_base_url(p)
api_key = llm_providers.resolve_api_key(p)
if not base_url:
return {"ok": False, "detail": "No Base URL set for this provider."}
return {"ok": False, "kind": "config", "detail": "No Base URL set for this provider."}
if not api_key:
return {"ok": False, "detail": "No API key configured for this provider."}
return {"ok": False, "kind": "config", "detail": "No API key configured for this provider."}
t0 = _time.monotonic()
try:
from openai import OpenAI
client = OpenAI(api_key=api_key, base_url=base_url)
@@ -319,10 +365,49 @@ def test_llm_provider(provider_id: str):
timeout=20,
)
reply = (res.choices[0].message.content or "").strip()
return {"ok": True, "model": llm_providers.resolve_model(p), "reply": reply[:80]}
return {
"ok": True,
"model": llm_providers.resolve_model(p),
"reply": reply[:80],
"latency_ms": int((_time.monotonic() - t0) * 1000),
}
except Exception as e: # noqa: BLE001 — surface a clean, scrubbed error to the UI
from core.scrub import scrub_text
return {"ok": False, "detail": scrub_text(f"{type(e).__name__}: {e}")}
return {
"ok": False,
"kind": _classify_llm_error(e),
"detail": _scrub_llm_detail(e, api_key),
"latency_ms": int((_time.monotonic() - t0) * 1000),
}
@router.get("/llm-providers/{provider_id}/models")
def list_llm_provider_models(provider_id: str):
"""List model ids the provider's key can access (OpenAI-compat /models).
Powers the model-picker datalist in Settings → LLM Providers so users
don't have to guess model names. Read-only; failures return the same
classified shape as /test; capped so a huge catalog can't bloat the UI.
"""
from services import llm_providers
p = llm_providers.get_provider(provider_id)
if p is None:
raise HTTPException(status_code=404, detail=f"unknown provider {provider_id!r}")
base_url = llm_providers.resolve_base_url(p)
api_key = llm_providers.resolve_api_key(p)
if not base_url or not api_key:
return {"ok": False, "kind": "config", "models": []}
try:
from openai import OpenAI
client = OpenAI(api_key=api_key, base_url=base_url)
ids = sorted(m.id for m in client.models.list(timeout=10))
return {"ok": True, "models": ids[:200]}
except Exception as e: # noqa: BLE001
return {
"ok": False,
"kind": _classify_llm_error(e),
"detail": _scrub_llm_detail(e, api_key),
"models": [],
}
# ── License acceptance (Phase 3 Plan 03-01 / TTS-05) ──────────────────────
@@ -1,5 +1,5 @@
/**
* Settings → System → LLM Providers (v0.3.8).
* Settings → System → LLM Providers (v0.3.8; test/UX/i18n pass for v0.3.9).
*
* One place to configure the high-quality LLM that powers Cinematic and
* Autofit translation (fitting each line to its segment's time budget). Every
@@ -13,15 +13,22 @@
* notes,base_url,model,has_key,key_from_env,configured}]}
* PUT /api/settings/llm-providers/{id} {api_key?,base_url?,model?,account_id?,make_active?}
* POST /api/settings/llm-providers/active {provider}
* POST /api/settings/llm-providers/{id}/test → {ok, model?, reply?, detail?}
* POST /api/settings/llm-providers/{id}/test
* → {ok, model?, reply?, latency_ms?, kind?, detail?} (kind: config|auth|
* not_found|rate_limit|network|error → localized message below)
* GET /api/settings/llm-providers/{id}/models → {ok, models[], kind?}
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Brain, ExternalLink } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { apiJson, apiFetch, apiPost } from '../../api/client';
import { SettingsSection, SettingRow, SettingsInput } from './primitives';
import { Button, Badge, Select } from '../../ui';
const MODELS_DATALIST_ID = 'llm-provider-models-list';
export default function LLMProvidersPanel() {
const { t } = useTranslation();
const [providers, setProviders] = useState([]);
const [active, setActive] = useState(null);
const [editing, setEditing] = useState('');
@@ -29,6 +36,8 @@ export default function LLMProvidersPanel() {
const [saving, setSaving] = useState(false);
const [testing, setTesting] = useState(false);
const [test, setTest] = useState(null);
const [models, setModels] = useState(null); // null = not fetched; [] = fetched, none
const [loadingModels, setLoadingModels] = useState(false);
const [error, setError] = useState(null);
const current = useMemo(
@@ -36,6 +45,21 @@ export default function LLMProvidersPanel() {
[providers, editing],
);
// Failure kinds from /test and /models → localized, actionable messages.
const kindMessage = useCallback(
(res) => {
const byKind = {
config: t('settings.llmp_err_config'),
auth: t('settings.llmp_err_auth'),
not_found: t('settings.llmp_err_not_found'),
rate_limit: t('settings.llmp_err_rate_limit'),
network: t('settings.llmp_err_network'),
};
return byKind[res?.kind] || res?.detail || t('settings.llmp_err_error');
},
[t],
);
const populate = useCallback((list, id) => {
const p = list.find((x) => x.id === id);
if (!p) return;
@@ -43,6 +67,7 @@ export default function LLMProvidersPanel() {
// sane default; api_key is never echoed (only the has_key flag comes back).
setFields({ base_url: p.base_url || '', model: p.model || '', api_key: '', account_id: '' });
setTest(null);
setModels(null);
}, []);
const refresh = useCallback(
@@ -62,10 +87,10 @@ export default function LLMProvidersPanel() {
populate(data.providers || [], pick);
return data;
} catch (e) {
setError(e?.message || 'Failed to load providers');
setError(e?.message || t('settings.llmp_load_failed'));
}
},
[populate],
[populate, t],
);
useEffect(() => {
@@ -96,7 +121,7 @@ export default function LLMProvidersPanel() {
});
await refresh(current.id);
} catch (e) {
setError(e?.message || 'Failed to save');
setError(e?.message || t('settings.llmp_save_failed'));
} finally {
setSaving(false);
}
@@ -113,18 +138,40 @@ export default function LLMProvidersPanel() {
const res = await apiPost(`/api/settings/llm-providers/${current.id}/test`);
setTest(res);
} catch (e) {
setTest({ ok: false, detail: e?.message || 'Test failed' });
setTest({ ok: false, detail: e?.message || t('settings.llmp_err_error') });
} finally {
setTesting(false);
}
};
const fetchModels = async () => {
if (!current) return;
setLoadingModels(true);
setError(null);
try {
// Save non-key fields first so the probe uses the just-typed base URL.
await save(false);
const res = await apiJson(`/api/settings/llm-providers/${current.id}/models`);
if (res.ok) {
setModels(res.models || []);
} else {
setModels([]);
setTest({ ok: false, kind: res.kind, detail: res.detail });
}
} catch (e) {
setModels([]);
setTest({ ok: false, detail: e?.message || t('settings.llmp_err_error') });
} finally {
setLoadingModels(false);
}
};
if (!providers.length) {
return (
<SettingsSection
icon={Brain}
title="LLM Providers"
description="Configure a high-quality LLM for Cinematic & Autofit translation."
title={t('settings.llm_providers')}
description={t('settings.llmp_desc')}
>
{error && (
<div className="perfpanel__error" role="alert">
@@ -140,12 +187,12 @@ export default function LLMProvidersPanel() {
return (
<SettingsSection
icon={Brain}
title="LLM Providers"
description="Powers Cinematic & Autofit translation — the LLM rewrites each line to fit its segment's time budget so the video timing holds. Keys are stored encrypted; local providers (Ollama/LM Studio) stay fully offline."
title={t('settings.llm_providers')}
description={t('settings.llmp_desc')}
>
<SettingRow
title="Provider"
hint="Pick a provider to configure. The active one is used for Cinematic/Autofit translation. Local providers need no key but require their server to be running."
title={t('settings.llmp_provider')}
hint={t('settings.llmp_provider_hint')}
control={
<Select
value={editing}
@@ -155,9 +202,9 @@ export default function LLMProvidersPanel() {
{providers.map((p) => (
<option key={p.id} value={p.id}>
{p.display_name}
{p.local ? ' · local' : ''}
{p.local ? ` · ${t('settings.llmp_local_tag')}` : ''}
{p.configured ? ' ✓' : ''}
{active === p.id ? ' (active)' : ''}
{active === p.id ? ` (${t('settings.llmp_active_badge')})` : ''}
</option>
))}
</Select>
@@ -168,7 +215,7 @@ export default function LLMProvidersPanel() {
<>
{(current.notes || current.signup_url) && (
<SettingRow
title="About"
title={t('settings.llmp_about')}
control={
<div className="flex flex-col gap-[4px] min-w-0">
{current.notes && <span className="text-[12px] opacity-70">{current.notes}</span>}
@@ -179,7 +226,7 @@ export default function LLMProvidersPanel() {
rel="noreferrer"
className="text-[12px] inline-flex items-center gap-[4px] opacity-80 hover:opacity-100"
>
Get an API key <ExternalLink size={12} />
{t('settings.llmp_get_key')} <ExternalLink size={12} />
</a>
)}
</div>
@@ -189,14 +236,14 @@ export default function LLMProvidersPanel() {
{current.needs_account && (
<SettingRow
title="Account ID"
title={t('settings.llmp_account_id')}
control={
<SettingsInput
mono
type="text"
value={fields.account_id}
onChange={(e) => setFields((f) => ({ ...f, account_id: e.target.value }))}
placeholder="Cloudflare account id"
placeholder={t('settings.llmp_account_placeholder')}
data-testid="llm-account-id"
/>
}
@@ -205,7 +252,7 @@ export default function LLMProvidersPanel() {
{!current.local && (
<SettingRow
title="API key"
title={t('settings.llmp_api_key')}
control={
<SettingsInput
mono
@@ -214,10 +261,10 @@ export default function LLMProvidersPanel() {
onChange={(e) => setFields((f) => ({ ...f, api_key: e.target.value }))}
placeholder={
current.key_from_env
? 'set via environment (.env) — overrides this field'
? t('settings.llmp_key_env')
: current.has_key
? 'stored — type to replace'
: 'paste your API key'
? t('settings.llmp_key_stored')
: t('settings.llmp_key_paste')
}
disabled={current.key_from_env}
data-testid="llm-provider-key"
@@ -227,7 +274,7 @@ export default function LLMProvidersPanel() {
)}
<SettingRow
title="Base URL"
title={t('settings.llmp_base_url')}
control={
<SettingsInput
mono
@@ -240,16 +287,41 @@ export default function LLMProvidersPanel() {
}
/>
<SettingRow
title="Model"
title={t('settings.llmp_model')}
hint={
models?.length
? t('settings.llmp_models_loaded', { count: models.length })
: undefined
}
control={
<SettingsInput
mono
type="text"
value={fields.model}
onChange={(e) => setFields((f) => ({ ...f, model: e.target.value }))}
placeholder="model name"
data-testid="llm-provider-model"
/>
<div className="flex items-center gap-[8px] min-w-0">
<SettingsInput
mono
type="text"
value={fields.model}
onChange={(e) => setFields((f) => ({ ...f, model: e.target.value }))}
placeholder={t('settings.llmp_model_placeholder')}
list={models?.length ? MODELS_DATALIST_ID : undefined}
data-testid="llm-provider-model"
/>
<Button
variant="subtle"
size="sm"
onClick={fetchModels}
loading={loadingModels}
disabled={saving || testing || loadingModels}
data-testid="llm-provider-models"
>
{t('settings.llmp_fetch_models')}
</Button>
{models?.length ? (
<datalist id={MODELS_DATALIST_ID}>
{models.map((m) => (
<option key={m} value={m} />
))}
</datalist>
) : null}
</div>
}
/>
@@ -260,7 +332,7 @@ export default function LLMProvidersPanel() {
)}
<SettingRow
title="Status"
title={t('settings.llmp_status')}
control={
<div className="flex flex-wrap items-center gap-[8px]">
<Button
@@ -271,7 +343,7 @@ export default function LLMProvidersPanel() {
disabled={saving || testing}
data-testid="llm-provider-save"
>
Save
{t('settings.llmp_save')}
</Button>
<Button
variant="primary"
@@ -281,7 +353,7 @@ export default function LLMProvidersPanel() {
disabled={saving || testing}
data-testid="llm-provider-activate"
>
{isActive ? 'Save & keep active' : 'Save & use for translation'}
{isActive ? t('settings.llmp_save_keep') : t('settings.llmp_save_active')}
</Button>
<Button
variant="subtle"
@@ -291,16 +363,21 @@ export default function LLMProvidersPanel() {
disabled={saving || testing}
data-testid="llm-provider-test"
>
Test
{t('settings.llmp_test')}
</Button>
{isActive && (
<Badge tone="success" dot role="status">
active
{t('settings.llmp_active_badge')}
</Badge>
)}
{test && (
<Badge tone={test.ok ? 'success' : 'warn'} role="status">
{test.ok ? `ok — ${test.model || ''}` : test.detail || 'failed'}
{test.ok
? t('settings.llmp_test_ok', {
model: test.model || '',
ms: test.latency_ms ?? '—',
})
: kindMessage(test)}
</Badge>
)}
</div>
@@ -0,0 +1,131 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import LLMProvidersPanel from './LLMProvidersPanel';
const PROVIDERS = {
active: 'groq',
providers: [
{
id: 'groq',
display_name: 'Groq',
local: false,
needs_account: false,
signup_url: 'https://console.groq.com',
notes: 'fast inference',
base_url: 'https://api.groq.com/openai/v1',
model: 'llama-3.3-70b',
has_key: true,
key_from_env: false,
configured: true,
},
{
id: 'ollama',
display_name: 'Ollama',
local: true,
needs_account: false,
signup_url: null,
notes: null,
base_url: 'http://localhost:11434/v1',
model: 'llama3',
has_key: false,
key_from_env: false,
configured: false,
},
],
};
function mockFetchSequence(...responses) {
const fn = vi.fn();
for (const r of responses) {
fn.mockResolvedValueOnce({
ok: (r.status ?? 200) >= 200 && (r.status ?? 200) < 300,
status: r.status ?? 200,
json: async () => r.body,
text: async () => JSON.stringify(r.body),
});
}
return fn;
}
describe('LLMProvidersPanel', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it('loads providers and preselects the active one', async () => {
global.fetch = mockFetchSequence({ body: PROVIDERS });
render(<LLMProvidersPanel />);
const select = await screen.findByTestId('llm-provider-select');
await waitFor(() => expect(select.value).toBe('groq'));
expect(screen.getByTestId('llm-provider-base-url').value).toBe(
'https://api.groq.com/openai/v1',
);
});
it('successful test shows model + latency badge', async () => {
global.fetch = mockFetchSequence(
{ body: PROVIDERS }, // mount GET
{ body: {} }, // save PUT
{ body: PROVIDERS }, // refresh GET
{ body: { ok: true, model: 'llama-3.3-70b', reply: 'ok', latency_ms: 412 } }, // test POST
);
render(<LLMProvidersPanel />);
fireEvent.click(await screen.findByTestId('llm-provider-test'));
await waitFor(() => expect(screen.getByText(/llama-3\.3-70b · 412 ms/)).toBeInTheDocument());
});
it('auth failure renders the actionable localized message, not the raw detail', async () => {
global.fetch = mockFetchSequence(
{ body: PROVIDERS },
{ body: {} },
{ body: PROVIDERS },
{
body: {
ok: false,
kind: 'auth',
detail: 'AuthenticationError: Incorrect API key',
latency_ms: 130,
},
},
);
render(<LLMProvidersPanel />);
fireEvent.click(await screen.findByTestId('llm-provider-test'));
await waitFor(() => expect(screen.getByText(/Key rejected \(401\/403\)/)).toBeInTheDocument());
expect(screen.queryByText(/AuthenticationError/)).toBeNull();
});
it('network failure explains reachability (local server hint)', async () => {
global.fetch = mockFetchSequence(
{ body: PROVIDERS },
{ body: {} },
{ body: PROVIDERS },
{ body: { ok: false, kind: 'network', detail: 'APIConnectionError: refused' } },
);
render(<LLMProvidersPanel />);
fireEvent.click(await screen.findByTestId('llm-provider-test'));
await waitFor(() => expect(screen.getByText(/Can't reach the provider/)).toBeInTheDocument());
});
it('fetch models fills the datalist for the model input', async () => {
global.fetch = mockFetchSequence(
{ body: PROVIDERS },
{ body: {} }, // save PUT (models saves non-key fields first)
{ body: PROVIDERS }, // refresh GET
{ body: { ok: true, models: ['llama-3.1-8b', 'llama-3.3-70b'] } }, // models GET
);
render(<LLMProvidersPanel />);
fireEvent.click(await screen.findByTestId('llm-provider-models'));
await waitFor(() => expect(screen.getByTestId('llm-provider-model')).toHaveAttribute('list'));
expect(document.querySelectorAll('datalist option')).toHaveLength(2);
});
it('local provider hides the API key row', async () => {
global.fetch = mockFetchSequence({ body: PROVIDERS });
render(<LLMProvidersPanel />);
const select = await screen.findByTestId('llm-provider-select');
fireEvent.change(select, { target: { value: 'ollama' } });
await waitFor(() => expect(screen.queryByTestId('llm-provider-key')).toBeNull());
});
});
+33
View File
@@ -26,6 +26,39 @@
"engines": "المحركات",
"capture": "الاختصارات",
"credentials": "بيانات الاعتماد",
"llm_providers": "مقدمو خدمات LLM",
"llmp_desc": "يشغّل ترجمة Cinematic وAutofit — يعيد نموذج اللغة (LLM) صياغة كل سطر ليناسب الوقت المتاح لمقطعه حتى يبقى توقيت الفيديو متوافقًا. تُخزَّن المفاتيح مشفّرة؛ ويعمل مقدمو الخدمة المحليون (Ollama/LM Studio) دون اتصال بالإنترنت تمامًا.",
"llmp_provider": "مقدم الخدمة",
"llmp_provider_hint": "اختر مقدم خدمة لإعداده. يُستخدم النشط لترجمة Cinematic/Autofit. لا يحتاج المقدمون المحليون إلى مفتاح، لكن يجب أن يكون خادمهم قيد التشغيل.",
"llmp_local_tag": "محلي",
"llmp_about": "حول",
"llmp_get_key": "الحصول على مفتاح API",
"llmp_account_id": "معرّف الحساب",
"llmp_account_placeholder": "معرّف حساب Cloudflare",
"llmp_api_key": "مفتاح API",
"llmp_key_env": "مضبوط عبر البيئة (.env) — يتجاوز هذا الحقل",
"llmp_key_stored": "محفوظ — اكتب للاستبدال",
"llmp_key_paste": "الصق مفتاح API الخاص بك",
"llmp_base_url": "عنوان URL الأساسي",
"llmp_model": "النموذج",
"llmp_model_placeholder": "اسم النموذج",
"llmp_fetch_models": "جلب النماذج",
"llmp_models_loaded": "عدد النماذج المتاحة من مقدم الخدمة هذا: {{count}}",
"llmp_status": "الحالة",
"llmp_save": "حفظ",
"llmp_save_active": "حفظ واستخدام للترجمة",
"llmp_save_keep": "حفظ مع إبقاء النشط",
"llmp_test": "اختبار",
"llmp_active_badge": "نشط",
"llmp_test_ok": "تم — {{model}} · {{ms}} ms",
"llmp_err_config": "مفتاح API أو عنوان URL الأساسي مفقود — أدخلهما ثم احفظ.",
"llmp_err_auth": "رُفض المفتاح (401/403) — تحقق من مفتاح API الخاص بك.",
"llmp_err_not_found": "غير موجود (404) — تحقق من اسم النموذج ومسار عنوان URL الأساسي.",
"llmp_err_rate_limit": "تم تجاوز حد الطلبات (429) — المفتاح يعمل؛ حاول مجددًا بعد قليل.",
"llmp_err_network": "تعذّر الوصول إلى مقدم الخدمة — تحقق من عنوان URL الأساسي، أو الشبكة، أو من أن الخادم المحلي قيد التشغيل.",
"llmp_err_error": "فشل الاختبار",
"llmp_load_failed": "فشل تحميل مقدمي الخدمة",
"llmp_save_failed": "فشل الحفظ",
"updates": "التحديثات",
"logs": "السجلات",
"about": "حول",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Engines",
"capture": "Tastatur",
"credentials": "Anmeldedaten",
"llm_providers": "LLM-Anbieter",
"llmp_desc": "Treibt die Cinematic- und Autofit-Übersetzung an — das LLM formuliert jede Zeile so um, dass sie ins Zeitbudget ihres Segments passt und das Video-Timing erhalten bleibt. Schlüssel werden verschlüsselt gespeichert; lokale Anbieter (Ollama/LM Studio) bleiben vollständig offline.",
"llmp_provider": "Anbieter",
"llmp_provider_hint": "Anbieter zum Konfigurieren auswählen. Der aktive wird für die Cinematic/Autofit-Übersetzung verwendet. Lokale Anbieter benötigen keinen Schlüssel, ihr Server muss jedoch laufen.",
"llmp_local_tag": "lokal",
"llmp_about": "Über",
"llmp_get_key": "API-Schlüssel anfordern",
"llmp_account_id": "Konto-ID",
"llmp_account_placeholder": "Cloudflare-Konto-ID",
"llmp_api_key": "API-Schlüssel",
"llmp_key_env": "über Umgebung (.env) gesetzt — überschreibt dieses Feld",
"llmp_key_stored": "gespeichert — zum Ersetzen eintippen",
"llmp_key_paste": "API-Schlüssel einfügen",
"llmp_base_url": "Basis-URL",
"llmp_model": "Modell",
"llmp_model_placeholder": "Modellname",
"llmp_fetch_models": "Modelle abrufen",
"llmp_models_loaded": "{{count}} Modelle bei diesem Anbieter verfügbar",
"llmp_status": "Status",
"llmp_save": "Speichern",
"llmp_save_active": "Speichern & für Übersetzung verwenden",
"llmp_save_keep": "Speichern & aktiven Anbieter behalten",
"llmp_test": "Testen",
"llmp_active_badge": "aktiv",
"llmp_test_ok": "OK — {{model}} · {{ms}} ms",
"llmp_err_config": "API-Schlüssel oder Basis-URL fehlt — ausfüllen und speichern.",
"llmp_err_auth": "Schlüssel abgelehnt (401/403) — API-Schlüssel prüfen.",
"llmp_err_not_found": "Nicht gefunden (404) — Modellname und Pfad der Basis-URL prüfen.",
"llmp_err_rate_limit": "Rate-Limit erreicht (429) — der Schlüssel funktioniert; gleich erneut versuchen.",
"llmp_err_network": "Anbieter nicht erreichbar — Basis-URL und Netzwerk prüfen bzw. sicherstellen, dass der lokale Server läuft.",
"llmp_err_error": "Test fehlgeschlagen",
"llmp_load_failed": "Anbieter konnten nicht geladen werden",
"llmp_save_failed": "Speichern fehlgeschlagen",
"updates": "Updates",
"logs": "Protokolle",
"about": "Über",
+33 -1
View File
@@ -321,6 +321,38 @@
"appearance": "Appearance",
"credentials": "Credentials",
"llm_providers": "LLM Providers",
"llmp_desc": "Powers Cinematic & Autofit translation — the LLM rewrites each line to fit its segment's time budget so the video timing holds. Keys are stored encrypted; local providers (Ollama/LM Studio) stay fully offline.",
"llmp_provider": "Provider",
"llmp_provider_hint": "Pick a provider to configure. The active one is used for Cinematic/Autofit translation. Local providers need no key but require their server to be running.",
"llmp_local_tag": "local",
"llmp_about": "About",
"llmp_get_key": "Get an API key",
"llmp_account_id": "Account ID",
"llmp_account_placeholder": "Cloudflare account id",
"llmp_api_key": "API key",
"llmp_key_env": "set via environment (.env) — overrides this field",
"llmp_key_stored": "stored — type to replace",
"llmp_key_paste": "paste your API key",
"llmp_base_url": "Base URL",
"llmp_model": "Model",
"llmp_model_placeholder": "model name",
"llmp_fetch_models": "Fetch models",
"llmp_models_loaded": "{{count}} models available from this provider",
"llmp_status": "Status",
"llmp_save": "Save",
"llmp_save_active": "Save & use for translation",
"llmp_save_keep": "Save & keep active",
"llmp_test": "Test",
"llmp_active_badge": "active",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "Missing API key or Base URL — fill them in and save.",
"llmp_err_auth": "Key rejected (401/403) — check your API key.",
"llmp_err_not_found": "Not found (404) — check the model name and Base URL path.",
"llmp_err_rate_limit": "Rate-limited (429) — the key works; try again in a moment.",
"llmp_err_network": "Can't reach the provider — check the Base URL, your network, or that the local server is running.",
"llmp_err_error": "Test failed",
"llmp_load_failed": "Failed to load providers",
"llmp_save_failed": "Failed to save",
"updates": "Updates",
"proxy": "Proxy",
"proxy_desc": "HTTP/SOCKS5 proxy for downloads (yt-dlp, HuggingFace). Supports http://, https://, socks5://. Restart required if changed after backend start.",
@@ -2092,4 +2124,4 @@
"loaded_raw": "Recording loaded (raw — denoising unavailable)",
"too_short": "Recording too short"
}
}
}
+33
View File
@@ -26,6 +26,39 @@
"engines": "Motores",
"capture": "Captura",
"credentials": "Credenciales",
"llm_providers": "Proveedores LLM",
"llmp_desc": "Impulsa la traducción Cinematic y Autofit: el LLM reescribe cada línea para ajustarse al tiempo disponible de su segmento y mantener la sincronización del vídeo. Las claves se guardan cifradas; los proveedores locales (Ollama/LM Studio) funcionan totalmente sin conexión.",
"llmp_provider": "Proveedor",
"llmp_provider_hint": "Elige un proveedor para configurarlo. El activo se usa para la traducción Cinematic/Autofit. Los proveedores locales no necesitan clave, pero su servidor debe estar en ejecución.",
"llmp_local_tag": "local",
"llmp_about": "Acerca de",
"llmp_get_key": "Obtener una clave API",
"llmp_account_id": "ID de cuenta",
"llmp_account_placeholder": "ID de cuenta de Cloudflare",
"llmp_api_key": "Clave API",
"llmp_key_env": "definida por entorno (.env) — anula este campo",
"llmp_key_stored": "guardada — escribe para reemplazarla",
"llmp_key_paste": "pega tu clave API",
"llmp_base_url": "URL base",
"llmp_model": "Modelo",
"llmp_model_placeholder": "nombre del modelo",
"llmp_fetch_models": "Obtener modelos",
"llmp_models_loaded": "{{count}} modelos disponibles de este proveedor",
"llmp_status": "Estado",
"llmp_save": "Guardar",
"llmp_save_active": "Guardar y usar para traducción",
"llmp_save_keep": "Guardar y mantener el activo",
"llmp_test": "Probar",
"llmp_active_badge": "activo",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "Falta la clave API o la URL base — rellénalas y guarda.",
"llmp_err_auth": "Clave rechazada (401/403) — comprueba tu clave API.",
"llmp_err_not_found": "No encontrado (404) — comprueba el nombre del modelo y la ruta de la URL base.",
"llmp_err_rate_limit": "Límite de peticiones (429) — la clave funciona; inténtalo de nuevo en un momento.",
"llmp_err_network": "No se puede conectar con el proveedor — comprueba la URL base, tu red o que el servidor local esté en ejecución.",
"llmp_err_error": "Error en la prueba",
"llmp_load_failed": "Error al cargar los proveedores",
"llmp_save_failed": "Error al guardar",
"updates": "Actualizaciones",
"logs": "Registros",
"about": "Acerca de",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Moteurs",
"capture": "Saisie",
"credentials": "Identifiants",
"llm_providers": "Fournisseurs LLM",
"llmp_desc": "Alimente la traduction Cinematic et Autofit — le LLM reformule chaque ligne pour respecter le budget temps de son segment afin de préserver le timing de la vidéo. Les clés sont stockées chiffrées ; les fournisseurs locaux (Ollama/LM Studio) restent entièrement hors ligne.",
"llmp_provider": "Fournisseur",
"llmp_provider_hint": "Choisissez un fournisseur à configurer. Le fournisseur actif est utilisé pour la traduction Cinematic/Autofit. Les fournisseurs locaux ne nécessitent aucune clé, mais leur serveur doit être en cours d'exécution.",
"llmp_local_tag": "local",
"llmp_about": "À propos",
"llmp_get_key": "Obtenir une clé API",
"llmp_account_id": "ID de compte",
"llmp_account_placeholder": "ID de compte Cloudflare",
"llmp_api_key": "Clé API",
"llmp_key_env": "définie via l'environnement (.env) — remplace ce champ",
"llmp_key_stored": "enregistrée — saisissez pour remplacer",
"llmp_key_paste": "collez votre clé API",
"llmp_base_url": "URL de base",
"llmp_model": "Modèle",
"llmp_model_placeholder": "nom du modèle",
"llmp_fetch_models": "Récupérer les modèles",
"llmp_models_loaded": "{{count}} modèles disponibles chez ce fournisseur",
"llmp_status": "Statut",
"llmp_save": "Enregistrer",
"llmp_save_active": "Enregistrer et utiliser pour la traduction",
"llmp_save_keep": "Enregistrer et conserver l'actif",
"llmp_test": "Tester",
"llmp_active_badge": "actif",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "Clé API ou URL de base manquante — renseignez-les puis enregistrez.",
"llmp_err_auth": "Clé rejetée (401/403) — vérifiez votre clé API.",
"llmp_err_not_found": "Introuvable (404) — vérifiez le nom du modèle et le chemin de l'URL de base.",
"llmp_err_rate_limit": "Limite de débit atteinte (429) — la clé fonctionne ; réessayez dans un instant.",
"llmp_err_network": "Impossible de joindre le fournisseur — vérifiez l'URL de base, votre réseau, ou que le serveur local est en cours d'exécution.",
"llmp_err_error": "Échec du test",
"llmp_load_failed": "Échec du chargement des fournisseurs",
"llmp_save_failed": "Échec de l'enregistrement",
"updates": "Mises à jour",
"logs": "Journaux",
"about": "À propos",
+33
View File
@@ -26,6 +26,39 @@
"engines": "इंजन",
"capture": "कैप्चर",
"credentials": "क्रेडेंशियल",
"llm_providers": "LLM प्रदाता",
"llmp_desc": "Cinematic और Autofit अनुवाद को संचालित करता है — LLM हर पंक्ति को इस तरह फिर से लिखता है कि वह अपने खंड के समय-बजट में फ़िट हो और वीडियो की टाइमिंग बनी रहे। कुंजियाँ एन्क्रिप्ट करके संग्रहीत की जाती हैं; स्थानीय प्रदाता (Ollama/LM Studio) पूरी तरह ऑफ़लाइन रहते हैं।",
"llmp_provider": "प्रदाता",
"llmp_provider_hint": "कॉन्फ़िगर करने के लिए एक प्रदाता चुनें। सक्रिय प्रदाता का उपयोग Cinematic/Autofit अनुवाद के लिए होता है। स्थानीय प्रदाताओं को कुंजी की ज़रूरत नहीं, लेकिन उनका सर्वर चालू होना चाहिए।",
"llmp_local_tag": "स्थानीय",
"llmp_about": "विवरण",
"llmp_get_key": "API कुंजी प्राप्त करें",
"llmp_account_id": "खाता ID",
"llmp_account_placeholder": "Cloudflare खाता ID",
"llmp_api_key": "API कुंजी",
"llmp_key_env": "एनवायरनमेंट (.env) से सेट है — यह इस फ़ील्ड को ओवरराइड करता है",
"llmp_key_stored": "संग्रहीत — बदलने के लिए टाइप करें",
"llmp_key_paste": "अपनी API कुंजी पेस्ट करें",
"llmp_base_url": "बेस URL",
"llmp_model": "मॉडल",
"llmp_model_placeholder": "मॉडल का नाम",
"llmp_fetch_models": "मॉडल प्राप्त करें",
"llmp_models_loaded": "इस प्रदाता से {{count}} मॉडल उपलब्ध हैं",
"llmp_status": "स्थिति",
"llmp_save": "सहेजें",
"llmp_save_active": "सहेजें और अनुवाद के लिए उपयोग करें",
"llmp_save_keep": "सहेजें और मौजूदा सक्रिय रखें",
"llmp_test": "परीक्षण करें",
"llmp_active_badge": "सक्रिय",
"llmp_test_ok": "ठीक — {{model}} · {{ms}} ms",
"llmp_err_config": "API कुंजी या बेस URL मौजूद नहीं — उन्हें भरें और सहेजें।",
"llmp_err_auth": "कुंजी अस्वीकृत (401/403) — अपनी API कुंजी जाँचें।",
"llmp_err_not_found": "नहीं मिला (404) — मॉडल का नाम और बेस URL पथ जाँचें।",
"llmp_err_rate_limit": "रेट-लिमिट (429) — कुंजी काम करती है; थोड़ी देर में पुनः प्रयास करें।",
"llmp_err_network": "प्रदाता तक नहीं पहुँचा जा सका — बेस URL, अपना नेटवर्क, या स्थानीय सर्वर चालू है या नहीं, जाँचें।",
"llmp_err_error": "परीक्षण विफल",
"llmp_load_failed": "प्रदाता लोड करने में विफल",
"llmp_save_failed": "सहेजना विफल",
"updates": "अपडेट",
"logs": "लॉग",
"about": "विवरण",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Mesin",
"capture": "Pintasan",
"credentials": "Kredensial",
"llm_providers": "Penyedia LLM",
"llmp_desc": "Menggerakkan terjemahan Cinematic & Autofit — LLM menulis ulang setiap baris agar sesuai dengan jatah waktu segmennya sehingga pengaturan waktu video tetap terjaga. Kunci disimpan terenkripsi; penyedia lokal (Ollama/LM Studio) sepenuhnya offline.",
"llmp_provider": "Penyedia",
"llmp_provider_hint": "Pilih penyedia untuk dikonfigurasi. Penyedia aktif digunakan untuk terjemahan Cinematic/Autofit. Penyedia lokal tidak memerlukan kunci, tetapi servernya harus berjalan.",
"llmp_local_tag": "lokal",
"llmp_about": "Tentang",
"llmp_get_key": "Dapatkan kunci API",
"llmp_account_id": "ID akun",
"llmp_account_placeholder": "ID akun Cloudflare",
"llmp_api_key": "Kunci API",
"llmp_key_env": "disetel lewat environment (.env) — menggantikan kolom ini",
"llmp_key_stored": "tersimpan — ketik untuk mengganti",
"llmp_key_paste": "tempel kunci API Anda",
"llmp_base_url": "URL dasar",
"llmp_model": "Model",
"llmp_model_placeholder": "nama model",
"llmp_fetch_models": "Ambil model",
"llmp_models_loaded": "{{count}} model tersedia dari penyedia ini",
"llmp_status": "Status",
"llmp_save": "Simpan",
"llmp_save_active": "Simpan & gunakan untuk terjemahan",
"llmp_save_keep": "Simpan & pertahankan yang aktif",
"llmp_test": "Uji",
"llmp_active_badge": "aktif",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "Kunci API atau URL dasar belum diisi — isi lalu simpan.",
"llmp_err_auth": "Kunci ditolak (401/403) — periksa kunci API Anda.",
"llmp_err_not_found": "Tidak ditemukan (404) — periksa nama model dan jalur URL dasar.",
"llmp_err_rate_limit": "Terkena batas laju (429) — kunci berfungsi; coba lagi sebentar lagi.",
"llmp_err_network": "Tidak dapat menghubungi penyedia — periksa URL dasar, jaringan Anda, atau pastikan server lokal sedang berjalan.",
"llmp_err_error": "Pengujian gagal",
"llmp_load_failed": "Gagal memuat penyedia",
"llmp_save_failed": "Gagal menyimpan",
"updates": "Pembaruan",
"logs": "Log",
"about": "Tentang",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Motori",
"capture": "Scorciatoie",
"credentials": "Credenziali",
"llm_providers": "Provider LLM",
"llmp_desc": "Alimenta la traduzione Cinematic e Autofit: il LLM riscrive ogni riga per rientrare nel budget di tempo del suo segmento, preservando il timing del video. Le chiavi sono salvate cifrate; i provider locali (Ollama/LM Studio) restano completamente offline.",
"llmp_provider": "Provider",
"llmp_provider_hint": "Scegli un provider da configurare. Quello attivo viene usato per la traduzione Cinematic/Autofit. I provider locali non richiedono chiavi, ma il loro server deve essere in esecuzione.",
"llmp_local_tag": "locale",
"llmp_about": "Informazioni",
"llmp_get_key": "Ottieni una chiave API",
"llmp_account_id": "ID account",
"llmp_account_placeholder": "ID account Cloudflare",
"llmp_api_key": "Chiave API",
"llmp_key_env": "impostata via ambiente (.env) — ha precedenza su questo campo",
"llmp_key_stored": "salvata — digita per sostituirla",
"llmp_key_paste": "incolla la tua chiave API",
"llmp_base_url": "URL di base",
"llmp_model": "Modello",
"llmp_model_placeholder": "nome del modello",
"llmp_fetch_models": "Recupera modelli",
"llmp_models_loaded": "{{count}} modelli disponibili da questo provider",
"llmp_status": "Stato",
"llmp_save": "Salva",
"llmp_save_active": "Salva e usa per la traduzione",
"llmp_save_keep": "Salva e mantieni l'attivo",
"llmp_test": "Testa",
"llmp_active_badge": "attivo",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "Chiave API o URL di base mancante — compilali e salva.",
"llmp_err_auth": "Chiave rifiutata (401/403) — controlla la tua chiave API.",
"llmp_err_not_found": "Non trovato (404) — controlla il nome del modello e il percorso dell'URL di base.",
"llmp_err_rate_limit": "Limite di richieste (429) — la chiave funziona; riprova tra un momento.",
"llmp_err_network": "Impossibile raggiungere il provider — controlla l'URL di base, la rete o che il server locale sia in esecuzione.",
"llmp_err_error": "Test non riuscito",
"llmp_load_failed": "Impossibile caricare i provider",
"llmp_save_failed": "Salvataggio non riuscito",
"updates": "Aggiornamenti",
"logs": "Registri",
"about": "Informazioni",
+33
View File
@@ -26,6 +26,39 @@
"engines": "エンジン",
"capture": "キャプチャ",
"credentials": "資格情報",
"llm_providers": "LLM プロバイダー",
"llmp_desc": "Cinematic・Autofit 翻訳を支える機能です。LLM が各行をセグメントの時間枠に収まるように書き換え、動画のタイミングを保ちます。キーは暗号化して保存され、ローカルプロバイダー(Ollama/LM Studio)は完全にオフラインで動作します。",
"llmp_provider": "プロバイダー",
"llmp_provider_hint": "設定するプロバイダーを選択してください。アクティブなプロバイダーが Cinematic/Autofit 翻訳に使用されます。ローカルプロバイダーはキー不要ですが、サーバーが起動している必要があります。",
"llmp_local_tag": "ローカル",
"llmp_about": "情報",
"llmp_get_key": "API キーを取得",
"llmp_account_id": "アカウント ID",
"llmp_account_placeholder": "Cloudflare アカウント ID",
"llmp_api_key": "API キー",
"llmp_key_env": "環境変数 (.env) で設定済み — この欄より優先されます",
"llmp_key_stored": "保存済み — 入力すると置き換えます",
"llmp_key_paste": "API キーを貼り付け",
"llmp_base_url": "ベース URL",
"llmp_model": "モデル",
"llmp_model_placeholder": "モデル名",
"llmp_fetch_models": "モデルを取得",
"llmp_models_loaded": "このプロバイダーで {{count}} 個のモデルが利用可能です",
"llmp_status": "ステータス",
"llmp_save": "保存",
"llmp_save_active": "保存して翻訳に使用",
"llmp_save_keep": "保存(アクティブは変更しない)",
"llmp_test": "テスト",
"llmp_active_badge": "アクティブ",
"llmp_test_ok": "OK — {{model}} · {{ms}} ms",
"llmp_err_config": "API キーまたはベース URL が未入力です — 入力して保存してください。",
"llmp_err_auth": "キーが拒否されました (401/403) — API キーを確認してください。",
"llmp_err_not_found": "見つかりません (404) — モデル名とベース URL のパスを確認してください。",
"llmp_err_rate_limit": "レート制限中 (429) — キーは有効です。しばらくしてから再試行してください。",
"llmp_err_network": "プロバイダーに接続できません — ベース URL、ネットワーク、またはローカルサーバーが起動しているか確認してください。",
"llmp_err_error": "テストに失敗しました",
"llmp_load_failed": "プロバイダーの読み込みに失敗しました",
"llmp_save_failed": "保存に失敗しました",
"updates": "アップデート",
"logs": "ログ",
"about": "情報",
+33
View File
@@ -26,6 +26,39 @@
"engines": "엔진",
"capture": "입력",
"credentials": "자격 증명",
"llm_providers": "LLM 제공업체",
"llmp_desc": "Cinematic 및 Autofit 번역을 구동합니다. LLM이 각 줄을 해당 구간의 시간 안에 맞게 다시 써서 영상 타이밍을 유지합니다. 키는 암호화되어 저장되며, 로컬 제공업체(Ollama/LM Studio)는 완전히 오프라인으로 작동합니다.",
"llmp_provider": "제공업체",
"llmp_provider_hint": "구성할 제공업체를 선택하세요. 활성 제공업체가 Cinematic/Autofit 번역에 사용됩니다. 로컬 제공업체는 키가 필요 없지만 서버가 실행 중이어야 합니다.",
"llmp_local_tag": "로컬",
"llmp_about": "정보",
"llmp_get_key": "API 키 발급받기",
"llmp_account_id": "계정 ID",
"llmp_account_placeholder": "Cloudflare 계정 ID",
"llmp_api_key": "API 키",
"llmp_key_env": "환경 변수(.env)로 설정됨 — 이 필드보다 우선합니다",
"llmp_key_stored": "저장됨 — 입력하면 교체됩니다",
"llmp_key_paste": "API 키를 붙여넣으세요",
"llmp_base_url": "기본 URL",
"llmp_model": "모델",
"llmp_model_placeholder": "모델 이름",
"llmp_fetch_models": "모델 가져오기",
"llmp_models_loaded": "이 제공업체에서 {{count}}개의 모델을 사용할 수 있습니다",
"llmp_status": "상태",
"llmp_save": "저장",
"llmp_save_active": "저장 후 번역에 사용",
"llmp_save_keep": "저장(활성 제공업체 유지)",
"llmp_test": "테스트",
"llmp_active_badge": "활성",
"llmp_test_ok": "정상 — {{model}} · {{ms}} ms",
"llmp_err_config": "API 키 또는 기본 URL이 없습니다 — 입력한 후 저장하세요.",
"llmp_err_auth": "키가 거부되었습니다(401/403) — API 키를 확인하세요.",
"llmp_err_not_found": "찾을 수 없습니다(404) — 모델 이름과 기본 URL 경로를 확인하세요.",
"llmp_err_rate_limit": "요청 한도 초과(429) — 키는 정상입니다. 잠시 후 다시 시도하세요.",
"llmp_err_network": "제공업체에 연결할 수 없습니다 — 기본 URL, 네트워크 또는 로컬 서버 실행 여부를 확인하세요.",
"llmp_err_error": "테스트 실패",
"llmp_load_failed": "제공업체를 불러오지 못했습니다",
"llmp_save_failed": "저장하지 못했습니다",
"updates": "업데이트",
"logs": "로그",
"about": "정보",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Motoren",
"capture": "Sneltoetsen",
"credentials": "Inloggegevens",
"llm_providers": "LLM-providers",
"llmp_desc": "Drijft de Cinematic- en Autofit-vertaling aan — de LLM herschrijft elke regel zodat die binnen het tijdsbudget van zijn segment past en de videotiming behouden blijft. Sleutels worden versleuteld opgeslagen; lokale providers (Ollama/LM Studio) blijven volledig offline.",
"llmp_provider": "Provider",
"llmp_provider_hint": "Kies een provider om te configureren. De actieve wordt gebruikt voor Cinematic/Autofit-vertaling. Lokale providers hebben geen sleutel nodig, maar hun server moet wel draaien.",
"llmp_local_tag": "lokaal",
"llmp_about": "Over",
"llmp_get_key": "API-sleutel aanvragen",
"llmp_account_id": "Account-ID",
"llmp_account_placeholder": "Cloudflare-account-ID",
"llmp_api_key": "API-sleutel",
"llmp_key_env": "ingesteld via omgeving (.env) — overschrijft dit veld",
"llmp_key_stored": "opgeslagen — typ om te vervangen",
"llmp_key_paste": "plak je API-sleutel",
"llmp_base_url": "Basis-URL",
"llmp_model": "Model",
"llmp_model_placeholder": "modelnaam",
"llmp_fetch_models": "Modellen ophalen",
"llmp_models_loaded": "{{count}} modellen beschikbaar bij deze provider",
"llmp_status": "Status",
"llmp_save": "Opslaan",
"llmp_save_active": "Opslaan en gebruiken voor vertaling",
"llmp_save_keep": "Opslaan en actieve provider behouden",
"llmp_test": "Testen",
"llmp_active_badge": "actief",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "API-sleutel of basis-URL ontbreekt — vul ze in en sla op.",
"llmp_err_auth": "Sleutel geweigerd (401/403) — controleer je API-sleutel.",
"llmp_err_not_found": "Niet gevonden (404) — controleer de modelnaam en het pad van de basis-URL.",
"llmp_err_rate_limit": "Limiet bereikt (429) — de sleutel werkt; probeer het zo opnieuw.",
"llmp_err_network": "Kan de provider niet bereiken — controleer de basis-URL, je netwerk, of dat de lokale server draait.",
"llmp_err_error": "Test mislukt",
"llmp_load_failed": "Kan providers niet laden",
"llmp_save_failed": "Opslaan mislukt",
"updates": "Updates",
"logs": "Logboeken",
"about": "Over",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Silniki",
"capture": "Skróty klawiszowe",
"credentials": "Dane uwierzytelniające",
"llm_providers": "Dostawcy LLM",
"llmp_desc": "Zasila tłumaczenie Cinematic i Autofit — LLM przeredagowuje każdą linię tak, aby zmieściła się w budżecie czasowym swojego segmentu i synchronizacja wideo została zachowana. Klucze są przechowywane w postaci zaszyfrowanej; lokalni dostawcy (Ollama/LM Studio) działają całkowicie offline.",
"llmp_provider": "Dostawca",
"llmp_provider_hint": "Wybierz dostawcę do skonfigurowania. Aktywny jest używany do tłumaczenia Cinematic/Autofit. Lokalni dostawcy nie wymagają klucza, ale ich serwer musi być uruchomiony.",
"llmp_local_tag": "lokalny",
"llmp_about": "Informacje",
"llmp_get_key": "Uzyskaj klucz API",
"llmp_account_id": "ID konta",
"llmp_account_placeholder": "ID konta Cloudflare",
"llmp_api_key": "Klucz API",
"llmp_key_env": "ustawiony przez środowisko (.env) — nadpisuje to pole",
"llmp_key_stored": "zapisany — wpisz, aby zastąpić",
"llmp_key_paste": "wklej swój klucz API",
"llmp_base_url": "Bazowy adres URL",
"llmp_model": "Model",
"llmp_model_placeholder": "nazwa modelu",
"llmp_fetch_models": "Pobierz modele",
"llmp_models_loaded": "Dostępne modele u tego dostawcy: {{count}}",
"llmp_status": "Status",
"llmp_save": "Zapisz",
"llmp_save_active": "Zapisz i używaj do tłumaczenia",
"llmp_save_keep": "Zapisz i zachowaj aktywnego",
"llmp_test": "Testuj",
"llmp_active_badge": "aktywny",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "Brak klucza API lub bazowego adresu URL — uzupełnij je i zapisz.",
"llmp_err_auth": "Klucz odrzucony (401/403) — sprawdź swój klucz API.",
"llmp_err_not_found": "Nie znaleziono (404) — sprawdź nazwę modelu i ścieżkę bazowego adresu URL.",
"llmp_err_rate_limit": "Limit zapytań (429) — klucz działa; spróbuj ponownie za chwilę.",
"llmp_err_network": "Nie można połączyć się z dostawcą — sprawdź bazowy adres URL, sieć lub czy lokalny serwer jest uruchomiony.",
"llmp_err_error": "Test nie powiódł się",
"llmp_load_failed": "Nie udało się załadować dostawców",
"llmp_save_failed": "Nie udało się zapisać",
"updates": "Aktualizacje",
"logs": "Logi",
"about": "O programie",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Motores",
"capture": "Captura",
"credentials": "Credenciais",
"llm_providers": "Provedores de LLM",
"llmp_desc": "Alimenta a tradução Cinematic e Autofit — o LLM reescreve cada linha para caber no tempo do seu segmento, mantendo a sincronização do vídeo. As chaves são armazenadas criptografadas; provedores locais (Ollama/LM Studio) permanecem totalmente offline.",
"llmp_provider": "Provedor",
"llmp_provider_hint": "Escolha um provedor para configurar. O ativo é usado na tradução Cinematic/Autofit. Provedores locais não precisam de chave, mas o servidor deles precisa estar em execução.",
"llmp_local_tag": "local",
"llmp_about": "Sobre",
"llmp_get_key": "Obter uma chave de API",
"llmp_account_id": "ID da conta",
"llmp_account_placeholder": "ID da conta Cloudflare",
"llmp_api_key": "Chave de API",
"llmp_key_env": "definida via ambiente (.env) — substitui este campo",
"llmp_key_stored": "armazenada — digite para substituir",
"llmp_key_paste": "cole sua chave de API",
"llmp_base_url": "URL base",
"llmp_model": "Modelo",
"llmp_model_placeholder": "nome do modelo",
"llmp_fetch_models": "Buscar modelos",
"llmp_models_loaded": "{{count}} modelos disponíveis neste provedor",
"llmp_status": "Status",
"llmp_save": "Salvar",
"llmp_save_active": "Salvar e usar na tradução",
"llmp_save_keep": "Salvar e manter o ativo",
"llmp_test": "Testar",
"llmp_active_badge": "ativo",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "Falta a chave de API ou a URL base — preencha e salve.",
"llmp_err_auth": "Chave rejeitada (401/403) — verifique sua chave de API.",
"llmp_err_not_found": "Não encontrado (404) — verifique o nome do modelo e o caminho da URL base.",
"llmp_err_rate_limit": "Limite de requisições (429) — a chave funciona; tente novamente em instantes.",
"llmp_err_network": "Não foi possível acessar o provedor — verifique a URL base, sua rede ou se o servidor local está em execução.",
"llmp_err_error": "Falha no teste",
"llmp_load_failed": "Falha ao carregar provedores",
"llmp_save_failed": "Falha ao salvar",
"updates": "Atualizações",
"logs": "Registros",
"about": "Sobre",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Движки",
"capture": "Захват",
"credentials": "Ключи",
"llm_providers": "Поставщики LLM",
"llmp_desc": "Обеспечивает перевод Cinematic и Autofit — LLM переписывает каждую строку так, чтобы она укладывалась в отведённое время своего сегмента и синхронизация видео сохранялась. Ключи хранятся в зашифрованном виде; локальные поставщики (Ollama/LM Studio) работают полностью офлайн.",
"llmp_provider": "Поставщик",
"llmp_provider_hint": "Выберите поставщика для настройки. Активный используется для перевода Cinematic/Autofit. Локальным поставщикам ключ не нужен, но их сервер должен быть запущен.",
"llmp_local_tag": "локальный",
"llmp_about": "Сведения",
"llmp_get_key": "Получить ключ API",
"llmp_account_id": "ID аккаунта",
"llmp_account_placeholder": "ID аккаунта Cloudflare",
"llmp_api_key": "Ключ API",
"llmp_key_env": "задан через окружение (.env) — имеет приоритет над этим полем",
"llmp_key_stored": "сохранён — введите, чтобы заменить",
"llmp_key_paste": "вставьте ваш ключ API",
"llmp_base_url": "Базовый URL",
"llmp_model": "Модель",
"llmp_model_placeholder": "название модели",
"llmp_fetch_models": "Получить список моделей",
"llmp_models_loaded": "Доступно моделей у этого поставщика: {{count}}",
"llmp_status": "Статус",
"llmp_save": "Сохранить",
"llmp_save_active": "Сохранить и использовать для перевода",
"llmp_save_keep": "Сохранить, не меняя активного",
"llmp_test": "Проверить",
"llmp_active_badge": "активен",
"llmp_test_ok": "ок — {{model}} · {{ms}} мс",
"llmp_err_config": "Не указан ключ API или базовый URL — заполните их и сохраните.",
"llmp_err_auth": "Ключ отклонён (401/403) — проверьте ключ API.",
"llmp_err_not_found": "Не найдено (404) — проверьте название модели и путь базового URL.",
"llmp_err_rate_limit": "Превышен лимит запросов (429) — ключ работает; повторите попытку чуть позже.",
"llmp_err_network": "Не удаётся связаться с поставщиком — проверьте базовый URL, сеть или что локальный сервер запущен.",
"llmp_err_error": "Проверка не удалась",
"llmp_load_failed": "Не удалось загрузить поставщиков",
"llmp_save_failed": "Не удалось сохранить",
"updates": "Обновления",
"logs": "Логи",
"about": "О программе",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Motorer",
"capture": "Genvägar",
"credentials": "Autentisering",
"llm_providers": "LLM-leverantörer",
"llmp_desc": "Driver Cinematic- och Autofit-översättning — LLM:en skriver om varje rad så att den ryms inom segmentets tidsbudget och videons timing hålls. Nycklar lagras krypterat; lokala leverantörer (Ollama/LM Studio) förblir helt offline.",
"llmp_provider": "Leverantör",
"llmp_provider_hint": "Välj en leverantör att konfigurera. Den aktiva används för Cinematic/Autofit-översättning. Lokala leverantörer behöver ingen nyckel, men deras server måste vara igång.",
"llmp_local_tag": "lokal",
"llmp_about": "Om",
"llmp_get_key": "Skaffa en API-nyckel",
"llmp_account_id": "Konto-ID",
"llmp_account_placeholder": "Cloudflare-konto-ID",
"llmp_api_key": "API-nyckel",
"llmp_key_env": "satt via miljön (.env) — åsidosätter det här fältet",
"llmp_key_stored": "lagrad — skriv för att ersätta",
"llmp_key_paste": "klistra in din API-nyckel",
"llmp_base_url": "Bas-URL",
"llmp_model": "Modell",
"llmp_model_placeholder": "modellnamn",
"llmp_fetch_models": "Hämta modeller",
"llmp_models_loaded": "{{count}} modeller tillgängliga från denna leverantör",
"llmp_status": "Status",
"llmp_save": "Spara",
"llmp_save_active": "Spara och använd för översättning",
"llmp_save_keep": "Spara och behåll den aktiva",
"llmp_test": "Testa",
"llmp_active_badge": "aktiv",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "API-nyckel eller bas-URL saknas — fyll i dem och spara.",
"llmp_err_auth": "Nyckeln avvisades (401/403) — kontrollera din API-nyckel.",
"llmp_err_not_found": "Hittades inte (404) — kontrollera modellnamnet och bas-URL:ens sökväg.",
"llmp_err_rate_limit": "Hastighetsbegränsad (429) — nyckeln fungerar; försök igen om en stund.",
"llmp_err_network": "Kan inte nå leverantören — kontrollera bas-URL:en, ditt nätverk eller att den lokala servern är igång.",
"llmp_err_error": "Testet misslyckades",
"llmp_load_failed": "Det gick inte att ladda leverantörer",
"llmp_save_failed": "Det gick inte att spara",
"updates": "Uppdateringar",
"logs": "Loggar",
"about": "Om",
+33
View File
@@ -26,6 +26,39 @@
"engines": "เอนจิน",
"capture": "แป้นพิมพ์",
"credentials": "สิทธิ์การใช้งาน",
"llm_providers": "ผู้ให้บริการ LLM",
"llmp_desc": "ขับเคลื่อนการแปลแบบ Cinematic และ Autofit — LLM จะเขียนแต่ละบรรทัดใหม่ให้พอดีกับเวลาของช่วงนั้น เพื่อคงจังหวะเวลาของวิดีโอไว้ คีย์ถูกเก็บแบบเข้ารหัส ผู้ให้บริการภายในเครื่อง (Ollama/LM Studio) ทำงานแบบออฟไลน์ทั้งหมด",
"llmp_provider": "ผู้ให้บริการ",
"llmp_provider_hint": "เลือกผู้ให้บริการที่จะตั้งค่า ผู้ให้บริการที่ใช้งานอยู่จะถูกใช้สำหรับการแปล Cinematic/Autofit ผู้ให้บริการภายในเครื่องไม่ต้องใช้คีย์ แต่เซิร์ฟเวอร์ต้องกำลังทำงานอยู่",
"llmp_local_tag": "ภายในเครื่อง",
"llmp_about": "เกี่ยวกับ",
"llmp_get_key": "รับคีย์ API",
"llmp_account_id": "ID บัญชี",
"llmp_account_placeholder": "ID บัญชี Cloudflare",
"llmp_api_key": "คีย์ API",
"llmp_key_env": "ตั้งค่าผ่านสภาพแวดล้อม (.env) — จะแทนที่ช่องนี้",
"llmp_key_stored": "บันทึกแล้ว — พิมพ์เพื่อแทนที่",
"llmp_key_paste": "วางคีย์ API ของคุณ",
"llmp_base_url": "Base URL",
"llmp_model": "โมเดล",
"llmp_model_placeholder": "ชื่อโมเดล",
"llmp_fetch_models": "ดึงรายชื่อโมเดล",
"llmp_models_loaded": "มีโมเดลให้ใช้ {{count}} รายการจากผู้ให้บริการนี้",
"llmp_status": "สถานะ",
"llmp_save": "บันทึก",
"llmp_save_active": "บันทึกและใช้สำหรับการแปล",
"llmp_save_keep": "บันทึกโดยคงตัวที่ใช้งานอยู่",
"llmp_test": "ทดสอบ",
"llmp_active_badge": "ใช้งานอยู่",
"llmp_test_ok": "โอเค — {{model}} · {{ms}} ms",
"llmp_err_config": "ไม่มีคีย์ API หรือ Base URL — กรอกข้อมูลแล้วบันทึก",
"llmp_err_auth": "คีย์ถูกปฏิเสธ (401/403) — ตรวจสอบคีย์ API ของคุณ",
"llmp_err_not_found": "ไม่พบ (404) — ตรวจสอบชื่อโมเดลและพาธของ Base URL",
"llmp_err_rate_limit": "ถูกจำกัดอัตรา (429) — คีย์ใช้งานได้ ลองอีกครั้งในอีกสักครู่",
"llmp_err_network": "ติดต่อผู้ให้บริการไม่ได้ — ตรวจสอบ Base URL เครือข่ายของคุณ หรือว่าเซิร์ฟเวอร์ภายในเครื่องกำลังทำงานอยู่",
"llmp_err_error": "การทดสอบล้มเหลว",
"llmp_load_failed": "ไม่สามารถโหลดผู้ให้บริการได้",
"llmp_save_failed": "บันทึกล้มเหลว",
"updates": "อัปเดต",
"logs": "บันทึกระบบ",
"about": "เกี่ยวกับ",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Motorlar",
"capture": "Kısayollar",
"credentials": "Kimlik Bilgileri",
"llm_providers": "LLM sağlayıcıları",
"llmp_desc": "Cinematic ve Autofit çevirisini çalıştırır — LLM her satırı, bölümünün süre bütçesine sığacak şekilde yeniden yazar; böylece videonun zamanlaması korunur. Anahtarlar şifrelenmiş olarak saklanır; yerel sağlayıcılar (Ollama/LM Studio) tamamen çevrimdışı kalır.",
"llmp_provider": "Sağlayıcı",
"llmp_provider_hint": "Yapılandırmak için bir sağlayıcı seçin. Etkin olan, Cinematic/Autofit çevirisi için kullanılır. Yerel sağlayıcılar anahtar gerektirmez ama sunucularının çalışıyor olması gerekir.",
"llmp_local_tag": "yerel",
"llmp_about": "Hakkında",
"llmp_get_key": "API anahtarı alın",
"llmp_account_id": "Hesap kimliği",
"llmp_account_placeholder": "Cloudflare hesap kimliği",
"llmp_api_key": "API anahtarı",
"llmp_key_env": "ortam değişkeniyle (.env) ayarlandı — bu alanı geçersiz kılar",
"llmp_key_stored": "kayıtlı — değiştirmek için yazın",
"llmp_key_paste": "API anahtarınızı yapıştırın",
"llmp_base_url": "Temel URL",
"llmp_model": "Model",
"llmp_model_placeholder": "model adı",
"llmp_fetch_models": "Modelleri getir",
"llmp_models_loaded": "Bu sağlayıcıda {{count}} model mevcut",
"llmp_status": "Durum",
"llmp_save": "Kaydet",
"llmp_save_active": "Kaydet ve çeviri için kullan",
"llmp_save_keep": "Kaydet, etkin olanı koru",
"llmp_test": "Test et",
"llmp_active_badge": "etkin",
"llmp_test_ok": "tamam — {{model}} · {{ms}} ms",
"llmp_err_config": "API anahtarı veya Temel URL eksik — doldurup kaydedin.",
"llmp_err_auth": "Anahtar reddedildi (401/403) — API anahtarınızı kontrol edin.",
"llmp_err_not_found": "Bulunamadı (404) — model adını ve Temel URL yolunu kontrol edin.",
"llmp_err_rate_limit": "Hız sınırına takıldı (429) — anahtar çalışıyor; birazdan yeniden deneyin.",
"llmp_err_network": "Sağlayıcıya ulaşılamıyor — Temel URL'yi, ağınızı veya yerel sunucunun çalışıp çalışmadığını kontrol edin.",
"llmp_err_error": "Test başarısız oldu",
"llmp_load_failed": "Sağlayıcılar yüklenemedi",
"llmp_save_failed": "Kaydedilemedi",
"updates": "Güncellemeler",
"logs": "Günlükler",
"about": "Hakkında",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Движки",
"capture": "Введення",
"credentials": "Ключі",
"llm_providers": "Постачальники LLM",
"llmp_desc": "Забезпечує переклад Cinematic та Autofit — LLM переписує кожен рядок так, щоб він вкладався в часовий бюджет свого сегмента й хронометраж відео зберігався. Ключі зберігаються в зашифрованому вигляді; локальні постачальники (Ollama/LM Studio) працюють повністю офлайн.",
"llmp_provider": "Постачальник",
"llmp_provider_hint": "Виберіть постачальника для налаштування. Активний використовується для перекладу Cinematic/Autofit. Локальним постачальникам ключ не потрібен, але їхній сервер має бути запущений.",
"llmp_local_tag": "локальний",
"llmp_about": "Відомості",
"llmp_get_key": "Отримати ключ API",
"llmp_account_id": "ID облікового запису",
"llmp_account_placeholder": "ID облікового запису Cloudflare",
"llmp_api_key": "Ключ API",
"llmp_key_env": "задано через середовище (.env) — має пріоритет над цим полем",
"llmp_key_stored": "збережено — введіть, щоб замінити",
"llmp_key_paste": "вставте свій ключ API",
"llmp_base_url": "Базовий URL",
"llmp_model": "Модель",
"llmp_model_placeholder": "назва моделі",
"llmp_fetch_models": "Отримати список моделей",
"llmp_models_loaded": "Доступно моделей у цього постачальника: {{count}}",
"llmp_status": "Статус",
"llmp_save": "Зберегти",
"llmp_save_active": "Зберегти й використовувати для перекладу",
"llmp_save_keep": "Зберегти, не змінюючи активного",
"llmp_test": "Перевірити",
"llmp_active_badge": "активний",
"llmp_test_ok": "ок — {{model}} · {{ms}} мс",
"llmp_err_config": "Не вказано ключ API або базовий URL — заповніть їх і збережіть.",
"llmp_err_auth": "Ключ відхилено (401/403) — перевірте свій ключ API.",
"llmp_err_not_found": "Не знайдено (404) — перевірте назву моделі та шлях базового URL.",
"llmp_err_rate_limit": "Перевищено ліміт запитів (429) — ключ працює; спробуйте ще раз за мить.",
"llmp_err_network": "Не вдається з'єднатися з постачальником — перевірте базовий URL, мережу або чи запущено локальний сервер.",
"llmp_err_error": "Перевірка не вдалася",
"llmp_load_failed": "Не вдалося завантажити постачальників",
"llmp_save_failed": "Не вдалося зберегти",
"updates": "Оновлення",
"logs": "Логи",
"about": "Про програму",
+33
View File
@@ -26,6 +26,39 @@
"engines": "Công cụ",
"capture": "Phím tắt",
"credentials": "Xác thực",
"llm_providers": "Nhà cung cấp LLM",
"llmp_desc": "Cung cấp năng lực cho bản dịch Cinematic và Autofit — LLM viết lại từng dòng cho vừa quỹ thời gian của phân đoạn để giữ đúng nhịp thời gian của video. Khóa được lưu ở dạng mã hóa; nhà cung cấp cục bộ (Ollama/LM Studio) hoạt động hoàn toàn ngoại tuyến.",
"llmp_provider": "Nhà cung cấp",
"llmp_provider_hint": "Chọn nhà cung cấp để cấu hình. Nhà cung cấp đang hoạt động sẽ được dùng cho bản dịch Cinematic/Autofit. Nhà cung cấp cục bộ không cần khóa nhưng máy chủ của chúng phải đang chạy.",
"llmp_local_tag": "cục bộ",
"llmp_about": "Giới thiệu",
"llmp_get_key": "Lấy khóa API",
"llmp_account_id": "ID tài khoản",
"llmp_account_placeholder": "ID tài khoản Cloudflare",
"llmp_api_key": "Khóa API",
"llmp_key_env": "đặt qua biến môi trường (.env) — ghi đè trường này",
"llmp_key_stored": "đã lưu — gõ để thay thế",
"llmp_key_paste": "dán khóa API của bạn",
"llmp_base_url": "URL cơ sở",
"llmp_model": "Mô hình",
"llmp_model_placeholder": "tên mô hình",
"llmp_fetch_models": "Lấy danh sách mô hình",
"llmp_models_loaded": "{{count}} mô hình khả dụng từ nhà cung cấp này",
"llmp_status": "Trạng thái",
"llmp_save": "Lưu",
"llmp_save_active": "Lưu và dùng cho bản dịch",
"llmp_save_keep": "Lưu và giữ nhà cung cấp đang hoạt động",
"llmp_test": "Kiểm tra",
"llmp_active_badge": "đang hoạt động",
"llmp_test_ok": "ok — {{model}} · {{ms}} ms",
"llmp_err_config": "Thiếu khóa API hoặc URL cơ sở — hãy điền và lưu.",
"llmp_err_auth": "Khóa bị từ chối (401/403) — kiểm tra khóa API của bạn.",
"llmp_err_not_found": "Không tìm thấy (404) — kiểm tra tên mô hình và đường dẫn của URL cơ sở.",
"llmp_err_rate_limit": "Bị giới hạn tốc độ (429) — khóa vẫn hoạt động; hãy thử lại sau giây lát.",
"llmp_err_network": "Không thể kết nối tới nhà cung cấp — kiểm tra URL cơ sở, mạng của bạn, hoặc máy chủ cục bộ có đang chạy không.",
"llmp_err_error": "Kiểm tra thất bại",
"llmp_load_failed": "Không tải được danh sách nhà cung cấp",
"llmp_save_failed": "Lưu không thành công",
"updates": "Cập nhật",
"logs": "Nhật ký",
"about": "Giới thiệu",
+33
View File
@@ -251,6 +251,39 @@
"sharing": "分享",
"appearance": "外观",
"credentials": "凭证",
"llm_providers": "LLM 提供商",
"llmp_desc": "为 Cinematic 与 Autofit 翻译提供支持:LLM 会改写每一行,使其符合所在片段的时间预算,从而保持视频时序。密钥加密存储;本地提供商(Ollama/LM Studio)完全离线运行。",
"llmp_provider": "提供商",
"llmp_provider_hint": "选择要配置的提供商。当前启用的提供商将用于 Cinematic/Autofit 翻译。本地提供商无需密钥,但其服务器必须处于运行状态。",
"llmp_local_tag": "本地",
"llmp_about": "关于",
"llmp_get_key": "获取 API 密钥",
"llmp_account_id": "账户 ID",
"llmp_account_placeholder": "Cloudflare 账户 ID",
"llmp_api_key": "API 密钥",
"llmp_key_env": "已通过环境变量 (.env) 设置,将覆盖此字段",
"llmp_key_stored": "已保存,输入即可替换",
"llmp_key_paste": "粘贴你的 API 密钥",
"llmp_base_url": "基础 URL",
"llmp_model": "模型",
"llmp_model_placeholder": "模型名称",
"llmp_fetch_models": "获取模型",
"llmp_models_loaded": "该提供商有 {{count}} 个可用模型",
"llmp_status": "状态",
"llmp_save": "保存",
"llmp_save_active": "保存并用于翻译",
"llmp_save_keep": "保存但保持当前启用项",
"llmp_test": "测试",
"llmp_active_badge": "已启用",
"llmp_test_ok": "正常:{{model}} · {{ms}} ms",
"llmp_err_config": "缺少 API 密钥或基础 URL,请填写后保存。",
"llmp_err_auth": "密钥被拒绝(401/403),请检查你的 API 密钥。",
"llmp_err_not_found": "未找到(404),请检查模型名称和基础 URL 路径。",
"llmp_err_rate_limit": "已被限流(429),密钥有效,请稍后重试。",
"llmp_err_network": "无法连接到提供商,请检查基础 URL、网络,或确认本地服务器正在运行。",
"llmp_err_error": "测试失败",
"llmp_load_failed": "无法加载提供商",
"llmp_save_failed": "保存失败",
"updates": "更新",
"proxy": "代理",
"proxy_desc": "HTTP/SOCKS5 下载代理(yt-dlp、HuggingFace)。支持 http://、https://、socks5://。后端启动后更改需重启。",
+33
View File
@@ -26,6 +26,39 @@
"engines": "配音引擎",
"capture": "鍵盤設定",
"credentials": "金鑰憑證",
"llm_providers": "LLM 提供者",
"llmp_desc": "為 Cinematic 與 Autofit 翻譯提供支援:LLM 會改寫每一行,使其符合所在片段的時間預算,以維持影片時序。金鑰以加密方式儲存;本機提供者(Ollama/LM Studio)完全離線運作。",
"llmp_provider": "提供者",
"llmp_provider_hint": "選擇要設定的提供者。目前啟用的提供者將用於 Cinematic/Autofit 翻譯。本機提供者無需金鑰,但其伺服器必須處於執行狀態。",
"llmp_local_tag": "本機",
"llmp_about": "關於",
"llmp_get_key": "取得 API 金鑰",
"llmp_account_id": "帳戶 ID",
"llmp_account_placeholder": "Cloudflare 帳戶 ID",
"llmp_api_key": "API 金鑰",
"llmp_key_env": "已透過環境變數 (.env) 設定,將覆寫此欄位",
"llmp_key_stored": "已儲存,輸入即可取代",
"llmp_key_paste": "貼上你的 API 金鑰",
"llmp_base_url": "基礎 URL",
"llmp_model": "模型",
"llmp_model_placeholder": "模型名稱",
"llmp_fetch_models": "取得模型",
"llmp_models_loaded": "此提供者有 {{count}} 個可用模型",
"llmp_status": "狀態",
"llmp_save": "儲存",
"llmp_save_active": "儲存並用於翻譯",
"llmp_save_keep": "儲存但維持目前啟用項",
"llmp_test": "測試",
"llmp_active_badge": "已啟用",
"llmp_test_ok": "正常:{{model}} · {{ms}} ms",
"llmp_err_config": "缺少 API 金鑰或基礎 URL,請填寫後儲存。",
"llmp_err_auth": "金鑰遭拒(401/403),請檢查你的 API 金鑰。",
"llmp_err_not_found": "找不到(404),請檢查模型名稱與基礎 URL 路徑。",
"llmp_err_rate_limit": "已達速率限制(429),金鑰有效,請稍後再試。",
"llmp_err_network": "無法連線至提供者,請檢查基礎 URL、網路,或確認本機伺服器正在執行。",
"llmp_err_error": "測試失敗",
"llmp_load_failed": "無法載入提供者",
"llmp_save_failed": "儲存失敗",
"updates": "更新",
"logs": "系統日誌",
"about": "關於軟體",
+1
View File
@@ -24,6 +24,7 @@ GET /api/settings/hf-token/state
GET /api/settings/license/{engine_id}
GET /api/settings/llm-endpoint
GET /api/settings/llm-providers
GET /api/settings/llm-providers/{provider_id}/models
GET /api/settings/perf/torch-compile-disabled
GET /api/settings/storage/models-dir
GET /archetypes
+162
View File
@@ -0,0 +1,162 @@
"""Router surface for /api/settings/llm-providers (v0.3.9 testing pass).
`tests/test_llm_providers.py` covers the registry service; these cover the
router handlers the UI calls the /test probe's error classification
(kind: config/auth/not_found/rate_limit/network/error + latency_ms) and the
/models discovery endpoint, with the OpenAI client faked at the SDK boundary
(no network) and settings_store backed by in-memory dicts (house convention,
same as test_llm_providers.py direct handler calls, no TestClient, so the
loopback auth guard isn't in play).
"""
from __future__ import annotations
import os
import sys
import types
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "backend"))
os.environ.setdefault("OMNIVOICE_MODEL", "test")
os.environ.setdefault("OMNIVOICE_DISABLE_FILE_LOG", "1")
_HAS_OPENAI = __import__("importlib").util.find_spec("openai") is not None
pytestmark = pytest.mark.skipif(not _HAS_OPENAI, reason="openai package not installed")
@pytest.fixture
def settings_mod(monkeypatch):
"""Router module with settings_store in-memory (no SQLite, no prefs I/O)."""
from services import settings_store as ss
text: dict[str, str] = {}
secrets: dict[str, str] = {}
monkeypatch.setattr(ss, "get_text", lambda k, default=None: text.get(k, default))
monkeypatch.setattr(ss, "set_text", lambda k, v: text.__setitem__(k, v))
monkeypatch.setattr(ss, "get_secret", lambda n: secrets.get(n))
monkeypatch.setattr(ss, "set_secret", lambda n, v: secrets.__setitem__(n, v) if v else secrets.pop(n, None))
monkeypatch.setattr(ss, "list_secret_names", lambda: list(secrets))
for var in ("LLM_DEFAULT_PROVIDER", "TRANSLATE_BASE_URL", "TRANSLATE_API_KEY",
"TRANSLATE_MODEL", "OPENAI_API_KEY", "GROQ_API_KEY", "GROQ_MODEL"):
monkeypatch.delenv(var, raising=False)
import importlib
return importlib.import_module("api.routers.settings")
def _fake_openai(monkeypatch, *, reply="ok", models=None, raise_exc=None):
"""Fake `openai.OpenAI` with canned chat/models behavior."""
class _Msg:
def __init__(self, content):
self.message = types.SimpleNamespace(content=content)
class _FakeClient:
def __init__(self, api_key=None, base_url=None):
self.chat = types.SimpleNamespace(
completions=types.SimpleNamespace(create=self._create))
self.models = types.SimpleNamespace(list=self._models)
def _create(self, **kw):
if raise_exc is not None:
raise raise_exc
return types.SimpleNamespace(choices=[_Msg(reply)])
def _models(self, **kw):
if raise_exc is not None:
raise raise_exc
return [types.SimpleNamespace(id=m) for m in (models or [])]
import openai
monkeypatch.setattr(openai, "OpenAI", _FakeClient)
def _configure_groq(settings_mod, key="gsk-test-123"):
settings_mod.save_llm_provider(
"groq", settings_mod._LLMProviderBody(api_key=key, make_active=True))
# ── list / save ─────────────────────────────────────────────────────────────
def test_list_never_leaks_keys(settings_mod):
_configure_groq(settings_mod)
body = settings_mod.list_llm_providers()
assert body["active"] == "groq"
groq = next(p for p in body["providers"] if p["id"] == "groq")
assert groq["has_key"] is True and groq["configured"] is True
assert "gsk-test-123" not in str(body) # the key never round-trips
def test_unknown_provider_404s(settings_mod):
from fastapi import HTTPException
with pytest.raises(HTTPException):
settings_mod.test_llm_provider("nope")
with pytest.raises(HTTPException):
settings_mod.list_llm_provider_models("nope")
# ── /test probe ─────────────────────────────────────────────────────────────
def test_probe_ok_includes_latency(settings_mod, monkeypatch):
_configure_groq(settings_mod)
_fake_openai(monkeypatch, reply="ok")
body = settings_mod.test_llm_provider("groq")
assert body["ok"] is True and body["reply"] == "ok"
assert isinstance(body["latency_ms"], int) and body["latency_ms"] >= 0
def test_probe_unconfigured_is_kind_config(settings_mod):
# openai: no key stored, env cleared → config guidance, no network attempt
body = settings_mod.test_llm_provider("openai")
assert body["ok"] is False and body["kind"] == "config"
@pytest.mark.parametrize("exc_name,status,expected_kind", [
("AuthenticationError", 401, "auth"),
("NotFoundError", 404, "not_found"),
("RateLimitError", 429, "rate_limit"),
("APIConnectionError", None, "network"),
("ValueError", None, "error"),
])
def test_probe_classifies_failures(settings_mod, monkeypatch, exc_name, status, expected_kind):
_configure_groq(settings_mod)
exc = type(exc_name, (Exception,), {})()
if status is not None:
exc.status_code = status
_fake_openai(monkeypatch, raise_exc=exc)
body = settings_mod.test_llm_provider("groq")
assert body["ok"] is False
assert body["kind"] == expected_kind
assert "latency_ms" in body
def test_probe_failure_detail_is_scrubbed(settings_mod, monkeypatch):
_configure_groq(settings_mod)
_fake_openai(monkeypatch, raise_exc=RuntimeError(
"boom key=gsk-test-123 at /Users/someone/secret"))
body = settings_mod.test_llm_provider("groq")
assert body["ok"] is False
assert "gsk-test-123" not in body["detail"]
# ── /models discovery ───────────────────────────────────────────────────────
def test_models_lists_sorted_ids(settings_mod, monkeypatch):
_configure_groq(settings_mod)
_fake_openai(monkeypatch, models=["zeta", "alpha", "mid"])
body = settings_mod.list_llm_provider_models("groq")
assert body["ok"] is True
assert body["models"] == ["alpha", "mid", "zeta"]
def test_models_unconfigured_is_kind_config(settings_mod):
body = settings_mod.list_llm_provider_models("openai")
assert body == {"ok": False, "kind": "config", "models": []}
def test_models_failure_is_classified(settings_mod, monkeypatch):
_configure_groq(settings_mod)
exc = type("AuthenticationError", (Exception,), {})()
exc.status_code = 401
_fake_openai(monkeypatch, raise_exc=exc)
body = settings_mod.list_llm_provider_models("groq")
assert body["ok"] is False and body["kind"] == "auth" and body["models"] == []