From b680097dfbfb202763172def602eb2d9cc9a82f3 Mon Sep 17 00:00:00 2001 From: Elem Oghenekaro <71514976+e3o8o@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:39:46 +0300 Subject: [PATCH 1/9] Fix: keep the uploaded file extension when transcribing (#903) /transcribe wrote every upload to a temp file named .wav regardless of its real format. librosa picks its decoder from the extension, so any non-wav upload failed with "could not open/decode file" even though the format is one the app handles elsewhere. profiles.py already solves this for voice samples by keeping the uploaded extension when it is one of the audio types it accepts, and falling back to .wav otherwise. Same approach here, same set. The fallback means an unknown or missing extension behaves exactly as it does today. --- backend/routes/transcription.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/routes/transcription.py b/backend/routes/transcription.py index dc949132..7ba19d74 100644 --- a/backend/routes/transcription.py +++ b/backend/routes/transcription.py @@ -15,6 +15,10 @@ router = APIRouter() UPLOAD_CHUNK_SIZE = 1024 * 1024 # 1MB +# Same set profiles.py accepts for voice samples. librosa picks its decoder from the +# file extension, so the temp file has to keep the uploaded one. +ALLOWED_AUDIO_EXTS = {".wav", ".mp3", ".m4a", ".ogg", ".flac", ".aac", ".webm", ".opus"} + @router.post("/transcribe", response_model=models.TranscriptionResponse) async def transcribe_audio( @@ -23,7 +27,10 @@ async def transcribe_audio( model: str | None = Form(None), ): """Transcribe audio file to text.""" - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: + uploaded_ext = Path(file.filename or "").suffix.lower() + file_suffix = uploaded_ext if uploaded_ext in ALLOWED_AUDIO_EXTS else ".wav" + + with tempfile.NamedTemporaryFile(suffix=file_suffix, delete=False) as tmp: while chunk := await file.read(UPLOAD_CHUNK_SIZE): tmp.write(chunk) tmp_path = tmp.name From 2c9d02af62ff978ad33b1e9f303547b26becb079 Mon Sep 17 00:00:00 2001 From: Daniel Knoodle Date: Mon, 20 Jul 2026 14:39:58 -0500 Subject: [PATCH 2/9] fix(models): stop reporting errored downloads as still downloading (#926) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TaskManager.error_download() intentionally keeps a failed task in the active list (status="error") so /tasks/active can surface the error and retry UI — but /models/status derived its "downloading" flag from the same unfiltered list. One failed download therefore showed the model as downloading:true / downloaded:false for the life of the process, masking the model's real cache state (even a fully valid on-disk cache) until an app restart. Likely behind endless-spinner reports like #181 and the restart-fixes-it pattern in #883. Add TaskManager.get_pending_downloads() (downloading/extracting only) and use it in /models/status; /tasks/active behavior is unchanged. Fixes #925 Claude-Session: https://claude.ai/code/session_011iwL9AyeAWgz2jpgcHxJpC Co-authored-by: Claude Fable 5 --- backend/routes/models.py | 5 +- .../test_model_status_pending_downloads.py | 51 +++++++++++++++++++ backend/utils/tasks.py | 13 +++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_model_status_pending_downloads.py diff --git a/backend/routes/models.py b/backend/routes/models.py index 7cbb7b04..f6d56566 100644 --- a/backend/routes/models.py +++ b/backend/routes/models.py @@ -231,7 +231,10 @@ async def get_model_status(): backend_type = get_backend_type() task_manager = get_task_manager() - active_download_names = {task.model_name for task in task_manager.get_active_downloads()} + # Pending only — an errored task stays in the active list for the + # error/retry UI, but reporting it as "downloading" here would mask + # the model's real cache state until the app restarts (issue #925). + active_download_names = {task.model_name for task in task_manager.get_pending_downloads()} try: from huggingface_hub import scan_cache_dir diff --git a/backend/tests/test_model_status_pending_downloads.py b/backend/tests/test_model_status_pending_downloads.py new file mode 100644 index 00000000..ee69cfb5 --- /dev/null +++ b/backend/tests/test_model_status_pending_downloads.py @@ -0,0 +1,51 @@ +"""Errored downloads must not be reported as still downloading. + +A failed download intentionally stays in the TaskManager with +``status="error"`` so ``/tasks/active`` can surface the error and retry +UI — but ``/models/status`` derives its ``downloading`` flag from the +same list. Without a status filter, one failed download shows the model +as "downloading" forever and masks its real cache state until the app +restarts (issue #925, symptom reports like #181). +""" + +from backend.utils.tasks import TaskManager + + +def test_errored_download_is_not_pending(): + tm = TaskManager() + tm.start_download("whisper-turbo") + assert [t.model_name for t in tm.get_pending_downloads()] == ["whisper-turbo"] + + tm.error_download("whisper-turbo", "boom") + + assert tm.get_pending_downloads() == [] + # Still visible to /tasks/active for the error/retry UI. + active = tm.get_active_downloads() + assert [t.model_name for t in active] == ["whisper-turbo"] + assert active[0].status == "error" + assert active[0].error == "boom" + + +def test_retry_after_error_is_pending_again(): + tm = TaskManager() + tm.start_download("qwen3-4b") + tm.error_download("qwen3-4b", "boom") + tm.start_download("qwen3-4b") + assert [t.model_name for t in tm.get_pending_downloads()] == ["qwen3-4b"] + + +def test_completed_download_is_removed_everywhere(): + tm = TaskManager() + tm.start_download("whisper-turbo") + tm.complete_download("whisper-turbo") + assert tm.get_pending_downloads() == [] + assert tm.get_active_downloads() == [] + + +def test_cancel_dismisses_errored_download(): + tm = TaskManager() + tm.start_download("whisper-turbo") + tm.error_download("whisper-turbo", "boom") + assert tm.cancel_download("whisper-turbo") is True + assert tm.get_active_downloads() == [] + assert tm.get_pending_downloads() == [] diff --git a/backend/utils/tasks.py b/backend/utils/tasks.py index 8baf71c3..efec184e 100644 --- a/backend/utils/tasks.py +++ b/backend/utils/tasks.py @@ -67,6 +67,19 @@ class TaskManager: def get_active_downloads(self) -> List[DownloadTask]: """Get all active downloads.""" return list(self._active_downloads.values()) + + def get_pending_downloads(self) -> List[DownloadTask]: + """Get downloads that are still in flight. + + Excludes errored tasks, which stay in the active list so the + error/retry UI can show them but must not be reported as + "downloading" by /models/status. + """ + return [ + task + for task in self._active_downloads.values() + if task.status in ("downloading", "extracting") + ] def get_active_generations(self) -> List[GenerationTask]: """Get all active generations.""" From 4a6b5da793e0fde623b957a16dd735501a1967fb Mon Sep 17 00:00:00 2001 From: Johnny Boero Date: Mon, 20 Jul 2026 14:40:10 -0500 Subject: [PATCH 3/9] fix(linux): skip click-through toggle on dictate pill to prevent startup crash (#906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dictate pill window is built hidden at setup and the frontend emits dictate:hide as soon as it mounts. The handler calls set_ignore_cursor_events(true) on a window GTK has never realized, and tao's CursorIgnoreEvents path unwraps the missing GdkWindow (tao-0.34.5 event_loop.rs:449), panicking inside a glib dispatch that cannot unwind — the process aborts within seconds of launch on Linux. The click-through toggle exists as a macOS workaround for transparent always-on-top NSWindows lingering as invisible click targets; it was never needed on Linux. Gate all three call sites so Linux never toggles it: the true/false pair stays balanced (never set, never unset), and macOS/Windows builds are unchanged. Co-authored-by: Claude Fable 5 --- tauri/src-tauri/src/hotkey_monitor.rs | 3 +++ tauri/src-tauri/src/main.rs | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/tauri/src-tauri/src/hotkey_monitor.rs b/tauri/src-tauri/src/hotkey_monitor.rs index c2c01e98..bb0cbd12 100644 --- a/tauri/src-tauri/src/hotkey_monitor.rs +++ b/tauri/src-tauri/src/hotkey_monitor.rs @@ -264,6 +264,9 @@ fn apply_effect(app: &AppHandle, effect: Effect) { let _ = window.set_position(tauri::PhysicalPosition::new(x, y)); } } + // Skip on Linux: aborts if the window was never realized + // (see show_dictate_window in main.rs). + #[cfg(not(target_os = "linux"))] let _ = window.set_ignore_cursor_events(false); // Deliberately no set_focus() — taking key focus would yank // it out of whatever app the user was typing in, which is diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index cd5f405b..0f44ac90 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -112,6 +112,10 @@ pub fn show_dictate_window(app: &tauri::AppHandle) { let _ = window.set_position(PhysicalPosition::new(x, y)); } } + // Skip on Linux: tao's CursorIgnoreEvents handler unwraps the GdkWindow, + // which is None until the window is first shown, aborting the process. + // The click-through toggle is a macOS workaround and is never set on Linux. + #[cfg(not(target_os = "linux"))] let _ = window.set_ignore_cursor_events(false); let _ = window.show(); } @@ -1421,6 +1425,9 @@ pub fn run() { let handle_for_hide = app.handle().clone(); app.handle().listen("dictate:hide", move |_event| { if let Some(window) = handle_for_hide.get_webview_window(DICTATE_WINDOW_LABEL) { + // Skip on Linux: aborts if the window was never realized + // (see show_dictate_window). + #[cfg(not(target_os = "linux"))] let _ = window.set_ignore_cursor_events(true); let _ = window.set_position(PhysicalPosition::new(-10_000, -10_000)); let _ = window.hide(); From 05d90790f815b2b94d32dc07bcc0b409081c75bf Mon Sep 17 00:00:00 2001 From: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:14:31 +0200 Subject: [PATCH 4/9] Add Italian translation (#904) * Add Italian translation * Add Italian language support to i18n --- app/src/i18n/index.ts | 3 + app/src/i18n/locales/it/translation.json | 1273 ++++++++++++++++++++++ 2 files changed, 1276 insertions(+) create mode 100644 app/src/i18n/locales/it/translation.json diff --git a/app/src/i18n/index.ts b/app/src/i18n/index.ts index 2deb3a5d..85de4789 100644 --- a/app/src/i18n/index.ts +++ b/app/src/i18n/index.ts @@ -7,6 +7,7 @@ import ptBR from './locales/pt-BR/translation.json'; import zhCN from './locales/zh-CN/translation.json'; import zhTW from './locales/zh-TW/translation.json'; import fr from './locales/fr/translation.json'; +import it from './locales/it/translation.json'; export const SUPPORTED_LANGUAGES = [ { code: 'en', label: 'English' }, @@ -15,6 +16,7 @@ export const SUPPORTED_LANGUAGES = [ { code: 'zh-CN', label: '简体中文' }, { code: 'zh-TW', label: '繁體中文' }, { code: 'fr', label: 'Français' }, + { code: 'it', label: 'Italiano' }, ] as const; export type LanguageCode = (typeof SUPPORTED_LANGUAGES)[number]['code']; @@ -30,6 +32,7 @@ i18n 'zh-CN': { translation: zhCN }, 'zh-TW': { translation: zhTW }, fr: { translation: fr }, + it: { translation: it }, }, fallbackLng: 'en', supportedLngs: SUPPORTED_LANGUAGES.map((l) => l.code), diff --git a/app/src/i18n/locales/it/translation.json b/app/src/i18n/locales/it/translation.json new file mode 100644 index 00000000..5c21cb9d --- /dev/null +++ b/app/src/i18n/locales/it/translation.json @@ -0,0 +1,1273 @@ +{ + "common": { + "cancel": "Annulla", + "save": "Salva", + "delete": "Elimina", + "edit": "Modifica", + "close": "Chiudi", + "confirm": "Conferma", + "loading": "Caricamento…", + "error": "Errore", + "unknown": "Sconosciuto", + "unknownError": "Errore sconosciuto" + }, + "nav": { + "generate": "Genera", + "stories": "Storie", + "captures": "Acquisizioni", + "voices": "Voci", + "effects": "Effetti", + "audio": "Audio", + "models": "Modelli", + "settings": "Impostazioni", + "updateBadge": "Aggiorna" + }, + "captures": { + "title": "Acquisizioni", + "beta": "Beta", + "searchPlaceholder": "Cerca trascrizioni…", + "snippetEmpty": "(nessuna trascrizione)", + "noTranscriptError": "L'acquisizione non ha ancora una trascrizione", + "captureCardLabel": "Acquisizione · {{when}}", + "header": { + "modelSummary": "Whisper {{stt}} · Qwen3 · {{llm}}" + }, + "source": { + "dictation": "Dettatura", + "recording": "Registrazione", + "file": "File" + }, + "transcript": { + "refined": "Perfezionata", + "raw": "Grezza", + "refinedHint": "Perfezionata con Qwen3 · {{model}}", + "rawHint": "Trascritto con Whisper {{model}}" + }, + "actions": { + "configure": "Configura", + "import": "Importa", + "importing": "Caricamento…", + "dictate": "Detta", + "stop": "Interrompi", + "copy": "Copia", + "refine": "Perfeziona", + "reRefine": "Perfeziona di nuovo", + "export": "Esporta", + "exportDropdownLabel": "Esporta acquisizione come", + "exportAudio": "Audio (WAV)", + "exportTranscript": "Trascrizione (TXT)", + "exportMarkdown": "Markdown (MD)", + "delete": "Elimina", + "playAs": "Riproduci come {{name}}", + "playAsFallback": "Riproduci come…", + "playAsGenerating": "Generazione in corso…", + "playAsStop": "Interrompi · {{name}}", + "playAsStopFallback": "Interrompi · Voce", + "playAsDropdownLabel": "Riproduci trascrizione come" + }, + "empty": { + "noMatches": "Nessuna acquisizione corrisponde a \"{{query}}\"", + "none": "Ancora nessuna acquisizione.", + "loading": "Caricamento acquisizioni…", + "pickOne": "Scegli un'acquisizione per vedere la trascrizione.", + "holdToRecord": "Tieni premuto per registrare", + "toggleHandsFree": "Attiva/disattiva mani libere", + "pressShortcut": "Premi la scorciatoia in qualsiasi punto del computer per avviare la tua prima acquisizione.", + "turnOnShortcut": "Attiva la scorciatoia globale per dettare da ovunque — oppure clicca su Detta qui sopra per un'acquisizione nell'app.", + "openSettings": "Apri impostazioni Acquisizioni" + }, + "deleteDialog": { + "title": "Elimina acquisizione", + "description": "Questo eliminerà permanentemente l'acquisizione, il suo audio e la sua trascrizione. L'azione non può essere annullata.", + "deleting": "Eliminazione in corso…" + }, + "toast": { + "deleteFailed": "Eliminazione non riuscita", + "playAsFailed": "Riproduzione non riuscita", + "noVoice": "Nessun profilo vocale", + "noVoiceDescription": "Crea un profilo vocale prima di usare Riproduci come.", + "transcriptCopied": "Trascrizione copiata", + "copyFailed": "Copia non riuscita", + "exportSuccess": "Esportato in {{path}}", + "exportFailed": "Esportazione non riuscita", + "exportEmpty": "Nulla da esportare", + "shortcutNotArmed": "Scorciatoia attiva, ma non ancora pronta", + "shortcutNotArmedDescription_one": "Il download di {{names}} è ancora necessario. Apri la scheda Acquisizioni per iniziare.", + "shortcutNotArmedDescription_other": "Il download di {{names}} è ancora necessario. Apri la scheda Acquisizioni per iniziare." + }, + "pill": { + "recording": "Registrazione in corso", + "transcribing": "Trascrizione in corso", + "refining": "Perfezionamento in corso", + "speaking": "Riproduzione voce in corso", + "completed": "Fatto", + "stopAria": "Interrompi registrazione", + "errorFallback": "Qualcosa è andato storto", + "errorCopyTooltip": "Clicca per copiare l'errore" + }, + "chord": { + "capturing": "Acquisizione in corso…", + "pressShortcut": "Premi la tua scorciatoia", + "noKeys": "Nessun tasto ancora", + "unsupported": "\"{{key}}\" non è supportato nelle combinazioni. Prova con un tasto modificatore o una lettera.", + "notSet": "Non impostato" + }, + "readiness": { + "title": "Alcune cose prima di poter dettare", + "subheading": "La scorciatoia rimane disattivata finché tutto ciò che segue non è pronto.", + "downloadButton": "Scarica", + "downloading": "Download in corso…", + "downloadingPercent": "Download in corso… {{pct}}%", + "downloadStarted": "Download avviato", + "downloadStartedDescription": "Il download di {{name}} è avviato. La scorciatoia si attiverà al termine del processo.", + "downloadFailed": "Download non riuscito", + "stt": { + "label": "{{name}} (da voce a testo)", + "ready": "Modello scaricato.", + "missing": "Necessario per trascrivere il tuo audio", + "missingWithSize": "Necessario per trascrivere il tuo audio · {{size}}" + }, + "llm": { + "label": "{{name}} (perfezionamento)", + "ready": "Modello scaricato.", + "missing": "Sistemazione della trascrizione grezza prima dell'incollo", + "missingWithSize": "Sistemazione della trascrizione grezza prima dell'incollo · {{size}}" + }, + "inputMonitoring": { + "label": "Permesso Monitoraggio input", + "ready": "macOS consente a Voicebox di rilevare la tua scorciatoia globale.", + "missing": "macOS deve consentire a Voicebox di rilevare la scorciatoia globale.", + "openSettings": "Apri Impostazioni" + }, + "accessibility": { + "label": "Permesso Accessibilità", + "ready": "Voicebox può incollare le trascrizioni in altre app.", + "missing": "Richiesto per poter incollare le trascrizioni nell'app in primo piano.", + "openSettings": "Apri Impostazioni" + } + }, + "permissions": { + "accessibility": { + "title": "Concedi il permesso di Accessibilità per attivare l'incollo automatico", + "body": "Voicebox richiede Impostazioni di Sistema → Privacy e Sicurezza → Accessibilità per incollare le trascrizioni in altre app. Senza di questo, la tua dettatura verrà comunque salvata nella scheda Acquisizioni.", + "openSettings": "Apri Impostazioni", + "recheck": "L'ho abilitato", + "rechecking": "Verifica in corso…", + "stillMissing": "Non ancora rilevato. Di solito macOS richiede la chiusura e la riapertura di Voicebox dopo aver attivato il permesso." + }, + "inputMonitoring": { + "title": "Concedi il Monitoraggio input per attivare la scorciatoia globale", + "body": "Voicebox richiede Impostazioni di Sistema → Privacy e Sicurezza → Monitoraggio input per rilevare la tua combinazione di dettatura. L'opzione è attiva, ma macOS sta bloccando gli eventi della tastiera finché non lo consenti.", + "openSettings": "Opzioni Impostazioni", + "recheck": "L'ho abilitato", + "rechecking": "Verifica in corso…", + "stillMissing": "Non ancora rilevato. Di solito macOS richiede la chiusura e la riapertura di Voicebox dopo aver attivato il permesso." + } + } + }, + "voicesTab": { + "title": "Voci", + "loading": "Caricamento voci…", + "searchPlaceholder": "Cerca voci…", + "newVoice": "Nuova voce", + "avatarAlt": "Avatar di {{name}}", + "selectChannels": "Seleziona canali…", + "channelDefaultLabel": "{{name}} (Predefinito)", + "columns": { + "name": "Nome", + "language": "Lingua", + "generations": "Generazioni", + "samples": "Campioni", + "effects": "Effetti", + "channels": "Canali" + } + }, + "voiceInspector": { + "loading": "Caricamento…", + "defaultEffectsHint": "Applicati automaticamente alle nuove generazioni con questa voce.", + "fields": { + "description": "Descrizione" + }, + "toast": { + "invalidImageFormat": "Seleziona un formato PNG, JPG o WebP", + "avatarUpdated": "Avatar aggiornato", + "savedDescription": "\"{{name}}\" salvato." + } + }, + "audioChannels": { + "title": "Canali audio", + "newChannel": "Nuovo canale", + "loading": "Caricamento…", + "confirmDelete": "Eliminare questo canale?", + "noVoicesAssigned": "Nessuna voce assegnata", + "selectDevice": "Seleziona dispositivo", + "addDevice": "Aggiungi dispositivo", + "addVoice": "Aggiungi voce", + "defaultSuffix": "predefinito", + "empty": { + "message": "Ancora nessun canale audio. Crea il tuo primo canale per reindirizzare le voci a dispositivi specifici.", + "action": "Crea canale" + }, + "labels": { + "outputDevices": "Dispositivi di output", + "assignedVoices": "Voci assegnate" + }, + "devices": { + "title": "Dispositivi disponibili", + "defaultNote": "Il canale predefinito utilizza il dispositivo predefinito di sistema", + "toggleHint": "Clicca sui dispositivi per aggiungerli o rimuoverli dal canale selezionato", + "selectHint": "Seleziona un canale per assegnare i dispositivi", + "empty": "Nessun dispositivo audio trovato", + "requiresTauri": "La selezione del dispositivo audio richiede Tauri" + }, + "fields": { + "name": "Nome canale", + "namePlaceholder": "es. Virtual Cable, Broadcast" + }, + "createDialog": { + "title": "Crea canale audio", + "description": "Crea un nuovo canale audio (bus) per reindirizzare le voci a dispositivi di output specifici.", + "action": "Crea" + }, + "editDialog": { + "title": "Modifica canale", + "description": "Aggiorna le impostazioni del canale e le assegnazioni delle voci." + } + }, + "profileForm": { + "createTitle": "Crea voce", + "editTitle": "Modifica voce", + "createDescription": "Crea un nuovo profilo vocale da un campione audio o da una voce integrata.", + "editDescription": "Aggiorna i dettagli del tuo profilo vocale e gestisci i campioni.", + "draftRestored": "Bozza ripristinata", + "discard": "Scarta", + "source": { + "clone": "Clona da audio", + "builtin": "Voce integrata" + }, + "builtin": { + "hint": "Scegli una voce predefinita. Queste non richiedono un campione audio.", + "badge": "Voce integrata", + "note": "Questo profilo utilizza una voce integrata. La voce non può essere modificata dopo la creazione." + }, + "sampleTabs": { + "upload": "Carica", + "record": "Registra", + "system": "Audio di sistema" + }, + "fields": { + "engine": "Motore", + "voice": "Voce", + "name": "Nome", + "namePlaceholder": "La mia voce", + "descriptionLabel": "Descrizione (Facoltativa)", + "descriptionPlaceholder": "Descrivi questa voce…", + "language": "Lingua", + "referenceText": "Testo di riferimento", + "referenceTextPlaceholder": "Inserisci il testo esatto pronunciato nell'audio…", + "defaultEngine": "Motore predefinito", + "noPreference": "Nessuna preferenza", + "defaultEngineHint": "Seleziona automaticamente questo motore quando viene scelto il profilo.", + "defaultEffects": "Effetti predefiniti", + "defaultEffectsHint": "Effetti applicati automaticamente a tutte le nuove generazioni con questa voce.", + "personalityLabel": "Personalità", + "personalityPlaceholder": "es. \"un pirata scontroso che parla solo con metafore marinaresche\"", + "personalityHint": "Chi è questa voce e come parla. Guida il pulsante Componi e l'opzione di riscrittura nel personaggio nella pagina di generazione. Lascia vuoto per nasconderli entrambi." + }, + "avatar": { + "alt": "Anteprima avatar" + }, + "actions": { + "saving": "Salvataggio…", + "saveChanges": "Salva modifiche", + "createProfile": "Crea profilo" + }, + "validation": { + "nameRequired": "Il nome è richiesto", + "referenceRequired": "Il testo di riferimento è richiesto quando si aggiunge un campione", + "sampleRequired": "Il campione audio è richiesto", + "referenceTextRequired": "Il testo di riferimento è richiesto", + "audioTooLong": "L'audio è troppo lungo ({{duration}}). La durata massima è {{max}}.", + "audioFailed": "Impossibile convalidare il file audio. Prova con un file diverso." + }, + "toast": { + "recordingComplete": "Registrazione completata", + "recordingCompleteDescription": "L'audio è stato registrato con successo.", + "recordingError": "Errore di registrazione", + "systemAudioCaptured": "Audio di sistema acquisito", + "systemAudioCapturedDescription": "L'audio è stato acquisito con successo.", + "systemAudioError": "Errore di acquisizione dell'audio di sistema", + "transcribeFailed": "Trascrizione non riuscita", + "transcribeFailedFallback": "Impossibile trascrivere l'audio", + "noFile": "Nessun file selezionato", + "noFileDescription": "Seleziona prima un file audio.", + "invalidFile": "Tipo di file non valido", + "invalidImageFormat": "Seleziona un file immagine (PNG, JPG o WebP)", + "fileTooLarge": "File troppo grande", + "imageTooLargeDescription": "L'immagine deve essere inferiore a 5MB", + "avatarRemoved": "Avatar rimosso", + "avatarRemovedDescription": "L'immagine dell'avatar è stata rimossa con successo.", + "avatarRemoveFailed": "Impossibile rimuovere l'avatar", + "avatarUploadFailed": "Caricamento dell'avatar non riuscito", + "avatarUploadFailedFallback": "Impossibile caricare l'avatar", + "effectsUpdateFailed": "Aggiornamento degli effetti non riuscito", + "effectsUpdateFailedFallback": "Impossibile salvare la catena di effetti", + "voiceUpdated": "Voce aggiornata", + "voiceUpdatedDescription": "\"{{name}}\" è stata aggiornata con successo.", + "noVoiceSelected": "Nessuna voce selezionata", + "noVoiceSelectedDescription": "Seleziona una voce integrata.", + "profileCreated": "Profilo creato", + "profileCreatedBuiltin": "\"{{name}}\" è stato creato con una voce integrata.", + "profileCreatedSample": "\"{{name}}\" è stato creato con un campione.", + "sampleRequired": "Campione audio richiesto", + "sampleRequiredDescription": "Fornisci un campione audio per creare il profilo vocale.", + "referenceTextRequired": "Testo di riferimento richiesto", + "referenceTextRequiredDescription": "Fornisci il testo di riferimento per il campione audio.", + "invalidAudio": "File audio non valido", + "invalidAudioDescription": "La durata dell'audio è {{duration}}, ma il massimo consentito è {{max}}.", + "validationError": "Errore di convalida", + "rollbackFailed": "Ripristino non riuscito", + "rollbackFailedDescription": "Impossibile rimuovere il profilo creato dopo il fallimento del caricamento del campione.", + "profileRolledBack": "Il profilo è stato ripristinato.", + "sampleFailed": "Impossibile aggiungere il campione", + "sampleFailedDescription": "Impossibile aggiungere il campione.", + "sampleFailedRolledBack": "Impossibile aggiungere il campione. Il profilo è stato ripristinato.", + "saveFailed": "Impossibile salvare il profilo" + } + }, + "audioSample": { + "chooseFile": "Scegli file", + "uploadHint": "Clicca per scegliere un file o trascinalo qui. Durata massima: 30 secondi.", + "fileUploaded": "File caricato", + "fileLabel": "File: {{name}}", + "play": "Riproduci", + "pause": "Pausa", + "transcribe": "Trascrivi", + "transcribing": "Trascrizione in corso…", + "remove": "Rimuovi", + "startRecording": "Avvia registrazione", + "recordHint": "Clicca per avviare la registrazione. Durata massima: 30 secondi.", + "stopRecording": "Interrompi registrazione", + "remaining": "{{time}} rimanenti", + "recordingComplete": "Registrazione completata", + "recordAgain": "Registra di nuovo", + "startCapture": "Avvia acquisizione", + "systemHint": "Acquisisci l'audio dal tuo sistema. Durata massima: 30 secondi.", + "stopCapture": "Interrompi acquisizione", + "captureComplete": "Acquisizione completata", + "captureAgain": "Acquisisci di nuovo" + }, + "sampleList": { + "loading": "Caricamento campioni…", + "empty": { + "title": "Ancora nessun campione", + "hint": "Aggiungi il tuo primo campione audio per iniziare" + }, + "editing": "Modifica della trascrizione", + "placeholder": "Inserisci il testo di riferimento…", + "saving": "Salvataggio…", + "editTranscription": "Modifica trascrizione", + "deleteSample": "Elimina campione", + "addSample": "Aggiungi campione", + "note": "Nota: Un singolo campione di 30 secondi rappresenta la combinazione ideale. La qualità potrebbe diminuire con più campioni. In un aggiornamento futuro, i campioni potrebbero essere intercambiabili e contrassegnati per stili diversi della stessa voce.", + "deleteDialog": { + "title": "Elimina campione", + "description": "Sei sicuro di voler eliminare questo campione audio? Questa azione non può essere annullata.", + "deleting": "Eliminazione in corso…" + }, + "player": { + "play": "Riproduci campione", + "pause": "Pausa campione", + "stop": "Interrompi", + "stopAria": "Interrompi riproduzione", + "position": "Posizione riproduzione campione", + "positionValue": "{{current}} di {{total}}" + }, + "toast": { + "invalidText": "Testo non valido", + "invalidTextDescription": "Il testo di riferimento non può essere vuoto.", + "updated": "Campione aggiornato", + "updatedDescription": "Il testo di riferimento è stato aggiornato con successo.", + "updateFailed": "Aggiornamento non riuscito", + "updateFailedFallback": "Impossibile aggiornare il campione" + } + }, + "profiles": { + "card": { + "noDescription": "Nessuna descrizione", + "designed": "progettato", + "export": "Esporta profilo", + "edit": "Modifica profilo", + "delete": "Elimina profilo", + "selectLabel": "{{name}}, {{language}}. Seleziona come voce per la generazione.", + "selectLabelSelected": "{{name}}, {{language}}. Selezionato come voce per la generazione." + }, + "list": { + "errorLoading": "Errore durante il caricamento dei profili: {{message}}", + "empty": "Ancora nessun profilo vocale. Crea il tuo primo profilo per iniziare.", + "createVoice": "Crea voce", + "unsupportedNote": "È possibile selezionare solo i profili vocali supportati dal modello attuale." + }, + "deleteDialog": { + "title": "Elimina profilo", + "body": "Sei sicuro di voler eliminare \"{{name}}\"? Questa azione non può essere annullata.", + "deleting": "Eliminazione in corso…" + } + }, + "effects": { + "title": "Effetti", + "newPreset": "Nuovo preset", + "noDescription": "Nessuna descrizione", + "placeholder": "Seleziona un preset o creane uno nuovo", + "effectCount_one": "{{count}} effetto", + "effectCount_other": "{{count}} effetti", + "sections": { + "builtin": "Integrati", + "custom": "Personalizzati", + "new": "Nuovo" + }, + "badge": { + "builtin": "integrato" + }, + "unsaved": { + "title": "Preset non salvato", + "hint": "Configura gli effetti nel pannello di destra." + }, + "detail": { + "newTitle": "Nuovo preset", + "editTitle": "Modifica preset", + "savePreset": "Salva preset", + "saveAsCustom": "Salva come personalizzato", + "saving": "Salvataggio…", + "deleting": "Eliminazione in corso…" + }, + "fields": { + "name": "Nome", + "namePlaceholder": "Il mio preset…", + "description": "Descrizione", + "descriptionPlaceholder": "Descrivi cosa fa questo preset…" + }, + "preview": { + "label": "Anteprima", + "button": "Anteprima", + "processing": "Elaborazione in corso…", + "hint": "L'anteprima applica gli effetti alla versione pulita senza salvare." + }, + "saveAs": { + "title": "Salva come preset personalizzato", + "description": "Crea un nuovo preset personalizzato basato sulla catena di effetti attuale.", + "suggestedName": "{{name}} (Copia)" + }, + "toast": { + "saved": "Preset salvato", + "createdDescription": "\"{{name}}\" è stato creato.", + "updated": "Preset aggiornato", + "deleted": "Preset eliminato", + "saveFailed": "Salvataggio non riuscito", + "deleteFailed": "Eliminazione non riuscita", + "previewFailed": "Anteprima non riuscita", + "nameRequired": "Nome richiesto" + }, + "chain": { + "loadPreset": "Carica preset…", + "addEffect": "Aggiungi effetto…", + "clear": "Svuota", + "enable": "Abilita", + "disable": "Disabilita", + "remove": "Rimuovi" + }, + "types": { + "chorus": { + "label": "Chorus / Flanger", + "params": { + "rate_hz": "Velocità LFO (Hz)", + "depth": "Profondità di modulazione", + "feedback": "Quantità di feedback", + "centre_delay_ms": "Ritardo centrale (ms)", + "mix": "Miscela Wet/Dry" + } + }, + "reverb": { + "label": "Riverbero", + "params": { + "room_size": "Dimensione stanza", + "damping": "Smorzamento alte frequenze", + "wet_level": "Livello Wet", + "dry_level": "Livello Dry", + "width": "Ampiezza stereo" + } + }, + "delay": { + "label": "Delay", + "params": { + "delay_seconds": "Tempo di ritardo (secondi)", + "feedback": "Quantità di feedback", + "mix": "Miscela Wet/Dry" + } + }, + "compressor": { + "label": "Compressore", + "params": { + "threshold_db": "Soglia (dB)", + "ratio": "Rapporto di compressione", + "attack_ms": "Tempo di attacco (ms)", + "release_ms": "Tempo di rilascio (ms)" + } + }, + "gain": { + "label": "Guadagno", + "params": { + "gain_db": "Guadagno (dB)" + } + }, + "highpass": { + "label": "Filtro passa-alto", + "params": { + "cutoff_frequency_hz": "Frequenza di taglio (Hz)" + } + }, + "lowpass": { + "label": "Filtro passa-basso", + "params": { + "cutoff_frequency_hz": "Frequenza di taglio (Hz)" + } + }, + "pitch_shift": { + "label": "Pitch Shift", + "params": { + "semitones": "Semitoni di spostamento" + } + } + }, + "builtinPresets": { + "Robotic": { + "name": "Robotico", + "description": "Voce robotica metallica (flanger con LFO lento e feedback elevato)" + }, + "Radio": { + "name": "Radio", + "description": "Voce sottile da radio AM con filtraggio passa-banda e leggera compressione" + }, + "Echo Chamber": { + "name": "Camera d'eco", + "description": "Riverbero ampio con eco prolungato" + }, + "Deep Voice": { + "name": "Voce profonda", + "description": "Tonalità più bassa con maggiore calore" + } + } + }, + "stories": { + "title": "Storie", + "newStory": "Nuova storia", + "loading": "Caricamento storie…", + "searchPlaceholder": "Cerca storie…", + "empty": { + "title": "Ancora nessuna storia", + "hint": "Crea la tua prima storia per iniziare", + "noMatches": "Nessuna storia corrisponde a \"{{query}}\"" + }, + "row": { + "itemCount_one": "{{count}} elemento", + "itemCount_other": "{{count}} elementi", + "ariaLabel": "Storia {{name}}, {{count}} elementi, {{updated}}", + "actionsLabel": "Azioni per {{name}}" + }, + "createDialog": { + "title": "Crea nuova storia", + "description": "Crea una nuova storia per organizzare le tue generazioni vocali in conversazioni.", + "action": "Crea", + "creating": "Creazione in corso…" + }, + "editDialog": { + "title": "Modifica storia", + "description": "Aggiorna il nome e la descrizione della storia.", + "saving": "Salvataggio in corso…" + }, + "deleteDialog": { + "title": "Sei sicuro?", + "description": "Questo eliminerà permanentemente la storia e tutti i suoi elementi. Questa azione non può essere annullata.", + "deleting": "Eliminazione in corso…" + }, + "fields": { + "name": "Nome", + "namePlaceholder": "La mia storia", + "descriptionLabel": "Descrizione (facoltativa)", + "descriptionPlaceholder": "Una conversazione tra…" + }, + "toast": { + "nameRequired": "Nome richiesto", + "nameRequiredDescription": "Inserisci il nome della storia", + "created": "Storia creata", + "createdDescription": "\"{{name}}\" è stata creata", + "createFailed": "Impossibile creare la storia", + "updateFailed": "Impossibile aggiornare la storia", + "deleteFailed": "Impossibile eliminare la storia" + } + }, + "storyContent": { + "selectStory": { + "title": "Seleziona una storia", + "hint": "Scegli una storia dall'elenco per visualizzarne il contenuto" + }, + "loading": "Caricamento storia…", + "notFound": { + "title": "Storia non trovata", + "hint": "Impossibile caricare la storia selezionata" + }, + "generatingCount_one": "Generazione di {{count}} traccia audio", + "generatingCount_other": "Generazione di {{count}} tracce audio", + "add": "Aggiungi", + "searchPlaceholder": "Cerca per nome o trascrizione…", + "searchNoMatches": "Nessuna generazione corrispondente trovata", + "searchNoAvailable": "Nessuna generazione disponibile", + "exportAudio": "Esporta audio", + "empty": { + "title": "Nessun elemento in questa storia", + "hint": "Genera del testo parlato utilizzando la casella sottostante per aggiungere elementi" + }, + "itemActions": { + "playFromHere": "Riproduci da qui", + "regenerate": "Rigenera", + "removeFromStory": "Rimuovi dalla storia" + }, + "importAudio": "Importa audio…", + "importing": "Importazione in corso…", + "dropToImport": "Rilascia l'audio per importarlo", + "toast": { + "removeFailed": "Impossibile rimuovere l'elemento", + "reorderFailed": "Impossibile riordinare gli elementi", + "exportFailed": "Impossibile esportare l'audio", + "addFailed": "Impossibile aggiungere la generazione", + "regenerateFailed": "Impossibile rigenerare", + "importFailed": "Impossibile importare l'audio" + } + }, + "history": { + "empty": "Ancora nessuna generazione vocale…", + "actions": { + "menu": "Azioni", + "play": "Riproduci", + "exportAudio": "Esporta audio", + "exportPackage": "Esporta pacchetto", + "applyEffects": "Applica effetti", + "regenerate": "Rigenera" + }, + "deleteDialog": { + "title": "Elimina generazione", + "body": "Sei sicuro di voler eliminare questa generazione da \"{{name}}\"? Questa azione non può essere annullata.", + "deleting": "Eliminazione in corso…" + }, + "clearFailedDialog": { + "title": "Cancella generazioni non riuscite", + "body_one": "Questo eliminerà permanentemente {{count}} generazione non riuscita dalla tua cronologia. L'azione non può essere annullata.", + "body_other": "Questo eliminerà permanentemente {{count}} generazioni non riuscite dalla tua cronologia. L'azione non può essere annullata.", + "clearing": "Cancellazione in corso…", + "clearAll": "Cancella tutto" + }, + "importDialog": { + "title": "Importa generazione", + "body": "Importa la generazione da \"{{name}}\". Questo la aggiungerà alla tua cronologia.", + "importing": "Importazione in corso…", + "action": "Importa" + }, + "effectsDialog": { + "title": "Applica effetti", + "body": "Configura gli effetti di post-elaborazione da applicare a questa generazione. Verrà creata una nuova versione.", + "sourceLabel": "Sorgente", + "sourcePlaceholder": "Seleziona la versione sorgente", + "apply": "Applica", + "applying": "Applicazione in corso…" + } + }, + "generation": { + "placeholder": { + "storyWithEffects": "Genera testo parlato per \"{{name}}\"… (digita / per gli effetti)", + "story": "Genera testo parlato per \"{{name}}\"…", + "profile": "Genera testo parlato usando {{name}}…", + "effectsHint": "Digita / per effetti come [risata], [sospiro]…", + "selectVoice": "Seleziona un profilo vocale qui sopra…" + }, + "button": { + "generate": "Genera testo parlato", + "generating": "Generazione in corso…", + "selectFirst": "Seleziona prima un profilo vocale" + }, + "instruct": { + "show": "Mostra istruzioni di pronuncia", + "hide": "Nascondi istruzioni di pronuncia", + "tooltip": "Istruzioni di pronuncia (tono, emozione, ritmo)", + "placeholder": "Istruzioni di pronuncia — es. Parla lentamente con calore, Autorevole e chiaro…" + }, + "voiceSelector": { + "placeholder": "Seleziona una voce…" + }, + "effects": { + "none": "Nessun effetto", + "profileDefault": "Predefinito del profilo" + }, + "compose": { + "tooltip": "Componi", + "ariaLabel": "Componi una battuta nel personaggio", + "failedTitle": "Composizione non riuscita", + "failedDescription": "Impossibile generare il testo da questa personalità." + }, + "persona": { + "tooltipActive": "Parla nel personaggio", + "tooltipInactive": "Parla nel personaggio", + "ariaLabelActive": "Parla nel personaggio", + "ariaLabelInactive": "Parla nel personaggio" + } + }, + "main": { + "importVoice": "Importa voce", + "createVoice": "Crea voce", + "import": { + "invalidTitle": "Tipo di file non valido", + "invalidDescription": "Seleziona un file .voicebox.zip valido", + "successTitle": "Profilo importato", + "successDescription": "Profilo vocale importato con successo", + "failedTitle": "Impossibile importare il profilo", + "dialogTitle": "Importa profilo", + "dialogDescription": "Importa il profilo da \"{{name}}\". Questo creerà un nuovo profilo contenente tutti i campioni.", + "importing": "Importazione in corso…", + "action": "Importa" + } + }, + "settings": { + "tabs": { + "general": "Generali", + "generation": "Generazione", + "captures": "Acquisizioni", + "mcp": "MCP", + "gpu": "GPU", + "logs": "Log", + "changelog": "Registro modifiche", + "about": "Informazioni" + }, + "language": { + "label": "Lingua", + "description": "Scegli la lingua di visualizzazione di Voicebox." + }, + "theme": { + "label": "Tema", + "description": "Adatta al sistema, oppure scegli un aspetto fisso chiaro o scuro.", + "options": { + "system": "Sistema", + "light": "Chiaro", + "dark": "Scuro" + } + }, + "general": { + "docs": { + "title": "Leggi la documentazione" + }, + "discord": { + "title": "Unisciti a Discord", + "subtitle": "Ricevi aiuto e condividi le voci" + }, + "serverUrl": { + "title": "URL del server", + "description": "L'indirizzo del server backend di Voicebox.", + "invalidUrl": "Inserisci un URL valido", + "updatedTitle": "URL del server aggiornato", + "updatedDescription": "Connesso a {{url}}" + }, + "keepServerRunning": { + "title": "Mantieni il server in esecuzione alla chiusura dell'app", + "description": "Il server continuerà a funzionare in background dopo la chiusura dell'applicazione.", + "failedTitle": "Impossibile aggiornare l'impostazione", + "failedDescription": "Impossibile sincronizzare l'impostazione con il backend.", + "updatedTitle": "Impostazione aggiornata", + "runningDescription": "Il server rimarrà in esecuzione alla chiusura dell'app", + "stoppedDescription": "Il server si arresterà alla chiusura dell'app" + }, + "networkAccess": { + "title": "Consenti l'accesso alla rete", + "description": "Rende il server accessibile da altri dispositivi sulla tua rete. Riavvia l'app dopo la modifica.", + "updatedTitle": "Impostazione aggiornata", + "enabled": "Accesso alla rete abilitato. Riavvia l'app per applicare le modifiche.", + "disabled": "Accesso alla rete disabilitato. Riavvia l'app per applicare le modifiche." + }, + "connection": { + "connecting": "Connessione in corso", + "offline": "Offline", + "online": "Online" + }, + "updates": { + "title": "Aggiornamenti dell'app", + "devSuffix": " (sviluppo)", + "devMode": { + "title": "Modalità di sviluppo", + "description": "Gli aggiornamenti automatici sono disabilitati in modalità di sviluppo." + }, + "check": { + "title": "Controlla aggiornamenti", + "available": "Versione {{version}} disponibile", + "checking": "Verifica in corso…", + "upToDate": "L'applicazione è aggiornata", + "button": "Controlla" + }, + "error": "Errore di aggiornamento", + "download": { + "title": "Aggiorna alla versione {{version}}", + "description": "Scarica e installa l'ultima versione.", + "button": "Scarica" + }, + "downloading": "Download dell'aggiornamento in corso…", + "ready": { + "title": "Aggiornamento pronto per l'installazione", + "description": "La versione {{version}} è stata scaricata. Riavvia per completare.", + "button": "Riavvia ora" + } + }, + "api": { + "title": "Accesso API", + "description": "Integra Voicebox nel tuo flusso di lavoro tramite l'API REST all'indirizzo {{url}}", + "viewReference": "Visualizza il riferimento API completo", + "endpoints": { + "generate": "Genera testo parlato", + "health": "Stato del server", + "profiles": "Elenca voci", + "history": "Generazioni passate" + } + } + }, + "generation": { + "title": "Generazione", + "description": "Controlli per la generazione di testi lunghi. Queste impostazioni si applicano a tutti i motori.", + "chunkLimit": { + "title": "Limite di suddivisione automatica", + "description": "I testi lunghi vengono suddivisi in blocchi in corrispondenza dei confini delle frasi. Valori più bassi possono migliorare la qualità per output lunghi.", + "value": "{{chars}} caratteri" + }, + "crossfade": { + "title": "Dissolvenza incrociata tra blocchi", + "description": "Sfuma l'audio tra i blocchi per rendere fluide le transizioni. Imposta a 0 per un taglio netto.", + "cut": "Taglio netto", + "ms": "{{ms}}ms" + }, + "normalize": { + "title": "Normalizza audio", + "description": "Regola il volume dell'output a un livello coerente in tutte le generazioni." + }, + "autoplay": { + "title": "Riproduzione automatica alla generazione", + "description": "Riproduci automaticamente l'audio al completamento di una generazione." + }, + "folder": { + "title": "Cartella delle generazioni", + "description": "Cartella del disco in cui vengono memorizzati i file audio generati.", + "open": "Apri" + }, + "sidebar": { + "aboutTitle": "Informazioni sulla generazione vocale", + "aboutBody": "Clona una voce da un breve campione, quindi genera testo parlato in qualsiasi voce e in qualsiasi lingua. Integra la sintesi vocale (TTS) in agenti IA, giochi, podcast o narrazioni a lungo formato.", + "differencesTitle": "Cosa cambia", + "clone": { + "title": "Clona qualsiasi voce in pochi secondi.", + "body": "Bastano pochi secondi di audio di riferimento. Supporto multi-campione per una qualità superiore quando ne hai bisogno." + }, + "engines": { + "title": "Sette motori, 23 lingue.", + "body": "Scegli il compromesso più adatto alle tue esigenze tra qualità, velocità o copertura multilingue." + }, + "agentReady": { + "title": "Pronto per gli agenti.", + "body": "API REST con controllo per singolo profilo — assegna a qualsiasi IA una voce che hai clonato." + } + } + }, + "captures": { + "dictation": { + "title": "Dettatura", + "description": "Acquisisci audio da qualsiasi punto del tuo computer con una scorciatoia globale.", + "globalShortcut": { + "title": "Scorciatoia globale", + "description": "Tieni premuta la scorciatoia per registrare da qualsiasi punto del tuo computer. Rilascia per trascrivere." + }, + "pushToTalk": { + "title": "Scorciatoia Push-to-talk", + "description": "Tieni premuti questi tasti in qualsiasi punto del sistema per registrare. Rilascia per interrompere e trascrivere.", + "change": "Cambia" + }, + "toggle": { + "title": "Scorciatoia di attivazione/disattivazione", + "description": "Premi una volta per avviare una registrazione a mani libere. Premi di nuovo per interromperla. Di solito corrisponde alla combinazione push-to-talk più lo Spazio.", + "change": "Cambia" + }, + "chordPicker": { + "pttTitle": "Imposta la scorciatoia push-to-talk", + "pttDescription": "Tieni premuti i tasti che desideri utilizzare, quindi rilasciali e clicca su Salva. L'indicatore del tasto modificatore sul lato destro mostra se si tratta della variante sinistra o destra.", + "toggleTitle": "Imposta la scorciatoia di attivazione/disattivazione", + "toggleDescription": "Tieni premuti i tasti che desideri utilizzare, quindi rilasciali e clicca su Salva. Scegli una combinazione diversa rispetto a quella del push-to-talk." + }, + "preview": { + "title": "Anteprima", + "description": "Cosa appare sullo schermo mentre tieni premuta la scorciatoia." + }, + "copyToClipboard": { + "title": "Copia trascrizione negli appunti", + "description": "La trascrizione ripulita viene salvata negli appunti al termine dell'acquisizione." + }, + "autoPaste": { + "title": "Incollo automatico nel campo di testo attivo", + "description": "Se un campo di inserimento testo è attivo in un'altra app, incolla direttamente al suo interno. Voicebox salva e ripristina il contenuto precedente dei tuoi appunti." + } + }, + "transcription": { + "title": "Trascrizione", + "description": "Scegli quale modello di riconoscimento vocale (speech-to-text) eseguire sulle tue acquisizioni.", + "model": { + "title": "Modello di trascrizione", + "description": "Whisper è integrato in Voicebox ed è eseguito interamente sul tuo computer.", + "base": "Whisper Base · 74M · {{tail}}", + "small": "Whisper Small · 244M · {{tail}}", + "medium": "Whisper Medium · 769M · {{tail}}", + "large": "Whisper Large · 1.5B · {{tail}}", + "turbo": "Whisper Turbo · Large v3 ridotto · {{tail}}", + "tail": { + "fast": "Veloce", + "balanced": "Bilanciato", + "higher": "Precisione superiore", + "best": "Massima precisione", + "nearBest": "Precisione quasi massima, veloce" + } + }, + "language": { + "title": "Lingua", + "description": "Il rilevamento automatico funziona per la maggior parte delle acquisizioni. Imposta una lingua fissa se parli sempre la stessa.", + "auto": "Rilevamento automatico", + "en": "Inglese", + "es": "Spagnolo", + "fr": "Francese", + "de": "Tedesco", + "ja": "Giapponese", + "zh": "Cinese", + "hi": "Hindi" + }, + "archive": { + "title": "Archivia audio", + "description": "Conserva la registrazione originale insieme a ciascuna trascrizione." + } + }, + "refinement": { + "title": "Perfezionamento", + "description": "Esegui facoltativamente un modello linguistico locale (LLM) sulle trascrizioni per rimuovere intercalari, correggere la punteggiatura e le autocorrezioni.", + "auto": { + "title": "Perfeziona automaticamente le trascrizioni", + "description": "Viene eseguito dopo ogni acquisizione. Puoi comunque passare dalla versione grezza a quella perfezionata nella scheda Acquisizioni." + }, + "model": { + "title": "Modello di perfezionamento", + "description": "I modelli più grandi sono più lenti ma gestiscono meglio le sottili autocorrezioni e il vocabolario tecnico.", + "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", + "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", + "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}", + "tail": { + "veryFast": "Molto veloce", + "fast": "Veloce", + "fullQuality": "Massima qualità" + } + }, + "smartCleanup": { + "title": "Pulizia intelligente", + "description": "Rimuovi gli intercalari (ehm, uh, cioè), ripristina la punteggiatura e correggi le maiuscole senza riformulare il testo." + }, + "selfCorrection": { + "title": "Rimuovi autocorrezioni", + "description": "Quando cambi idea a metà frase (\"anzi no...\", \"aspetta, volevo dire...\"), elimina la parte ritrattata e mantieni solo l'intenzione finale." + }, + "preserveTechnical": { + "title": "Preserva i termini tecnici", + "description": "Mantieni gli identificatori di codice, i nomi dei comandi e gli acronimi esattamente come vengono pronunciati. Attiva questa opzione quando detti all'interno di un prompt di codice." + } + }, + "playback": { + "title": "Riproduzione", + "description": "Voce predefinita per l'azione \"Riproduci come\" nella scheda Acquisizioni.", + "defaultVoice": { + "title": "Voce predefinita", + "description": "Utilizzata quando clicchi su Riproduci come senza scegliere prima una voce. Puoi modificarla per ogni singola acquisizione.", + "noClonedVoices": "Ancora nessuna voce clonata", + "noneSelected": "Nessuna selezionata", + "clonedVoices": "Voci clonate" + } + }, + "storage": { + "title": "Archiviazione", + "description": "Le acquisizioni vengono salvate come file audio e di trascrizione accoppiati nella cartella dei dati di Voicebox.", + "retention": { + "title": "Conservazione", + "description": "Per quanto tempo conservare le acquisizioni. Si applica sia all'audio che alle trascrizioni.", + "forever": "Conserva per sempre", + "d90": "90 giorni", + "d30": "30 giorni", + "d7": "7 giorni" + }, + "folder": { + "title": "Cartella delle acquisizioni", + "description": "Cartella del disco in cui vengono memorizzati l'audio e le trascrizioni delle acquisizioni.", + "open": "Apri" + } + }, + "sidebar": { + "aboutTitle": "Informazioni sulle Acquisizioni", + "aboutBody": "Tieni premuta una scorciatoia in qualsiasi punto del tuo computer, parla e Voicebox trasformerà la tua voce in testo. Riproducilo con qualsiasi voce clonata, incollalo in qualsiasi app o invialo direttamente al tuo agente di programmazione.", + "differencesTitle": "Cosa cambia", + "local": { + "title": "Interamente locale.", + "body": "Whisper e il modello LLM di perfezionamento vengono eseguiti sull'hardware del tuo computer. Nessun cloud, nessun account, la tua voce non lascia mai la macchina." + }, + "playAs": { + "title": "Riproduci con qualsiasi voce.", + "body": "Le trascrizioni possono essere rilette da qualsiasi profilo che hai clonato." + }, + "crossPlatform": { + "title": "Multipiattaforma.", + "body": "Stessa scorciatoia, stesso flusso di lavoro su macOS, Windows e Linux." + }, + "windowsCaveat": { + "title": "Attenzione su Windows", + "body": "La scorciatoia non si attiverà mentre Voicebox stesso o qualsiasi applicazione eseguita come amministratore è in primo piano. Ci stiamo lavorando." + } + } + }, + "mcp": { + "install": { + "title": "Installa nel tuo agente", + "description": "Voicebox espone un server MCP locale ogni volta che l'app è aperta. Incolla uno di questi frammenti nella configurazione MCP del tuo agente.", + "http": { + "title": "HTTP (consigliato)", + "description": "Per i client che supportano HTTP MCP — Claude Code, Cursor, Windsurf, VS Code." + }, + "claudeCode": { + "title": "Comando a riga singola per Claude Code", + "description": "Si registra tramite l'interfaccia a riga di comando (CLI) di Claude Code." + }, + "stdio": { + "title": "Stdio (alternativa)", + "description": "Per i client che avviano solo processi stdio. Il file binario shim viene fornito insieme all'app." + }, + "copy": "Copia", + "copied": "Copiato" + }, + "defaultVoice": { + "title": "Voce predefinita", + "description": "Utilizzata quando un agente chiama voicebox.speak senza specificare un profilo e non ha un'associazione per singolo client.", + "label": "Voce di riproduzione predefinita", + "labelHint": "Condivisa con il menu a discesa 'Riproduci come voce' della scheda Acquisizioni — una sola voce predefinita per la riproduzione passiva.", + "none": "(nessuna)" + }, + "bindings": { + "title": "Voce per singolo agente", + "description": "Associa agenti specifici a voci specifiche, in modo da poter capire chi sta parlando senza guardare lo schermo. L'agente si identifica tramite l'intestazione X-Voicebox-Client-Id (o la variabile d'ambiente VOICEBOX_CLIENT_ID per stdio).", + "empty": "Ancora nessuna associazione. Aggiungine una qui sotto, quindi configura il tuo client MCP per inviare il corrispondente X-Voicebox-Client-Id.", + "lastSeen": "ultimo rilevamento {{when}}", + "lastSeenTitle": "Ultimo rilevamento {{when}}", + "neverConnected": "mai connesso", + "defaultOption": "(predefinita)", + "removeAria": "Rimuovi associazione per {{client}}", + "add": { + "title": "Aggiungi un'associazione", + "clientIdPlaceholder": "id client (es. claude-code)", + "labelPlaceholder": "etichetta (facoltativa)", + "action": "Aggiungi associazione" + } + }, + "sidebar": { + "aboutTitle": "Informazioni su MCP", + "aboutBody": "Il protocollo Model Context Protocol consente al tuo agente di programmazione IA — Claude Code, Cursor, Windsurf — di chiamare gli strumenti di Voicebox. Parla con una voce clonata, trascrivi tracce audio, sfoglia le acquisizioni.", + "toolsTitle": "Strumenti disponibili", + "tools": { + "speak": "Pronuncia il testo all'interno di un profilo vocale.", + "transcribe": "Riconoscimento vocale Whisper STT su una traccia.", + "listCaptures": "Dettature / registrazioni recenti.", + "listProfiles": "Profili vocali disponibili." + }, + "postSpeak": "Esposto anche come POST /speak per script di shell, ACP, A2A." + } + }, + "gpu": { + "cpuOnly": "Solo CPU", + "vramUsed": "{{mb}} MB VRAM", + "noAcceleration": "Nessuna accelerazione GPU rilevata", + "active": "Attiva", + "cuda": { + "title": "Backend CUDA", + "activeTitle": "Backend CUDA attivo", + "description": "Accelerazione GPU NVIDIA tramite un backend CUDA scaricabile.", + "downloading": "Download del backend CUDA in corso…", + "downloadingShort": "Download in corso…", + "updating": "Aggiornamento in corso…" + }, + "activeBackend": { + "description": "L'accelerazione GPU è attualmente abilitata." + }, + "restart": { + "ready": "Server riavviato con successo", + "waiting": "Riavvio del server in corso…", + "stopping": "Arresto del server in corso…" + }, + "download": { + "title": "Scarica il backend CUDA", + "description": "Download di circa 2,4 GB. Richiede una GPU NVIDIA con supporto CUDA.", + "button": "Scarica" + }, + "switchToCuda": { + "title": "Passa al backend CUDA", + "description": "Il backend CUDA è stato scaricato ed è pronto. Riavvia per abilitarlo.", + "button": "Riavvia" + }, + "switchToCpu": { + "title": "Passa al backend CPU", + "description": "Disabilita l'accelerazione GPU. Potrai scaricare nuovamente il backend GPU in un secondo momento.", + "button": "Passa a CPU" + }, "remove": { + "title": "Rimuovi il backend CUDA", + "description": "Elimina il file binario CUDA scaricato per liberare spazio sul disco.", + "button": "Rimuovi" + }, + "errors": { + "downloadFailed": "Download non riuscito", + "downloadStart": "Impossibile avviare il download", + "restartFailed": "Riavvio non riuscito", + "switchCpu": "Impossibile passare alla CPU", + "deleteCuda": "Impossibile eliminare il backend CUDA", + "deleteRocm": "Impossibile eliminare il backend ROCm" + }, + "footer": "Voicebox rileva e utilizza automaticamente la migliore GPU disponibile sul tuo sistema. Sui Mac con chip Apple Silicon, il backend MLX viene eseguito nativamente sul Neural Engine e sulla GPU tramite Metal Performance Shaders (MPS), senza richiedere alcuna configurazione aggiuntiva. Su Windows, puoi scaricare i backend opzionali CUDA (NVIDIA) o ROCm (AMD) per l'inferenza con accelerazione hardware. Dove disponibili tramite PyTorch, sono supportati anche Intel XPU e DirectML. Quando non viene rilevata alcuna GPU, Voicebox si affida alla CPU — tutti i motori continuano a funzionare, solo più lentamente.", + "rocm": { + "title": "Backend AMD ROCm", + "activeTitle": "Backend ROCm attivo", + "description": "Accelerazione GPU AMD tramite un backend ROCm scaricabile.", + "downloading": "Download del backend ROCm in corso…", + "downloadingShort": "Download in corso…", + "updating": "Aggiornamento in corso…" + }, + "downloadRocm": { + "title": "Scarica il backend AMD ROCm", + "description": "Download di circa 2-3 GB. Richiede una GPU AMD Radeon con supporto ROCm.", + "button": "Scarica" + }, + "switchToRocm": { + "title": "Passa al backend ROCm", + "description": "Il backend ROCm è stato scaricato ed è pronto. Riavvia per abilitarlo.", + "button": "Riavvia" + }, + "removeRocm": { + "title": "Rimuovi il backend ROCm", + "description": "Elimina il file binario ROCm scaricato per liberare spazio sul disco.", + "button": "Rimuovi" + } + }, + "logs": { + "title": "Log del server", + "lineCount_one": "{{count}} riga", + "lineCount_other": "{{count}} righe", + "scrollToBottom": "Scorri fino in fondo", + "clear": "Svuota", + "empty": "Ancora nessun log disponibile.", + "devHint": "I log del server vengono acquisiti solo quando l'app gestisce il processo del server (build di produzione)." + }, + "changelog": { + "devBadge": "sviluppo", + "showLess": "Mostra meno", + "showMore": "Mostra di più" + }, + "about": { + "tagline": "Lo studio di sintesi vocale open source. Clona voci, genera testo parlato, applica effetti e sviluppa applicazioni vocali — tutto eseguito localmente sul tuo computer.", + "createdBy": "Creato da", + "buyCoffee": "Offrimi un caffè", + "license": "Rilasciato sotto licenza MIT" + } + }, + "models": { + "title": "Modelli", + "subtitle": "Scarica e gestisci i modelli di intelligenza artificiale per la generazione vocale e la trascrizione", + "defaultName": "Modello", + "unknownSize": "Dimensione sconosciuta", + "sections": { + "voiceGeneration": "Generazione vocale", + "transcription": "Trascrizione", + "languageModels": "Modelli linguistici" + }, + "status": { + "loaded": "Caricato" + }, + "storage": { + "location": "Posizione di archiviazione", + "open": "Apri", + "change": "Cambia", + "migrating": "Migrazione in corso…", + "reset": "Ripristina", + "pickerTitle": "Scegli la cartella di archiviazione dei modelli" + }, + "progress": { + "connecting": "Connessione in corso…", + "connectingHf": "Connessione a HuggingFace in corso…" + }, + "problems": { + "title": "Problemi", + "clearAll": "Cancella tutto", + "noDetails": "Nessun dettaglio sull'errore disponibile. Prova a scaricare di nuovo.", + "startedAt": "avviato alle {{time}}" + }, + "detail": { + "loadingInfo": "Caricamento informazioni modello…", + "byAuthor": "di {{author}}", + "downloads": "Download", + "likes": "Mi piace", + "license": "Licenza", + "languagesCount": "{{count}} lingue supportate", + "languagesList": "Lingue: {{list}}", + "onDisk": "{{size}} su disco" + }, + "actions": { + "download": "Scarica", + "retry": "Riprova download", + "unload": "Disattiva", + "unloading": "Disattivazione in corso…", + "unloadFirst": "Disattiva il modello prima di eliminarlo", + "deleteModel": "Elimina modello" + }, + "deleteDialog": { + "title": "Elimina modello", + "body": "Sei sicuro di voler eliminare {{name}}?", + "sizeNote": "Questo libererà {{size}} di spazio sul disco. Il modello dovrà essere scaricato nuovamente se desideri utilizzarlo ancora.", + "deleting": "Eliminazione in corso…" + }, + "migrateDialog": { + "title": "Spostare i modelli nella nuova posizione?", + "description": "Il server si arresterà durante lo spostamento dei modelli nella nuova cartella. Si riavvierà automaticamente al termine della migrazione.", + "action": "Sposta modelli", + "preparing": "Preparazione in corso…", + "restartingServer": "Riavvio del server in corso…" + }, + "migrate": { + "title": "Spostamento dei modelli", + "offline": "Il server è offline durante lo spostamento dei modelli." + }, + "toast": { + "downloadFailed": "Download non riuscito", + "cancelFailed": "Annullamento non riuscito", + "cancelFailedDescription": "Impossibile annullare l'attività di download.", + "deleted": "Modello eliminato", + "deletedDescription": "{{name}} è stato eliminato con successo.", + "deleteFailed": "Eliminazione non riuscita", + "unloaded": "Modello disattivato", + "unloadedDescription": "{{name}} è stato rimosso dalla memoria.", + "unloadFailed": "Disattivazione non riuscita", + "openFolderFailed": "Impossibile aprire la cartella del modello", + "pickerFailed": "Impossibile aprire il selettore di cartelle", + "resetToDefault": "Ripristinato alla posizione predefinita. Riavvio del server in corso…", + "noModelsToMigrate": "Nessun modello da migrare", + "noModelsToMigrateDescription": "Scarica almeno un modello prima di modificare la posizione di archiviazione.", + "migrated": "Modelli spostati con successo", + "migrationFailed": "Migrazione non riuscita", + "migrationFailedGeneric": "Impossibile migrare i modelli", + "migrationConnectionLost": "Connessione interrotta durante la migrazione" + } + } +} From 2dc3b075d534f687fc012d7477c8df1df88dce64 Mon Sep 17 00:00:00 2001 From: Jaime Cardona Villegas Date: Mon, 20 Jul 2026 22:15:57 +0200 Subject: [PATCH 5/9] feat(i18n): add Spanish (es) locale (#798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Spanish as a UI display language, matching the existing 4-locale pattern (en, ja, zh-CN, zh-TW) with full key parity. - app/src/i18n/locales/es/translation.json: 832 strings across 18 namespaces, translated from the en master. Keys, {{interpolation}} placeholders, /// tags and _one/_other plurals preserved. Brand/model names (Whisper, Qwen3, CUDA, MCP…) left untranslated by design. - app/src/i18n/index.ts: register `es` in SUPPORTED_LANGUAGES and resources; the language switcher and LanguageCode derive automatically. - app/src/lib/utils/format.ts: wire the date-fns `es` locale for relative-date formatting. Verified: key parity 832/832 (no missing/extra, placeholders & tags intact), biome check clean, app+web typecheck pass, build:web succeeds. Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Jamie Pine --- app/src/i18n/index.ts | 7 +- app/src/i18n/locales/es/translation.json | 1274 ++++++++++++++++++++++ app/src/lib/utils/format.ts | 4 +- 3 files changed, 1282 insertions(+), 3 deletions(-) create mode 100644 app/src/i18n/locales/es/translation.json diff --git a/app/src/i18n/index.ts b/app/src/i18n/index.ts index 85de4789..7d26f2ab 100644 --- a/app/src/i18n/index.ts +++ b/app/src/i18n/index.ts @@ -2,15 +2,17 @@ import i18n from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import { initReactI18next } from 'react-i18next'; import en from './locales/en/translation.json'; +import es from './locales/es/translation.json'; +import fr from './locales/fr/translation.json'; +import it from './locales/it/translation.json'; import ja from './locales/ja/translation.json'; import ptBR from './locales/pt-BR/translation.json'; import zhCN from './locales/zh-CN/translation.json'; import zhTW from './locales/zh-TW/translation.json'; -import fr from './locales/fr/translation.json'; -import it from './locales/it/translation.json'; export const SUPPORTED_LANGUAGES = [ { code: 'en', label: 'English' }, + { code: 'es', label: 'Español' }, { code: 'pt-BR', label: 'Português (Brasil)' }, { code: 'ja', label: '日本語' }, { code: 'zh-CN', label: '简体中文' }, @@ -27,6 +29,7 @@ i18n .init({ resources: { en: { translation: en }, + es: { translation: es }, 'pt-BR': { translation: ptBR }, ja: { translation: ja }, 'zh-CN': { translation: zhCN }, diff --git a/app/src/i18n/locales/es/translation.json b/app/src/i18n/locales/es/translation.json new file mode 100644 index 00000000..f1cbfd19 --- /dev/null +++ b/app/src/i18n/locales/es/translation.json @@ -0,0 +1,1274 @@ +{ + "common": { + "cancel": "Cancelar", + "save": "Guardar", + "delete": "Eliminar", + "edit": "Editar", + "close": "Cerrar", + "confirm": "Confirmar", + "loading": "Cargando…", + "error": "Error", + "unknown": "Desconocido", + "unknownError": "Error desconocido" + }, + "nav": { + "generate": "Generar", + "stories": "Historias", + "captures": "Capturas", + "voices": "Voces", + "effects": "Efectos", + "audio": "Audio", + "models": "Modelos", + "settings": "Ajustes", + "updateBadge": "Actualizar" + }, + "captures": { + "title": "Capturas", + "beta": "Beta", + "searchPlaceholder": "Buscar transcripciones…", + "snippetEmpty": "(sin transcripción)", + "noTranscriptError": "La captura aún no tiene transcripción", + "captureCardLabel": "Captura · {{when}}", + "header": { + "modelSummary": "Whisper {{stt}} · Qwen3 · {{llm}}" + }, + "source": { + "dictation": "Dictado", + "recording": "Grabación", + "file": "Archivo" + }, + "transcript": { + "refined": "Refinada", + "raw": "En bruto", + "refinedHint": "Refinada con Qwen3 · {{model}}", + "rawHint": "Transcrita con Whisper {{model}}" + }, + "actions": { + "configure": "Configurar", + "import": "Importar", + "importing": "Subiendo…", + "dictate": "Dictar", + "stop": "Detener", + "copy": "Copiar", + "refine": "Refinar", + "reRefine": "Volver a refinar", + "export": "Exportar", + "exportDropdownLabel": "Exportar captura como", + "exportAudio": "Audio (WAV)", + "exportTranscript": "Transcripción (TXT)", + "exportMarkdown": "Markdown (MD)", + "delete": "Eliminar", + "playAs": "Reproducir como {{name}}", + "playAsFallback": "Reproducir como…", + "playAsGenerating": "Generando…", + "playAsStop": "Detener · {{name}}", + "playAsStopFallback": "Detener · Voz", + "playAsDropdownLabel": "Reproducir transcripción como" + }, + "empty": { + "noMatches": "Ninguna captura coincide con \"{{query}}\"", + "none": "Aún no hay capturas.", + "loading": "Cargando capturas…", + "pickOne": "Elige una captura para ver la transcripción.", + "holdToRecord": "Mantén pulsado para grabar", + "toggleHandsFree": "Alternar manos libres", + "pressShortcut": "Pulsa el atajo en cualquier parte de tu equipo para iniciar tu primera captura.", + "turnOnShortcut": "Activa el atajo global para dictar desde cualquier sitio — o haz clic en Dictar arriba para una captura dentro de la app.", + "openSettings": "Abrir ajustes de Capturas" + }, + "deleteDialog": { + "title": "Eliminar captura", + "description": "Esto eliminará permanentemente la captura, su audio y su transcripción. No se puede deshacer.", + "deleting": "Eliminando…" + }, + "toast": { + "deleteFailed": "Error al eliminar", + "playAsFailed": "Error al reproducir como", + "noVoice": "Sin perfil de voz", + "noVoiceDescription": "Crea un perfil de voz antes de usar Reproducir como.", + "transcriptCopied": "Transcripción copiada", + "copyFailed": "Error al copiar", + "exportSuccess": "Exportado a {{path}}", + "exportFailed": "Error al exportar", + "exportEmpty": "Nada que exportar", + "shortcutNotArmed": "Atajo activado, pero aún no listo", + "shortcutNotArmedDescription_one": "{{names}} aún necesita descargarse. Abre la pestaña Capturas para empezar.", + "shortcutNotArmedDescription_other": "{{names}} aún necesitan descargarse. Abre la pestaña Capturas para empezar." + }, + "pill": { + "recording": "Grabando", + "transcribing": "Transcribiendo", + "refining": "Refinando", + "speaking": "Hablando", + "completed": "Listo", + "stopAria": "Detener grabación", + "errorFallback": "Algo salió mal", + "errorCopyTooltip": "Haz clic para copiar el error" + }, + "chord": { + "capturing": "Capturando…", + "pressShortcut": "Pulsa tu atajo", + "noKeys": "Aún no hay teclas", + "unsupported": "\"{{key}}\" no se admite en combinaciones. Prueba con un modificador o una tecla de letra.", + "notSet": "Sin definir" + }, + "readiness": { + "title": "Algunas cosas antes de que puedas dictar", + "subheading": "El atajo permanece desactivado hasta que todo lo de abajo esté listo.", + "downloadButton": "Descargar", + "downloading": "Descargando…", + "downloadingPercent": "Descargando… {{pct}}%", + "downloadStarted": "Descarga iniciada", + "downloadStartedDescription": "{{name}} se está descargando. El atajo se activará cuando termine.", + "downloadFailed": "Error de descarga", + "stt": { + "label": "{{name}} (voz a texto)", + "ready": "Modelo descargado.", + "missing": "Necesario para transcribir tu audio", + "missingWithSize": "Necesario para transcribir tu audio · {{size}}" + }, + "llm": { + "label": "{{name}} (refinamiento)", + "ready": "Modelo descargado.", + "missing": "Limpia la transcripción en bruto antes de pegar", + "missingWithSize": "Limpia la transcripción en bruto antes de pegar · {{size}}" + }, + "inputMonitoring": { + "label": "Permiso de Monitorización de entrada", + "ready": "macOS permite que Voicebox detecte tu atajo global.", + "missing": "macOS debe permitir que Voicebox detecte el atajo global.", + "openSettings": "Abrir Ajustes" + }, + "accessibility": { + "label": "Permiso de Accesibilidad", + "ready": "Voicebox puede pegar transcripciones en otras apps.", + "missing": "Necesario para que las transcripciones se peguen en la app activa.", + "openSettings": "Abrir Ajustes" + } + }, + "permissions": { + "accessibility": { + "title": "Concede el permiso de Accesibilidad para habilitar el pegado automático", + "body": "Voicebox necesita Ajustes del Sistema → Privacidad y seguridad → Accesibilidad para pegar transcripciones en otras apps. Tu dictado igualmente aparece en la pestaña Capturas sin él.", + "openSettings": "Abrir Ajustes", + "recheck": "Ya lo he activado", + "rechecking": "Comprobando…", + "stillMissing": "Sigue sin detectarse. macOS suele requerir salir y reabrir Voicebox tras cambiar el permiso." + }, + "inputMonitoring": { + "title": "Concede Monitorización de entrada para habilitar el atajo global", + "body": "Voicebox necesita Ajustes del Sistema → Privacidad y seguridad → Monitorización de entrada para detectar tu combinación de dictado. La opción está activada, pero macOS bloquea los eventos de teclado hasta que lo permitas.", + "openSettings": "Abrir Ajustes", + "recheck": "Ya lo he activado", + "rechecking": "Comprobando…", + "stillMissing": "Sigue sin detectarse. macOS suele requerir salir y reabrir Voicebox tras cambiar el permiso." + } + } + }, + "voicesTab": { + "title": "Voces", + "loading": "Cargando voces…", + "searchPlaceholder": "Buscar voces…", + "newVoice": "Nueva voz", + "avatarAlt": "Avatar de {{name}}", + "selectChannels": "Seleccionar canales…", + "channelDefaultLabel": "{{name}} (Predeterminado)", + "columns": { + "name": "Nombre", + "language": "Idioma", + "generations": "Generaciones", + "samples": "Muestras", + "effects": "Efectos", + "channels": "Canales" + } + }, + "voiceInspector": { + "loading": "Cargando…", + "defaultEffectsHint": "Se aplican automáticamente a las nuevas generaciones con esta voz.", + "fields": { + "description": "Descripción" + }, + "toast": { + "invalidImageFormat": "Selecciona PNG, JPG o WebP", + "avatarUpdated": "Avatar actualizado", + "savedDescription": "\"{{name}}\" guardada." + } + }, + "audioChannels": { + "title": "Canales de audio", + "newChannel": "Nuevo canal", + "loading": "Cargando…", + "confirmDelete": "¿Eliminar este canal?", + "noVoicesAssigned": "Sin voces asignadas", + "selectDevice": "Seleccionar dispositivo", + "addDevice": "Añadir dispositivo", + "addVoice": "Añadir voz", + "defaultSuffix": "predeterminado", + "empty": { + "message": "Aún no hay canales de audio. Crea tu primer canal para enrutar voces a dispositivos concretos.", + "action": "Crear canal" + }, + "labels": { + "outputDevices": "Dispositivos de salida", + "assignedVoices": "Voces asignadas" + }, + "devices": { + "title": "Dispositivos disponibles", + "defaultNote": "El canal predeterminado usa el dispositivo predeterminado del sistema", + "toggleHint": "Haz clic en los dispositivos para añadirlos o quitarlos del canal seleccionado", + "selectHint": "Selecciona un canal para asignar dispositivos", + "empty": "No se encontraron dispositivos de audio", + "requiresTauri": "La selección de dispositivos de audio requiere Tauri" + }, + "fields": { + "name": "Nombre del canal", + "namePlaceholder": "p. ej., Cable virtual, Emisión" + }, + "createDialog": { + "title": "Crear canal de audio", + "description": "Crea un nuevo canal de audio (bus) para enrutar voces a dispositivos de salida concretos.", + "action": "Crear" + }, + "editDialog": { + "title": "Editar canal", + "description": "Actualiza los ajustes del canal y las asignaciones de voz." + } + }, + "profileForm": { + "createTitle": "Crear voz", + "editTitle": "Editar voz", + "createDescription": "Crea un nuevo perfil de voz a partir de una muestra de audio o de una voz integrada.", + "editDescription": "Actualiza los detalles de tu perfil de voz y gestiona las muestras.", + "draftRestored": "Borrador restaurado", + "discard": "Descartar", + "source": { + "clone": "Clonar desde audio", + "builtin": "Voz integrada" + }, + "builtin": { + "hint": "Elige una voz prediseñada. Estas no requieren una muestra de audio.", + "badge": "Voz integrada", + "note": "Este perfil usa una voz integrada. La voz no se puede cambiar después de crearlo." + }, + "sampleTabs": { + "upload": "Subir", + "record": "Grabar", + "system": "Audio del sistema" + }, + "fields": { + "engine": "Motor", + "voice": "Voz", + "name": "Nombre", + "namePlaceholder": "Mi voz", + "descriptionLabel": "Descripción (opcional)", + "descriptionPlaceholder": "Describe esta voz…", + "language": "Idioma", + "referenceText": "Texto de referencia", + "referenceTextPlaceholder": "Introduce el texto exacto hablado en el audio…", + "defaultEngine": "Motor predeterminado", + "noPreference": "Sin preferencia", + "defaultEngineHint": "Selecciona automáticamente este motor cuando se elige el perfil.", + "defaultEffects": "Efectos predeterminados", + "defaultEffectsHint": "Efectos aplicados automáticamente a todas las nuevas generaciones con esta voz.", + "personalityLabel": "Personalidad", + "personalityPlaceholder": "p. ej. \"un pirata gruñón que solo habla con metáforas náuticas\"", + "personalityHint": "Quién es esta voz y cómo habla. Impulsa el botón Redactar y el interruptor de reescritura en personaje de la página de generación. Déjalo en blanco para ocultar ambos." + }, + "avatar": { + "alt": "Vista previa del avatar" + }, + "actions": { + "saving": "Guardando…", + "saveChanges": "Guardar cambios", + "createProfile": "Crear perfil" + }, + "validation": { + "nameRequired": "El nombre es obligatorio", + "referenceRequired": "El texto de referencia es obligatorio al añadir una muestra", + "sampleRequired": "La muestra de audio es obligatoria", + "referenceTextRequired": "El texto de referencia es obligatorio", + "audioTooLong": "El audio es demasiado largo ({{duration}}). La duración máxima es {{max}}.", + "audioFailed": "Error al validar el archivo de audio. Prueba con otro archivo." + }, + "toast": { + "recordingComplete": "Grabación completada", + "recordingCompleteDescription": "El audio se ha grabado correctamente.", + "recordingError": "Error de grabación", + "systemAudioCaptured": "Audio del sistema capturado", + "systemAudioCapturedDescription": "El audio se ha capturado correctamente.", + "systemAudioError": "Error de captura de audio del sistema", + "transcribeFailed": "Error de transcripción", + "transcribeFailedFallback": "Error al transcribir el audio", + "noFile": "Ningún archivo seleccionado", + "noFileDescription": "Selecciona primero un archivo de audio.", + "invalidFile": "Tipo de archivo no válido", + "invalidImageFormat": "Selecciona un archivo de imagen (PNG, JPG o WebP)", + "fileTooLarge": "Archivo demasiado grande", + "imageTooLargeDescription": "La imagen debe ocupar menos de 5 MB", + "avatarRemoved": "Avatar eliminado", + "avatarRemovedDescription": "La imagen del avatar se ha eliminado correctamente.", + "avatarRemoveFailed": "Error al eliminar el avatar", + "avatarUploadFailed": "Error al subir el avatar", + "avatarUploadFailedFallback": "Error al subir el avatar", + "effectsUpdateFailed": "Error al actualizar los efectos", + "effectsUpdateFailedFallback": "Error al guardar la cadena de efectos", + "voiceUpdated": "Voz actualizada", + "voiceUpdatedDescription": "\"{{name}}\" se ha actualizado correctamente.", + "noVoiceSelected": "Ninguna voz seleccionada", + "noVoiceSelectedDescription": "Selecciona una voz integrada.", + "profileCreated": "Perfil creado", + "profileCreatedBuiltin": "\"{{name}}\" se ha creado con una voz integrada.", + "profileCreatedSample": "\"{{name}}\" se ha creado con una muestra.", + "sampleRequired": "Muestra de audio obligatoria", + "sampleRequiredDescription": "Proporciona una muestra de audio para crear el perfil de voz.", + "referenceTextRequired": "Texto de referencia obligatorio", + "referenceTextRequiredDescription": "Proporciona el texto de referencia para la muestra de audio.", + "invalidAudio": "Archivo de audio no válido", + "invalidAudioDescription": "La duración del audio es {{duration}}, pero el máximo es {{max}}.", + "validationError": "Error de validación", + "rollbackFailed": "Error al revertir", + "rollbackFailedDescription": "El perfil creado no se pudo eliminar tras el fallo de subida de la muestra.", + "profileRolledBack": "El perfil se ha revertido.", + "sampleFailed": "Error al añadir la muestra", + "sampleFailedDescription": "Error al añadir la muestra.", + "sampleFailedRolledBack": "Error al añadir la muestra. El perfil se ha revertido.", + "saveFailed": "Error al guardar el perfil" + } + }, + "audioSample": { + "chooseFile": "Elegir archivo", + "uploadHint": "Haz clic para elegir un archivo o arrástralo y suéltalo. Duración máxima: 30 segundos.", + "fileUploaded": "Archivo subido", + "fileLabel": "Archivo: {{name}}", + "play": "Reproducir", + "pause": "Pausar", + "transcribe": "Transcribir", + "transcribing": "Transcribiendo…", + "remove": "Quitar", + "startRecording": "Iniciar grabación", + "recordHint": "Haz clic para empezar a grabar. Duración máxima: 30 segundos.", + "stopRecording": "Detener grabación", + "remaining": "{{time}} restantes", + "recordingComplete": "Grabación completada", + "recordAgain": "Grabar de nuevo", + "startCapture": "Iniciar captura", + "systemHint": "Captura el audio de tu sistema. Duración máxima: 30 segundos.", + "stopCapture": "Detener captura", + "captureComplete": "Captura completada", + "captureAgain": "Capturar de nuevo" + }, + "sampleList": { + "loading": "Cargando muestras…", + "empty": { + "title": "Aún no hay muestras", + "hint": "Añade tu primera muestra de audio para empezar" + }, + "editing": "Editando transcripción", + "placeholder": "Introduce el texto de referencia…", + "saving": "Guardando…", + "editTranscription": "Editar transcripción", + "deleteSample": "Eliminar muestra", + "addSample": "Añadir muestra", + "note": "Nota: una sola muestra de 30 segundos es el punto ideal. La calidad puede disminuir con varias muestras. En una futura actualización las muestras podrían ser intercambiables y etiquetarse para distintos estilos de la misma voz.", + "deleteDialog": { + "title": "Eliminar muestra", + "description": "¿Seguro que quieres eliminar esta muestra de audio? Esta acción no se puede deshacer.", + "deleting": "Eliminando…" + }, + "player": { + "play": "Reproducir muestra", + "pause": "Pausar muestra", + "stop": "Detener", + "stopAria": "Detener reproducción", + "position": "Posición de reproducción de la muestra", + "positionValue": "{{current}} de {{total}}" + }, + "toast": { + "invalidText": "Texto no válido", + "invalidTextDescription": "El texto de referencia no puede estar vacío.", + "updated": "Muestra actualizada", + "updatedDescription": "El texto de referencia se ha actualizado correctamente.", + "updateFailed": "Error al actualizar", + "updateFailedFallback": "Error al actualizar la muestra" + } + }, + "profiles": { + "card": { + "noDescription": "Sin descripción", + "designed": "diseñada", + "export": "Exportar perfil", + "edit": "Editar perfil", + "delete": "Eliminar perfil", + "selectLabel": "{{name}}, {{language}}. Seleccionar como voz para la generación.", + "selectLabelSelected": "{{name}}, {{language}}. Seleccionada como voz para la generación." + }, + "list": { + "errorLoading": "Error al cargar los perfiles: {{message}}", + "empty": "Aún no hay perfiles de voz. Crea tu primer perfil para empezar.", + "createVoice": "Crear voz", + "unsupportedNote": "Solo se pueden seleccionar los perfiles de voz compatibles con el modelo actual." + }, + "deleteDialog": { + "title": "Eliminar perfil", + "body": "¿Seguro que quieres eliminar \"{{name}}\"? Esta acción no se puede deshacer.", + "deleting": "Eliminando…" + } + }, + "effects": { + "title": "Efectos", + "newPreset": "Nuevo preajuste", + "noDescription": "Sin descripción", + "placeholder": "Selecciona un preajuste o crea uno nuevo", + "effectCount_one": "{{count}} efecto", + "effectCount_other": "{{count}} efectos", + "sections": { + "builtin": "Integrados", + "custom": "Personalizados", + "new": "Nuevo" + }, + "badge": { + "builtin": "integrado" + }, + "unsaved": { + "title": "Preajuste sin guardar", + "hint": "Configura los efectos en el panel de la derecha." + }, + "detail": { + "newTitle": "Nuevo preajuste", + "editTitle": "Editar preajuste", + "savePreset": "Guardar preajuste", + "saveAsCustom": "Guardar como personalizado", + "saving": "Guardando…", + "deleting": "Eliminando…" + }, + "fields": { + "name": "Nombre", + "namePlaceholder": "Mi preajuste…", + "description": "Descripción", + "descriptionPlaceholder": "Describe qué hace este preajuste…" + }, + "preview": { + "label": "Vista previa", + "button": "Vista previa", + "processing": "Procesando…", + "hint": "La vista previa aplica los efectos a la versión limpia sin guardar." + }, + "saveAs": { + "title": "Guardar como preajuste personalizado", + "description": "Crea un nuevo preajuste personalizado basado en la cadena de efectos actual.", + "suggestedName": "{{name}} (copia)" + }, + "toast": { + "saved": "Preajuste guardado", + "createdDescription": "\"{{name}}\" se ha creado.", + "updated": "Preajuste actualizado", + "deleted": "Preajuste eliminado", + "saveFailed": "Error al guardar", + "deleteFailed": "Error al eliminar", + "previewFailed": "Error en la vista previa", + "nameRequired": "Nombre obligatorio" + }, + "chain": { + "loadPreset": "Cargar preajuste…", + "addEffect": "Añadir efecto…", + "clear": "Limpiar", + "enable": "Activar", + "disable": "Desactivar", + "remove": "Quitar" + }, + "types": { + "chorus": { + "label": "Coro / Flanger", + "params": { + "rate_hz": "Velocidad del LFO (Hz)", + "depth": "Profundidad de modulación", + "feedback": "Cantidad de realimentación", + "centre_delay_ms": "Retardo central (ms)", + "mix": "Mezcla húmedo/seco" + } + }, + "reverb": { + "label": "Reverberación", + "params": { + "room_size": "Tamaño de sala", + "damping": "Amortiguación de altas frecuencias", + "wet_level": "Nivel húmedo", + "dry_level": "Nivel seco", + "width": "Anchura estéreo" + } + }, + "delay": { + "label": "Retardo", + "params": { + "delay_seconds": "Tiempo de retardo (segundos)", + "feedback": "Cantidad de realimentación", + "mix": "Mezcla húmedo/seco" + } + }, + "compressor": { + "label": "Compresor", + "params": { + "threshold_db": "Umbral (dB)", + "ratio": "Relación de compresión", + "attack_ms": "Tiempo de ataque (ms)", + "release_ms": "Tiempo de liberación (ms)" + } + }, + "gain": { + "label": "Ganancia", + "params": { + "gain_db": "Ganancia (dB)" + } + }, + "highpass": { + "label": "Filtro paso alto", + "params": { + "cutoff_frequency_hz": "Frecuencia de corte (Hz)" + } + }, + "lowpass": { + "label": "Filtro paso bajo", + "params": { + "cutoff_frequency_hz": "Frecuencia de corte (Hz)" + } + }, + "pitch_shift": { + "label": "Cambio de tono", + "params": { + "semitones": "Semitonos a desplazar" + } + } + }, + "builtinPresets": { + "Robotic": { + "name": "Robótica", + "description": "Voz robótica metálica (flanger con LFO lento y realimentación alta)" + }, + "Radio": { + "name": "Radio", + "description": "Voz fina de radio AM con filtrado paso banda y compresión ligera" + }, + "Echo Chamber": { + "name": "Cámara de eco", + "description": "Reverberación amplia con eco de cola" + }, + "Deep Voice": { + "name": "Voz grave", + "description": "Tono más grave con calidez añadida" + } + } + }, + "stories": { + "title": "Historias", + "newStory": "Nueva historia", + "loading": "Cargando historias…", + "searchPlaceholder": "Buscar historias…", + "empty": { + "title": "Aún no hay historias", + "hint": "Crea tu primera historia para empezar", + "noMatches": "Ninguna historia coincide con \"{{query}}\"" + }, + "row": { + "itemCount_one": "{{count}} elemento", + "itemCount_other": "{{count}} elementos", + "ariaLabel": "Historia {{name}}, {{count}} elementos, {{updated}}", + "actionsLabel": "Acciones para {{name}}" + }, + "createDialog": { + "title": "Crear nueva historia", + "description": "Crea una nueva historia para organizar tus generaciones de voz en conversaciones.", + "action": "Crear", + "creating": "Creando…" + }, + "editDialog": { + "title": "Editar historia", + "description": "Actualiza el nombre y la descripción de la historia.", + "saving": "Guardando…" + }, + "deleteDialog": { + "title": "¿Estás seguro?", + "description": "Esto eliminará permanentemente la historia y todos sus elementos. Esta acción no se puede deshacer.", + "deleting": "Eliminando…" + }, + "fields": { + "name": "Nombre", + "namePlaceholder": "Mi historia", + "descriptionLabel": "Descripción (opcional)", + "descriptionPlaceholder": "Una conversación entre…" + }, + "toast": { + "nameRequired": "Nombre obligatorio", + "nameRequiredDescription": "Introduce un nombre para la historia", + "created": "Historia creada", + "createdDescription": "\"{{name}}\" se ha creado", + "createFailed": "Error al crear la historia", + "updateFailed": "Error al actualizar la historia", + "deleteFailed": "Error al eliminar la historia" + } + }, + "storyContent": { + "selectStory": { + "title": "Selecciona una historia", + "hint": "Elige una historia de la lista para ver su contenido" + }, + "loading": "Cargando historia…", + "notFound": { + "title": "Historia no encontrada", + "hint": "No se pudo cargar la historia seleccionada" + }, + "generatingCount_one": "Generando {{count}} audio", + "generatingCount_other": "Generando {{count}} audios", + "add": "Añadir", + "searchPlaceholder": "Buscar por nombre o transcripción…", + "searchNoMatches": "No se encontraron generaciones coincidentes", + "searchNoAvailable": "No hay generaciones disponibles", + "exportAudio": "Exportar audio", + "empty": { + "title": "No hay elementos en esta historia", + "hint": "Genera voz con el cuadro de abajo para añadir elementos" + }, + "itemActions": { + "playFromHere": "Reproducir desde aquí", + "regenerate": "Regenerar", + "removeFromStory": "Quitar de la historia" + }, + "importAudio": "Importar audio…", + "importing": "Importando…", + "dropToImport": "Suelta el audio para importar", + "toast": { + "removeFailed": "Error al quitar el elemento", + "reorderFailed": "Error al reordenar los elementos", + "exportFailed": "Error al exportar el audio", + "addFailed": "Error al añadir la generación", + "regenerateFailed": "Error al regenerar", + "importFailed": "Error al importar el audio" + } + }, + "history": { + "empty": "Aún no hay generaciones de voz…", + "actions": { + "menu": "Acciones", + "play": "Reproducir", + "exportAudio": "Exportar audio", + "exportPackage": "Exportar paquete", + "applyEffects": "Aplicar efectos", + "regenerate": "Regenerar" + }, + "deleteDialog": { + "title": "Eliminar generación", + "body": "¿Seguro que quieres eliminar esta generación de \"{{name}}\"? Esta acción no se puede deshacer.", + "deleting": "Eliminando…" + }, + "clearFailedDialog": { + "title": "Borrar generaciones fallidas", + "body_one": "Esto eliminará permanentemente {{count}} generación fallida de tu historial. No se puede deshacer.", + "body_other": "Esto eliminará permanentemente {{count}} generaciones fallidas de tu historial. No se puede deshacer.", + "clearing": "Borrando…", + "clearAll": "Borrar todo" + }, + "importDialog": { + "title": "Importar generación", + "body": "Importa la generación de \"{{name}}\". Se añadirá a tu historial.", + "importing": "Importando…", + "action": "Importar" + }, + "effectsDialog": { + "title": "Aplicar efectos", + "body": "Configura los efectos de posprocesado que se aplicarán a esta generación. Se creará una nueva versión.", + "sourceLabel": "Origen", + "sourcePlaceholder": "Selecciona la versión de origen", + "apply": "Aplicar", + "applying": "Aplicando…" + } + }, + "generation": { + "placeholder": { + "storyWithEffects": "Genera voz para \"{{name}}\"… (escribe / para efectos)", + "story": "Genera voz para \"{{name}}\"…", + "profile": "Genera voz con {{name}}…", + "effectsHint": "Escribe / para efectos como [laugh], [sigh]…", + "selectVoice": "Selecciona un perfil de voz arriba…" + }, + "button": { + "generate": "Generar voz", + "generating": "Generando…", + "selectFirst": "Selecciona primero un perfil de voz" + }, + "instruct": { + "show": "Mostrar instrucciones de interpretación", + "hide": "Ocultar instrucciones de interpretación", + "tooltip": "Instrucciones de interpretación (tono, emoción, ritmo)", + "placeholder": "Instrucciones de interpretación — p. ej. Habla despacio y con calidez, Con autoridad y claridad…" + }, + "voiceSelector": { + "placeholder": "Selecciona una voz…" + }, + "effects": { + "none": "Sin efectos", + "profileDefault": "Predeterminado del perfil" + }, + "compose": { + "tooltip": "Redactar", + "ariaLabel": "Redactar una frase en personaje", + "failedTitle": "Error al redactar", + "failedDescription": "No se pudo generar texto a partir de esta personalidad." + }, + "persona": { + "tooltipActive": "Hablando en personaje", + "tooltipInactive": "Hablar en personaje", + "ariaLabelActive": "Hablando en personaje", + "ariaLabelInactive": "Hablar en personaje" + } + }, + "main": { + "importVoice": "Importar voz", + "createVoice": "Crear voz", + "import": { + "invalidTitle": "Tipo de archivo no válido", + "invalidDescription": "Selecciona un archivo .voicebox.zip válido", + "successTitle": "Perfil importado", + "successDescription": "Perfil de voz importado correctamente", + "failedTitle": "Error al importar el perfil", + "dialogTitle": "Importar perfil", + "dialogDescription": "Importa el perfil de \"{{name}}\". Se creará un nuevo perfil con todas las muestras.", + "importing": "Importando…", + "action": "Importar" + } + }, + "settings": { + "tabs": { + "general": "General", + "generation": "Generación", + "captures": "Capturas", + "mcp": "MCP", + "gpu": "GPU", + "logs": "Registros", + "changelog": "Cambios", + "about": "Acerca de" + }, + "language": { + "label": "Idioma", + "description": "Elige el idioma de la interfaz de Voicebox." + }, + "theme": { + "label": "Tema", + "description": "Adáptalo a tu sistema o elige una apariencia fija clara u oscura.", + "options": { + "system": "Sistema", + "light": "Claro", + "dark": "Oscuro" + } + }, + "general": { + "docs": { + "title": "Leer la documentación" + }, + "discord": { + "title": "Únete al Discord", + "subtitle": "Consigue ayuda y comparte voces" + }, + "serverUrl": { + "title": "URL del servidor", + "description": "La dirección de tu servidor backend de Voicebox.", + "invalidUrl": "Introduce una URL válida", + "updatedTitle": "URL del servidor actualizada", + "updatedDescription": "Conectado a {{url}}" + }, + "keepServerRunning": { + "title": "Mantener el servidor en marcha al cerrar la app", + "description": "El servidor seguirá ejecutándose en segundo plano después de cerrar la app.", + "failedTitle": "Error al actualizar el ajuste", + "failedDescription": "No se pudo sincronizar el ajuste con el backend.", + "updatedTitle": "Ajuste actualizado", + "runningDescription": "El servidor seguirá en marcha al cerrar la app", + "stoppedDescription": "El servidor se detendrá al cerrar la app" + }, + "networkAccess": { + "title": "Permitir acceso de red", + "description": "Hace que el servidor sea accesible desde otros dispositivos de tu red. Reinicia la app después de cambiarlo.", + "updatedTitle": "Ajuste actualizado", + "enabled": "Acceso de red habilitado. Reinicia la app para aplicarlo.", + "disabled": "Acceso de red deshabilitado. Reinicia la app para aplicarlo." + }, + "connection": { + "connecting": "Conectando", + "offline": "Sin conexión", + "online": "En línea" + }, + "updates": { + "title": "Actualizaciones de la app", + "devSuffix": " (dev)", + "devMode": { + "title": "Modo de desarrollo", + "description": "Las actualizaciones automáticas están deshabilitadas en el modo de desarrollo." + }, + "check": { + "title": "Buscar actualizaciones", + "available": "Versión {{version}} disponible", + "checking": "Comprobando…", + "upToDate": "Estás al día", + "button": "Comprobar" + }, + "error": "Error de actualización", + "download": { + "title": "Actualizar a {{version}}", + "description": "Descarga e instala la última versión.", + "button": "Descargar" + }, + "downloading": "Descargando actualización…", + "ready": { + "title": "Actualización lista para instalar", + "description": "Se ha descargado la versión {{version}}. Reinicia para completar.", + "button": "Reiniciar ahora" + } + }, + "api": { + "title": "Acceso a la API", + "description": "Integra Voicebox en tu flujo de trabajo mediante la API REST en {{url}}", + "viewReference": "Ver la referencia completa de la API", + "endpoints": { + "generate": "Generar voz", + "health": "Estado del servidor", + "profiles": "Listar voces", + "history": "Generaciones anteriores" + } + } + }, + "generation": { + "title": "Generación", + "description": "Controles para la generación de texto largo. Estos ajustes se aplican a todos los motores.", + "chunkLimit": { + "title": "Límite de fragmentación automática", + "description": "El texto largo se divide en fragmentos en los límites de las frases. Valores más bajos pueden mejorar la calidad en salidas largas.", + "value": "{{chars}} caracteres" + }, + "crossfade": { + "title": "Fundido cruzado entre fragmentos", + "description": "Mezcla el audio entre fragmentos para suavizar las transiciones. Pon 0 para un corte seco.", + "cut": "Corte", + "ms": "{{ms}} ms" + }, + "normalize": { + "title": "Normalizar audio", + "description": "Ajusta el volumen de salida a un nivel uniforme entre generaciones." + }, + "autoplay": { + "title": "Reproducción automática al generar", + "description": "Reproduce automáticamente el audio cuando se completa una generación." + }, + "folder": { + "title": "Carpeta de generaciones", + "description": "Dónde se guardan en disco los archivos de audio generados.", + "open": "Abrir" + }, + "sidebar": { + "aboutTitle": "Acerca de la generación de voz", + "aboutBody": "Clona una voz a partir de una muestra corta y luego genera voz con cualquier voz en cualquier idioma. Lleva TTS a agentes de IA, videojuegos, pódcasts o narración de formato largo.", + "differencesTitle": "Qué la diferencia", + "clone": { + "title": "Clona cualquier voz en segundos.", + "body": "Bastan unos segundos de audio de referencia. Compatible con varias muestras para mayor calidad cuando lo necesites." + }, + "engines": { + "title": "Siete motores, 23 idiomas.", + "body": "Elige el equilibrio que encaje: calidad, velocidad o cobertura multilingüe." + }, + "agentReady": { + "title": "Listo para agentes.", + "body": "API REST con control por perfil: dale a cualquier IA una voz que hayas clonado." + } + } + }, + "captures": { + "dictation": { + "title": "Dictado", + "description": "Captura desde cualquier parte de tu equipo con un atajo global.", + "globalShortcut": { + "title": "Atajo global", + "description": "Mantén pulsado el atajo para grabar desde cualquier parte de tu equipo. Suelta para transcribir." + }, + "pushToTalk": { + "title": "Atajo de pulsar para hablar", + "description": "Mantén estas teclas pulsadas en cualquier parte de tu sistema para grabar. Suelta para detener y transcribir.", + "change": "Cambiar" + }, + "toggle": { + "title": "Atajo de alternancia", + "description": "Pulsa una vez para iniciar una grabación manos libres. Pulsa de nuevo para detener. Normalmente, pulsar para hablar más Espacio.", + "change": "Cambiar" + }, + "chordPicker": { + "pttTitle": "Definir el atajo de pulsar para hablar", + "pttDescription": "Mantén pulsadas las teclas que quieras usar, luego suéltalas y haz clic en Guardar. La insignia del modificador derecho muestra si una tecla es la variante izquierda o derecha.", + "toggleTitle": "Definir el atajo de alternancia", + "toggleDescription": "Mantén pulsadas las teclas que quieras usar, luego suéltalas y haz clic en Guardar. Elige algo distinto de tu combinación de pulsar para hablar." + }, + "preview": { + "title": "Vista previa", + "description": "Lo que aparece en pantalla mientras mantienes pulsado el atajo." + }, + "copyToClipboard": { + "title": "Copiar la transcripción al portapapeles", + "description": "La transcripción ya limpia llega a tu portapapeles cuando termina la captura." + }, + "autoPaste": { + "title": "Pegar automáticamente en el campo de texto activo", + "description": "Si hay un campo de texto activo en otra app, pega directamente en él. Voicebox guarda y restaura lo que hubiera en tu portapapeles." + } + }, + "transcription": { + "title": "Transcripción", + "description": "Elige qué modelo de voz a texto se ejecuta en tus capturas.", + "model": { + "title": "Modelo de transcripción", + "description": "Whisper viene incluido con Voicebox y se ejecuta enteramente en tu equipo.", + "base": "Whisper Base · 74M · {{tail}}", + "small": "Whisper Small · 244M · {{tail}}", + "medium": "Whisper Medium · 769M · {{tail}}", + "large": "Whisper Large · 1.5B · {{tail}}", + "turbo": "Whisper Turbo · Pruned Large v3 · {{tail}}", + "tail": { + "fast": "Rápido", + "balanced": "Equilibrado", + "higher": "Mayor precisión", + "best": "Mejor precisión", + "nearBest": "Casi la mejor, rápido" + } + }, + "language": { + "title": "Idioma", + "description": "La detección automática funciona para la mayoría de las capturas. Fíjalo si siempre hablas el mismo idioma.", + "auto": "Detección automática", + "en": "Inglés", + "es": "Español", + "fr": "Francés", + "de": "Alemán", + "ja": "Japonés", + "zh": "Chino", + "hi": "Hindi" + }, + "archive": { + "title": "Archivar audio", + "description": "Conserva la grabación original junto a cada transcripción." + } + }, + "refinement": { + "title": "Refinamiento", + "description": "Opcionalmente, ejecuta un LLM local sobre las transcripciones para limpiar muletillas, puntuación y autocorrecciones.", + "auto": { + "title": "Refinar transcripciones automáticamente", + "description": "Se ejecuta después de cada captura. Aún puedes alternar entre en bruto y refinada en la pestaña Capturas." + }, + "model": { + "title": "Modelo de refinamiento", + "description": "Los modelos más grandes son más lentos, pero manejan mejor las autocorrecciones sutiles y el vocabulario técnico.", + "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", + "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", + "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}", + "tail": { + "veryFast": "Muy rápido", + "fast": "Rápido", + "fullQuality": "Calidad completa" + } + }, + "smartCleanup": { + "title": "Limpieza inteligente", + "description": "Elimina muletillas (em, eh, o sea), restaura la puntuación y corrige las mayúsculas sin reformular." + }, + "selfCorrection": { + "title": "Eliminar autocorrecciones", + "description": "Cuando cambias de idea a mitad de frase (\"en realidad, no...\", \"espera, quería decir...\"), descarta la parte retractada y conserva la intención final." + }, + "preserveTechnical": { + "title": "Conservar términos técnicos", + "description": "Mantén los identificadores de código, nombres de comandos y siglas exactamente como se dicen. Actívalo cuando dictes en un prompt de código." + } + }, + "playback": { + "title": "Reproducción", + "description": "Voz predeterminada para la acción \"Reproducir como\" en la pestaña Capturas.", + "defaultVoice": { + "title": "Voz predeterminada", + "description": "Se usa cuando haces clic en Reproducir como sin elegir antes una voz. Puedes cambiarla en cada captura.", + "noClonedVoices": "Aún no hay voces clonadas", + "noneSelected": "Ninguna seleccionada", + "clonedVoices": "Voces clonadas" + } + }, + "storage": { + "title": "Almacenamiento", + "description": "Las capturas se guardan como archivos emparejados de audio y transcripción en tu directorio de datos de Voicebox.", + "retention": { + "title": "Retención", + "description": "Cuánto tiempo conservar las capturas. Se aplica tanto al audio como a las transcripciones.", + "forever": "Conservar siempre", + "d90": "90 días", + "d30": "30 días", + "d7": "7 días" + }, + "folder": { + "title": "Carpeta de capturas", + "description": "Dónde se almacenan en disco el audio y las transcripciones de las capturas.", + "open": "Abrir" + } + }, + "sidebar": { + "aboutTitle": "Acerca de Capturas", + "aboutBody": "Mantén pulsado un atajo en cualquier parte de tu equipo, habla, y Voicebox convierte tu voz en texto. Reprodúcelo con cualquier voz clonada, pégalo en cualquier app o canalízalo a tu agente de programación.", + "differencesTitle": "Qué lo diferencia", + "local": { + "title": "Totalmente local.", + "body": "Whisper y el LLM de refinamiento se ejecutan en tu hardware. Sin nube, sin cuentas; tu voz nunca sale del equipo." + }, + "playAs": { + "title": "Reproduce con cualquier voz.", + "body": "Las transcripciones se pueden leer con cualquier perfil que hayas clonado." + }, + "crossPlatform": { + "title": "Multiplataforma.", + "body": "El mismo atajo, el mismo flujo en macOS, Windows y Linux." + }, + "windowsCaveat": { + "title": "Aviso en Windows", + "body": "El atajo no se activará mientras Voicebox o cualquier app ejecutada como administrador esté en primer plano. Estamos en ello." + } + } + }, + "mcp": { + "install": { + "title": "Instalar en tu agente", + "description": "Voicebox expone un servidor MCP local siempre que la app está abierta. Pega uno de estos fragmentos en la configuración MCP de tu agente.", + "http": { + "title": "HTTP (recomendado)", + "description": "Para clientes que hablan MCP por HTTP: Claude Code, Cursor, Windsurf, VS Code." + }, + "claudeCode": { + "title": "Comando de una línea para Claude Code", + "description": "Se registra mediante la CLI de Claude Code." + }, + "stdio": { + "title": "Stdio (alternativa)", + "description": "Para clientes que solo generan procesos stdio. El binario adaptador viene con la app." + }, + "copy": "Copiar", + "copied": "Copiado" + }, + "defaultVoice": { + "title": "Voz predeterminada", + "description": "Se usa cuando un agente llama a voicebox.speak sin un perfil específico y no tiene una vinculación por cliente.", + "label": "Voz de reproducción predeterminada", + "labelHint": "Compartida con el desplegable 'Reproducir como voz' de la pestaña Capturas: una voz predeterminada para la reproducción pasiva.", + "none": "(ninguna)" + }, + "bindings": { + "title": "Voz por agente", + "description": "Vincula agentes concretos a voces concretas para saber quién habla sin mirar. El agente se identifica mediante la cabecera X-Voicebox-Client-Id (o la variable de entorno VOICEBOX_CLIENT_ID para stdio).", + "empty": "Aún no hay vinculaciones. Añade una abajo y luego configura tu cliente MCP para que envíe el X-Voicebox-Client-Id correspondiente.", + "lastSeen": "visto por última vez {{when}}", + "lastSeenTitle": "Visto por última vez {{when}}", + "neverConnected": "nunca conectado", + "defaultOption": "(predeterminado)", + "removeAria": "Quitar vinculación de {{client}}", + "add": { + "title": "Añadir una vinculación", + "clientIdPlaceholder": "id de cliente (p. ej. claude-code)", + "labelPlaceholder": "etiqueta (opcional)", + "action": "Añadir vinculación" + } + }, + "sidebar": { + "aboutTitle": "Acerca de MCP", + "aboutBody": "El Model Context Protocol permite que tu agente de programación con IA —Claude Code, Cursor, Windsurf— llame a las herramientas de Voicebox. Habla con una voz clonada, transcribe audio, explora capturas.", + "toolsTitle": "Herramientas disponibles", + "tools": { + "speak": "Pronuncia texto con un perfil de voz.", + "transcribe": "Whisper STT sobre un clip.", + "listCaptures": "Dictados / grabaciones recientes.", + "listProfiles": "Perfiles de voz disponibles." + }, + "postSpeak": "También expuesto como POST /speak para scripts de shell, ACP, A2A." + } + }, + "gpu": { + "cpuOnly": "Solo CPU", + "vramUsed": "{{mb}} MB de VRAM", + "noAcceleration": "No se detectó aceleración por GPU", + "active": "Activa", + "cuda": { + "title": "Backend CUDA", + "description": "Aceleración por GPU NVIDIA mediante un backend CUDA descargable.", + "downloading": "Descargando el backend CUDA…", + "downloadingShort": "Descargando…", + "updating": "Actualizando…", + "activeTitle": "Backend CUDA activo" + }, + "restart": { + "ready": "Servidor reiniciado correctamente", + "waiting": "Reiniciando el servidor…", + "stopping": "Deteniendo el servidor…" + }, + "download": { + "title": "Descargar el backend CUDA", + "description": "Descarga de ~2.4 GB. Requiere una GPU NVIDIA compatible con CUDA.", + "button": "Descargar" + }, + "switchToCuda": { + "title": "Cambiar al backend CUDA", + "description": "El backend CUDA está descargado y listo. Reinicia para habilitarlo.", + "button": "Reiniciar" + }, + "switchToCpu": { + "title": "Cambiar al backend de CPU", + "description": "Deshabilita la aceleración por GPU. Puedes volver a descargar el backend de GPU más tarde.", + "button": "Cambiar" + }, + "remove": { + "title": "Quitar el backend CUDA", + "description": "Elimina el binario CUDA descargado para liberar espacio en disco.", + "button": "Quitar" + }, + "errors": { + "downloadFailed": "Error de descarga", + "downloadStart": "Error al iniciar la descarga", + "restartFailed": "Error al reiniciar", + "switchCpu": "Error al cambiar a CPU", + "deleteCuda": "Error al eliminar el backend CUDA", + "deleteRocm": "Error al eliminar el backend ROCm" + }, + "footer": "Voicebox detecta y usa automáticamente la mejor GPU disponible en tu sistema. En Macs con Apple Silicon, el backend MLX se ejecuta de forma nativa en el Neural Engine y la GPU mediante Metal Performance Shaders (MPS), sin configuración adicional. En Windows, puedes descargar backends opcionales CUDA (NVIDIA) o ROCm (AMD) para inferencia acelerada por hardware. Intel XPU y DirectML también son compatibles cuando están disponibles a través de PyTorch. Cuando no se detecta ninguna GPU, Voicebox recurre a la CPU: todos los motores siguen funcionando, solo que más despacio.", + "activeBackend": { + "description": "La aceleración por GPU está habilitada actualmente." + }, + "rocm": { + "title": "Backend AMD ROCm", + "activeTitle": "Backend ROCm activo", + "description": "Aceleración por GPU AMD mediante un backend ROCm descargable.", + "downloading": "Descargando el backend ROCm…", + "downloadingShort": "Descargando…", + "updating": "Actualizando…" + }, + "downloadRocm": { + "title": "Descargar el backend AMD ROCm", + "description": "Descarga de ~2-3 GB. Requiere una GPU AMD Radeon compatible con ROCm.", + "button": "Descargar" + }, + "switchToRocm": { + "title": "Cambiar al backend ROCm", + "description": "El backend ROCm está descargado y listo. Reinicia para habilitarlo.", + "button": "Reiniciar" + }, + "removeRocm": { + "title": "Quitar el backend ROCm", + "description": "Elimina el binario ROCm descargado para liberar espacio en disco.", + "button": "Quitar" + } + }, + "logs": { + "title": "Registros del servidor", + "lineCount_one": "{{count}} línea", + "lineCount_other": "{{count}} líneas", + "scrollToBottom": "Desplazar al final", + "clear": "Limpiar", + "empty": "Aún no hay salida de registro.", + "devHint": "Los registros del servidor solo se capturan cuando la app gestiona el proceso del servidor (compilaciones de producción)." + }, + "changelog": { + "devBadge": "dev", + "showLess": "Mostrar menos", + "showMore": "Mostrar más" + }, + "about": { + "tagline": "El estudio de síntesis de voz de código abierto. Clona voces, genera voz, aplica efectos y crea apps con voz, todo ejecutándose localmente en tu equipo.", + "createdBy": "Creado por", + "buyCoffee": "Invítame a un café", + "license": "Con licencia MIT" + } + }, + "models": { + "title": "Modelos", + "subtitle": "Descarga y gestiona modelos de IA para la generación de voz y la transcripción", + "defaultName": "Modelo", + "unknownSize": "Tamaño desconocido", + "sections": { + "voiceGeneration": "Generación de voz", + "transcription": "Transcripción", + "languageModels": "Modelos de lenguaje" + }, + "status": { + "loaded": "Cargado" + }, + "storage": { + "location": "Ubicación de almacenamiento", + "open": "Abrir", + "change": "Cambiar", + "migrating": "Migrando…", + "reset": "Restablecer", + "pickerTitle": "Elegir la carpeta de almacenamiento de modelos" + }, + "progress": { + "connecting": "Conectando…", + "connectingHf": "Conectando con HuggingFace…" + }, + "problems": { + "title": "Problemas", + "clearAll": "Borrar todo", + "noDetails": "No hay detalles del error disponibles. Prueba a descargar de nuevo.", + "startedAt": "iniciado a las {{time}}" + }, + "detail": { + "loadingInfo": "Cargando información del modelo…", + "byAuthor": "por {{author}}", + "downloads": "Descargas", + "likes": "Me gusta", + "license": "Licencia", + "languagesCount": "{{count}} idiomas admitidos", + "languagesList": "Idiomas: {{list}}", + "onDisk": "{{size}} en disco" + }, + "actions": { + "download": "Descargar", + "retry": "Reintentar descarga", + "unload": "Liberar", + "unloading": "Liberando…", + "unloadFirst": "Libera el modelo de la memoria antes de eliminarlo", + "deleteModel": "Eliminar modelo" + }, + "deleteDialog": { + "title": "Eliminar modelo", + "body": "¿Seguro que quieres eliminar {{name}}?", + "sizeNote": "Esto liberará {{size}} de espacio en disco. Habrá que volver a descargar el modelo si quieres usarlo de nuevo.", + "deleting": "Eliminando…" + }, + "migrateDialog": { + "title": "¿Mover los modelos a la nueva ubicación?", + "description": "El servidor se apagará mientras se mueven los modelos a la nueva carpeta. Se reiniciará automáticamente cuando la migración termine.", + "action": "Mover modelos", + "preparing": "Preparando…", + "restartingServer": "Reiniciando el servidor…" + }, + "migrate": { + "title": "Moviendo modelos", + "offline": "El servidor está sin conexión mientras se mueven los modelos." + }, + "toast": { + "downloadFailed": "Error de descarga", + "cancelFailed": "Error al cancelar", + "cancelFailedDescription": "No se pudo cancelar la tarea de descarga.", + "deleted": "Modelo eliminado", + "deletedDescription": "{{name}} se ha eliminado correctamente.", + "deleteFailed": "Error al eliminar", + "unloaded": "Modelo liberado", + "unloadedDescription": "{{name}} se ha liberado de la memoria.", + "unloadFailed": "Error al liberar", + "openFolderFailed": "Error al abrir la carpeta del modelo", + "pickerFailed": "Error al abrir el selector de carpetas", + "resetToDefault": "Restablecido a la ubicación predeterminada. Reiniciando el servidor…", + "noModelsToMigrate": "No hay modelos que migrar", + "noModelsToMigrateDescription": "Descarga al menos un modelo antes de cambiar la ubicación de almacenamiento.", + "migrated": "Modelos movidos correctamente", + "migrationFailed": "Error de migración", + "migrationFailedGeneric": "Error al migrar los modelos", + "migrationConnectionLost": "Se perdió la conexión durante la migración" + } + } +} diff --git a/app/src/lib/utils/format.ts b/app/src/lib/utils/format.ts index 0d01a7fd..6297332a 100644 --- a/app/src/lib/utils/format.ts +++ b/app/src/lib/utils/format.ts @@ -1,5 +1,5 @@ import { formatDistance } from 'date-fns'; -import { ja, zhCN, zhTW, fr } from 'date-fns/locale'; +import { es, fr, ja, zhCN, zhTW } from 'date-fns/locale'; import i18n from '@/i18n'; export function formatDuration(seconds: number): string { @@ -10,6 +10,8 @@ export function formatDuration(seconds: number): string { function getDateLocale() { switch (i18n.language) { + case 'es': + return es; case 'ja': return ja; case 'zh-CN': From e6cf50c7f792dfb2527cdeb8d8f6aee790d9f5ca Mon Sep 17 00:00:00 2001 From: TedChang-Lim Date: Tue, 21 Jul 2026 07:19:39 +0900 Subject: [PATCH 6/9] feat(i18n): add Korean (ko) locale with 559 translation keys (#814) * feat(i18n): add Korean (ko) locale with 559 translation keys * fix(i18n): complete Korean translations for current UI --------- Co-authored-by: Jamie Pine --- app/src/i18n/index.ts | 3 + app/src/i18n/locales/ko/translation.json | 1269 ++++++++++++++++++++++ 2 files changed, 1272 insertions(+) create mode 100644 app/src/i18n/locales/ko/translation.json diff --git a/app/src/i18n/index.ts b/app/src/i18n/index.ts index 7d26f2ab..e50c0b80 100644 --- a/app/src/i18n/index.ts +++ b/app/src/i18n/index.ts @@ -6,6 +6,7 @@ import es from './locales/es/translation.json'; import fr from './locales/fr/translation.json'; import it from './locales/it/translation.json'; import ja from './locales/ja/translation.json'; +import ko from './locales/ko/translation.json'; import ptBR from './locales/pt-BR/translation.json'; import zhCN from './locales/zh-CN/translation.json'; import zhTW from './locales/zh-TW/translation.json'; @@ -15,6 +16,7 @@ export const SUPPORTED_LANGUAGES = [ { code: 'es', label: 'Español' }, { code: 'pt-BR', label: 'Português (Brasil)' }, { code: 'ja', label: '日本語' }, + { code: 'ko', label: '한국어' }, { code: 'zh-CN', label: '简体中文' }, { code: 'zh-TW', label: '繁體中文' }, { code: 'fr', label: 'Français' }, @@ -32,6 +34,7 @@ i18n es: { translation: es }, 'pt-BR': { translation: ptBR }, ja: { translation: ja }, + ko: { translation: ko }, 'zh-CN': { translation: zhCN }, 'zh-TW': { translation: zhTW }, fr: { translation: fr }, diff --git a/app/src/i18n/locales/ko/translation.json b/app/src/i18n/locales/ko/translation.json new file mode 100644 index 00000000..94aa26fb --- /dev/null +++ b/app/src/i18n/locales/ko/translation.json @@ -0,0 +1,1269 @@ +{ + "common": { + "cancel": "취소", + "save": "저장", + "delete": "삭제", + "edit": "편집", + "close": "닫기", + "confirm": "확인", + "loading": "로딩 중…", + "error": "오류", + "unknown": "알 수 없음", + "unknownError": "알 수 없는 오류" + }, + "nav": { + "generate": "생성", + "stories": "스토리", + "captures": "캡처", + "voices": "음성", + "effects": "효과", + "audio": "오디오", + "models": "모델", + "settings": "설정", + "updateBadge": "업데이트" + }, + "captures": { + "title": "캡처", + "beta": "베타", + "searchPlaceholder": "대본 검색…", + "snippetEmpty": "(대본 없음)", + "noTranscriptError": "아직 대본이 없는 캡처입니다", + "captureCardLabel": "캡처 · {{when}}", + "header": { + "modelSummary": "Whisper {{stt}} · Qwen3 · {{llm}}" + }, + "source": { + "dictation": "받아쓰기", + "recording": "녹음", + "file": "파일" + }, + "transcript": { + "refined": "정제됨", + "raw": "원본", + "refinedHint": "Qwen3 · {{model}}로 정제됨", + "rawHint": "Whisper {{model}}로 변환됨" + }, + "actions": { + "configure": "설정", + "import": "가져오기", + "importing": "업로드 중…", + "dictate": "받아쓰기", + "stop": "중지", + "copy": "복사", + "refine": "정제", + "reRefine": "재정제", + "export": "내보내기", + "exportDropdownLabel": "캡처 내보내기 형식", + "exportAudio": "오디오 (WAV)", + "exportTranscript": "대본 (TXT)", + "exportMarkdown": "마크다운 (MD)", + "delete": "삭제", + "playAs": "{{name}}로 재생", + "playAsFallback": "다른 음성으로 재생…", + "playAsGenerating": "생성 중…", + "playAsStop": "중지 · {{name}}", + "playAsStopFallback": "중지 · 음성", + "playAsDropdownLabel": "대본을 다음 음성으로 재생" + }, + "empty": { + "noMatches": "\"{{query}}\"와 일치하는 캡처가 없습니다", + "none": "아직 캡처가 없습니다.", + "loading": "캡처 로딩 중…", + "pickOne": "캡처를 선택하여 대본을 확인하세요.", + "holdToRecord": "길게 눌러 녹음", + "toggleHandsFree": "핸즈프리 전환", + "pressShortcut": "아무 기기에서나 단축키를 눌러 첫 번째 캡처를 시작하세요.", + "turnOnShortcut": "전역 단축키를 켜서 어디서든 받아쓰기를 시작하세요. 또는 위의 받아쓰기 버튼을 클릭하세요.", + "openSettings": "캡처 설정 열기" + }, + "deleteDialog": { + "title": "캡처 삭제", + "description": "이 캡처와 오디오, 대본이 영구적으로 삭제됩니다. 되돌릴 수 없습니다.", + "deleting": "삭제 중…" + }, + "toast": { + "deleteFailed": "삭제 실패", + "playAsFailed": "음성 재생 실패", + "noVoice": "음성 프로필 없음", + "noVoiceDescription": "Play as를 사용하려면 먼저 음성 프로필을 생성하세요.", + "transcriptCopied": "대본이 복사되었습니다", + "copyFailed": "복사 실패", + "exportSuccess": "{{path}}로 내보내기 완료", + "exportFailed": "내보내기 실패", + "exportEmpty": "내보낼 내용이 없습니다", + "shortcutNotArmed": "단축키가 설정되었지만 아직 준비되지 않았습니다", + "shortcutNotArmedDescription_one": "{{names}}를 아직 다운로드해야 합니다. 캡처 탭을 열어 시작하세요.", + "shortcutNotArmedDescription_other": "{{names}}를 아직 다운로드해야 합니다. 캡처 탭을 열어 시작하세요." + }, + "pill": { + "recording": "녹음 중", + "transcribing": "변환 중", + "refining": "정제 중", + "speaking": "말하는 중", + "completed": "완료", + "stopAria": "녹음 중지", + "errorFallback": "문제가 발생했습니다", + "errorCopyTooltip": "클릭하여 오류 복사" + }, + "chord": { + "capturing": "캡처 중…", + "pressShortcut": "단축키를 누르세요", + "noKeys": "아직 키가 없습니다", + "unsupported": "\"{{key}}\"은(는) 코드에서 지원되지 않습니다. 수정자 또는 문자 키를 시도해 보세요.", + "notSet": "설정되지 않음" + }, + "readiness": { + "title": "받아쓰기 전에 준비할 것들", + "subheading": "모든 준비가 완료될 때까지 단축키는 꺼져 있습니다.", + "downloadButton": "다운로드", + "downloading": "다운로드 중…", + "downloadingPercent": "다운로드 중… {{pct}}%", + "downloadStarted": "다운로드 시작됨", + "downloadStartedDescription": "{{name}} 다운로드 중입니다. 완료되면 단축키가 자동으로 활성화됩니다.", + "downloadFailed": "다운로드 실패", + "stt": { + "label": "{{name}} (음성-텍스트)", + "ready": "모델 다운로드 완료.", + "missing": "오디오 변환에 필요합니다", + "missingWithSize": "오디오 변환에 필요합니다 · {{size}}" + }, + "llm": { + "label": "{{name}} (정제)", + "ready": "모델 다운로드 완료.", + "missing": "붙여넣기 전에 원시 대본을 정리합니다", + "missingWithSize": "붙여넣기 전에 원시 대본을 정리합니다 · {{size}}" + }, + "inputMonitoring": { + "label": "입력 모니터링 권한", + "ready": "macOS가 Voicebox의 전역 단축키 감지를 허용합니다.", + "missing": "macOS에서 Voicebox의 전역 단축키 감지를 허용해야 합니다.", + "openSettings": "설정 열기" + }, + "accessibility": { + "label": "손쉬운 사용 권한", + "ready": "Voicebox가 다른 앱에 대본을 붙여넣을 수 있습니다.", + "missing": "대본을 포커스된 앱에 붙여넣는 데 필요합니다.", + "openSettings": "설정 열기" + } + }, + "permissions": { + "accessibility": { + "title": "자동 붙여넣기를 활성화하려면 손쉬운 사용 권한을 허용하세요", + "body": "Voicebox가 다른 앱에 대본을 붙여넣으려면 시스템 설정 → 개인정보 보호 및 보안 → 손쉬운 사용 권한이 필요합니다. 권한이 없어도 받아쓰기는 캡처 탭에 저장됩니다.", + "openSettings": "설정 열기", + "recheck": "활성화했습니다", + "rechecking": "확인 중…", + "stillMissing": "아직 감지되지 않았습니다. macOS에서는 일반적으로 권한을 켠 후 Voicebox를 종료하고 다시 열어야 합니다." + }, + "inputMonitoring": { + "title": "전역 단축키를 활성화하려면 입력 모니터링을 허용하세요", + "body": "Voicebox가 받아쓰기 단축키를 감지하려면 시스템 설정 → 개인정보 보호 및 보안 → 입력 모니터링 권한이 필요합니다. 스위치가 켜져 있지만 macOS에서 키 이벤트를 차단하고 있습니다.", + "openSettings": "설정 열기", + "recheck": "활성화했습니다", + "rechecking": "확인 중…", + "stillMissing": "아직 감지되지 않았습니다. macOS에서는 일반적으로 권한을 켠 후 Voicebox를 종료하고 다시 열어야 합니다." + } + } + }, + "voicesTab": { + "title": "음성", + "loading": "음성 로딩 중…", + "searchPlaceholder": "음성 검색…", + "newVoice": "새 음성", + "avatarAlt": "{{name}} 아바타", + "selectChannels": "채널 선택…", + "channelDefaultLabel": "{{name}} (기본값)", + "columns": { + "name": "이름", + "language": "언어", + "generations": "생성 횟수", + "samples": "샘플", + "effects": "효과", + "channels": "채널" + } + }, + "voiceInspector": { + "loading": "로딩 중…", + "defaultEffectsHint": "이 음성으로 새로 생성할 때 자동으로 적용됩니다.", + "fields": { + "description": "설명" + }, + "toast": { + "invalidImageFormat": "PNG, JPG 또는 WebP를 선택해 주세요", + "avatarUpdated": "아바타 업데이트됨", + "savedDescription": "\"{{name}}\" 저장됨." + } + }, + "audioChannels": { + "title": "오디오 채널", + "newChannel": "새 채널", + "loading": "로딩 중…", + "confirmDelete": "이 채널을 삭제할까요?", + "noVoicesAssigned": "할당된 음성이 없습니다", + "selectDevice": "기기 선택", + "addDevice": "기기 추가", + "addVoice": "음성 추가", + "defaultSuffix": "기본값", + "empty": { + "message": "아직 오디오 채널이 없습니다. 첫 번째 채널을 만들어 음성을 특정 기기로 라우팅하세요.", + "action": "채널 만들기" + }, + "labels": { + "outputDevices": "출력 기기", + "assignedVoices": "할당된 음성" + }, + "devices": { + "title": "사용 가능한 기기", + "defaultNote": "기본 채널은 시스템 기본 기기를 사용합니다", + "toggleHint": "기기를 클릭하여 선택한 채널에 추가하거나 제거하세요", + "selectHint": "채널을 선택하여 기기를 할당하세요", + "empty": "오디오 기기를 찾을 수 없습니다", + "requiresTauri": "오디오 기기 선택에는 Tauri가 필요합니다" + }, + "fields": { + "name": "채널 이름", + "namePlaceholder": "예: 가상 케이블, 방송" + }, + "createDialog": { + "title": "오디오 채널 만들기", + "description": "새 오디오 채널(버스)을 만들어 음성을 특정 출력 기기로 라우팅합니다.", + "action": "만들기" + }, + "editDialog": { + "title": "채널 편집", + "description": "채널 설정과 음성 할당을 업데이트합니다." + } + }, + "profileForm": { + "createTitle": "음성 만들기", + "editTitle": "음성 편집", + "createDescription": "오디오 샘플 또는 내장 음성으로 새 음성 프로필을 만듭니다.", + "editDescription": "음성 프로필 세부 정보를 업데이트하고 샘플을 관리합니다.", + "draftRestored": "임시 저장이 복원되었습니다", + "discard": "취소", + "source": { + "clone": "오디오에서 복제", + "builtin": "내장 음성" + }, + "builtin": { + "hint": "미리 제작된 음성을 선택하세요. 오디오 샘플이 필요하지 않습니다.", + "badge": "내장 음성", + "note": "이 프로필은 내장 음성을 사용합니다. 생성 후에는 음성을 변경할 수 없습니다." + }, + "sampleTabs": { + "upload": "업로드", + "record": "녹음", + "system": "시스템 오디오" + }, + "fields": { + "engine": "엔진", + "voice": "음성", + "name": "이름", + "namePlaceholder": "내 음성", + "descriptionLabel": "설명 (선택사항)", + "descriptionPlaceholder": "이 음성에 대해 설명해 주세요…", + "language": "언어", + "referenceText": "참조 텍스트", + "referenceTextPlaceholder": "오디오에서 말한 텍스트를 정확히 입력하세요…", + "defaultEngine": "기본 엔진", + "noPreference": "선택 안 함", + "defaultEngineHint": "프로필을 선택할 때 이 엔진이 자동으로 선택됩니다.", + "defaultEffects": "기본 효과", + "defaultEffectsHint": "이 음성으로 새로 생성할 때 자동으로 적용되는 효과입니다.", + "personalityLabel": "성격", + "personalityPlaceholder": "예: \"항상 해양 은유로 말하는 심술궂은 해적\"", + "personalityHint": "이 음성이 누구인지, 어떻게 말하는지를 정의합니다. 생성 페이지의 작성 버튼과 역할극 전환 토글을 제어합니다. 비워두면 둘 다 숨겨집니다." + }, + "avatar": { + "alt": "아바타 미리보기" + }, + "actions": { + "saving": "저장 중…", + "saveChanges": "변경사항 저장", + "createProfile": "프로필 만들기" + }, + "validation": { + "nameRequired": "이름은 필수입니다", + "referenceRequired": "샘플을 추가할 때는 참조 텍스트가 필요합니다", + "sampleRequired": "오디오 샘플이 필요합니다", + "referenceTextRequired": "참조 텍스트가 필요합니다", + "audioTooLong": "오디오가 너무 깁니다 ({{duration}}). 최대 길이는 {{max}}입니다.", + "audioFailed": "오디오 파일 검증에 실패했습니다. 다른 파일을 시도해 주세요." + }, + "toast": { + "recordingComplete": "녹음 완료", + "recordingCompleteDescription": "오디오가 성공적으로 녹음되었습니다.", + "recordingError": "녹음 오류", + "systemAudioCaptured": "시스템 오디오 캡처됨", + "systemAudioCapturedDescription": "오디오가 성공적으로 캡처되었습니다.", + "systemAudioError": "시스템 오디오 캡처 오류", + "transcribeFailed": "변환 실패", + "transcribeFailedFallback": "오디오 변환에 실패했습니다", + "noFile": "선택된 파일 없음", + "noFileDescription": "먼저 오디오 파일을 선택해 주세요.", + "invalidFile": "잘못된 파일 형식", + "invalidImageFormat": "이미지 파일을 선택해 주세요 (PNG, JPG, WebP)", + "fileTooLarge": "파일이 너무 큽니다", + "imageTooLargeDescription": "이미지는 5MB 미만이어야 합니다", + "avatarRemoved": "아바타 제거됨", + "avatarRemovedDescription": "아바타 이미지가 성공적으로 제거되었습니다.", + "avatarRemoveFailed": "아바타 제거 실패", + "avatarUploadFailed": "아바타 업로드 실패", + "avatarUploadFailedFallback": "아바타 업로드에 실패했습니다", + "effectsUpdateFailed": "효과 업데이트 실패", + "effectsUpdateFailedFallback": "효과 체인 저장에 실패했습니다", + "voiceUpdated": "음성 업데이트됨", + "voiceUpdatedDescription": "\"{{name}}\"이(가) 성공적으로 업데이트되었습니다.", + "noVoiceSelected": "선택된 음성 없음", + "noVoiceSelectedDescription": "내장 음성을 선택해 주세요.", + "profileCreated": "프로필 생성됨", + "profileCreatedBuiltin": "\"{{name}}\"이(가) 내장 음성으로 생성되었습니다.", + "profileCreatedSample": "\"{{name}}\"이(가) 샘플로 생성되었습니다.", + "sampleRequired": "오디오 샘플 필요", + "sampleRequiredDescription": "음성 프로필을 만들려면 오디오 샘플을 제공해 주세요.", + "referenceTextRequired": "참조 텍스트 필요", + "referenceTextRequiredDescription": "오디오 샘플의 참조 텍스트를 제공해 주세요.", + "invalidAudio": "잘못된 오디오 파일", + "invalidAudioDescription": "오디오 길이가 {{duration}}입니다. 최대 길이는 {{max}}입니다.", + "validationError": "검증 오류", + "rollbackFailed": "롤백 실패", + "rollbackFailedDescription": "샘플 업로드 실패 후 생성된 프로필을 제거할 수 없습니다.", + "profileRolledBack": "프로필이 롤백되었습니다.", + "sampleFailed": "샘플 추가 실패", + "sampleFailedDescription": "샘플 추가에 실패했습니다.", + "sampleFailedRolledBack": "샘플 추가에 실패했습니다. 프로필이 롤백되었습니다.", + "saveFailed": "프로필 저장에 실패했습니다" + } + }, + "audioSample": { + "chooseFile": "파일 선택", + "uploadHint": "클릭하여 파일을 선택하거나 드래그 앤 드롭하세요. 최대 길이: 30초.", + "fileUploaded": "파일 업로드됨", + "fileLabel": "파일: {{name}}", + "play": "재생", + "pause": "일시정지", + "transcribe": "변환", + "transcribing": "변환 중…", + "remove": "제거", + "startRecording": "녹음 시작", + "recordHint": "클릭하여 녹음을 시작하세요. 최대 길이: 30초.", + "stopRecording": "녹음 중지", + "remaining": "{{time}} 남음", + "recordingComplete": "녹음 완료", + "recordAgain": "다시 녹음", + "startCapture": "캡처 시작", + "systemHint": "시스템에서 오디오를 캡처합니다. 최대 길이: 30초.", + "stopCapture": "캡처 중지", + "captureComplete": "캡처 완료", + "captureAgain": "다시 캡처" + }, + "sampleList": { + "loading": "샘플 로딩 중…", + "empty": { + "title": "아직 샘플이 없습니다", + "hint": "첫 번째 오디오 샘플을 추가하여 시작하세요" + }, + "editing": "대본 편집 중", + "placeholder": "참조 텍스트 입력…", + "saving": "저장 중…", + "editTranscription": "대본 편집", + "deleteSample": "샘플 삭제", + "addSample": "샘플 추가", + "note": "참고: 30초짜리 샘플 하나가 가장 적합합니다. 여러 샘플을 추가하면 오히려 품질이 떨어질 수 있습니다. 향후 업데이트에서 샘플을 교체하고 다양한 스타일로 태그할 수 있게 될 예정입니다.", + "deleteDialog": { + "title": "샘플 삭제", + "description": "이 오디오 샘플을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "deleting": "삭제 중…" + }, + "player": { + "play": "샘플 재생", + "pause": "샘플 일시정지", + "stop": "중지", + "stopAria": "재생 중지", + "position": "샘플 재생 위치", + "positionValue": "{{current}} / {{total}}" + }, + "toast": { + "invalidText": "잘못된 텍스트", + "invalidTextDescription": "참조 텍스트는 비워둘 수 없습니다.", + "updated": "샘플 업데이트됨", + "updatedDescription": "참조 텍스트가 성공적으로 업데이트되었습니다.", + "updateFailed": "업데이트 실패", + "updateFailedFallback": "샘플 업데이트에 실패했습니다" + } + }, + "profiles": { + "card": { + "noDescription": "설명 없음", + "designed": "디자인됨", + "export": "프로필 내보내기", + "edit": "프로필 편집", + "delete": "프로필 삭제", + "selectLabel": "{{name}}, {{language}}. 생성할 음성으로 선택하세요.", + "selectLabelSelected": "{{name}}, {{language}}. 생성할 음성으로 선택됨." + }, + "list": { + "errorLoading": "프로필 로딩 오류: {{message}}", + "empty": "아직 음성 프로필이 없습니다. 첫 번째 프로필을 만들어 시작하세요.", + "createVoice": "음성 만들기", + "unsupportedNote": "현재 모델에서 지원되는 음성 프로필만 선택할 수 있습니다." + }, + "deleteDialog": { + "title": "프로필 삭제", + "body": "\"{{name}}\"을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "deleting": "삭제 중…" + } + }, + "effects": { + "title": "효과", + "newPreset": "새 프리셋", + "noDescription": "설명 없음", + "placeholder": "프리셋을 선택하거나 새로 만드세요", + "effectCount_one": "효과 {{count}}개", + "effectCount_other": "효과 {{count}}개", + "sections": { + "builtin": "내장", + "custom": "사용자 정의", + "new": "새로 만들기" + }, + "badge": { + "builtin": "내장" + }, + "unsaved": { + "title": "저장되지 않은 프리셋", + "hint": "오른쪽 패널에서 효과를 구성하세요." + }, + "detail": { + "newTitle": "새 프리셋", + "editTitle": "프리셋 편집", + "savePreset": "프리셋 저장", + "saveAsCustom": "사용자 정의로 저장", + "saving": "저장 중…", + "deleting": "삭제 중…" + }, + "fields": { + "name": "이름", + "namePlaceholder": "내 프리셋…", + "description": "설명", + "descriptionPlaceholder": "이 프리셋이 무엇을 하는지 설명해 주세요…" + }, + "preview": { + "label": "미리보기", + "button": "미리보기", + "processing": "처리 중…", + "hint": "미리보기는 저장하지 않고 클린 버전에 효과를 적용합니다." + }, + "saveAs": { + "title": "사용자 정의 프리셋으로 저장", + "description": "현재 효과 체인을 기반으로 새 사용자 정의 프리셋을 만듭니다.", + "suggestedName": "{{name}} (복사본)" + }, + "toast": { + "saved": "프리셋 저장됨", + "createdDescription": "\"{{name}}\"이(가) 생성되었습니다.", + "updated": "프리셋 업데이트됨", + "deleted": "프리셋 삭제됨", + "saveFailed": "저장 실패", + "deleteFailed": "삭제 실패", + "previewFailed": "미리보기 실패", + "nameRequired": "이름이 필요합니다" + }, + "chain": { + "loadPreset": "프리셋 불러오기…", + "addEffect": "효과 추가…", + "clear": "초기화", + "enable": "활성화", + "disable": "비활성화", + "remove": "제거" + }, + "types": { + "chorus": { + "label": "코러스 / 플랜저", + "params": { + "rate_hz": "LFO 속도 (Hz)", + "depth": "변조 깊이", + "feedback": "피드백 양", + "centre_delay_ms": "센터 딜레이 (ms)", + "mix": "Wet/dry 믹스" + } + }, + "reverb": { + "label": "리버브", + "params": { + "room_size": "룸 크기", + "damping": "고주파 감쇠", + "wet_level": "Wet 레벨", + "dry_level": "Dry 레벨", + "width": "스테레오 폭" + } + }, + "delay": { + "label": "딜레이", + "params": { + "delay_seconds": "딜레이 시간 (초)", + "feedback": "피드백 양", + "mix": "Wet/dry 믹스" + } + }, + "compressor": { + "label": "컴프레서", + "params": { + "threshold_db": "임계값 (dB)", + "ratio": "압축 비율", + "attack_ms": "어택 시간 (ms)", + "release_ms": "릴리즈 시간 (ms)" + } + }, + "gain": { + "label": "게인", + "params": { + "gain_db": "게인 (dB)" + } + }, + "highpass": { + "label": "하이패스 필터", + "params": { + "cutoff_frequency_hz": "차단 주파수 (Hz)" + } + }, + "lowpass": { + "label": "로우패스 필터", + "params": { + "cutoff_frequency_hz": "차단 주파수 (Hz)" + } + }, + "pitch_shift": { + "label": "피치 시프트", + "params": { + "semitones": "변경할 반음 수" + } + } + }, + "builtinPresets": { + "Robotic": { + "name": "로봇 음성", + "description": "금속성 로봇 음성 (느린 LFO와 높은 피드백의 플랜저)" + }, + "Radio": { + "name": "라디오", + "description": "얇은 AM 라디오 음성 (대역통과 필터링 + 가벼운 압축)" + }, + "Echo Chamber": { + "name": "에코 챔버", + "description": "트레일링 에코가 있는 공간감 있는 리버브" + }, + "Deep Voice": { + "name": "깊은 목소리", + "description": "낮은 피치와 따뜻함이 더해진 음성" + } + } + }, + "stories": { + "title": "스토리", + "newStory": "새 스토리", + "loading": "스토리 로딩 중…", + "searchPlaceholder": "스토리 검색…", + "empty": { + "title": "아직 스토리가 없습니다", + "hint": "첫 번째 스토리를 만들어 시작하세요", + "noMatches": "\"{{query}}\"와 일치하는 스토리가 없습니다" + }, + "row": { + "itemCount_one": "항목 {{count}}개", + "itemCount_other": "항목 {{count}}개", + "ariaLabel": "스토리 {{name}}, 항목 {{count}}개, {{updated}}", + "actionsLabel": "{{name}}에 대한 작업" + }, + "createDialog": { + "title": "새 스토리 만들기", + "description": "음성 생성물을 대화로 구성하는 새 스토리를 만듭니다.", + "action": "만들기", + "creating": "만드는 중…" + }, + "editDialog": { + "title": "스토리 편집", + "description": "스토리 이름과 설명을 업데이트합니다.", + "saving": "저장 중…" + }, + "deleteDialog": { + "title": "확실하신가요?", + "description": "이 스토리와 모든 항목이 영구적으로 삭제됩니다. 되돌릴 수 없습니다.", + "deleting": "삭제 중…" + }, + "fields": { + "name": "이름", + "namePlaceholder": "내 스토리", + "descriptionLabel": "설명 (선택사항)", + "descriptionPlaceholder": "대화 내용…" + }, + "toast": { + "nameRequired": "이름이 필요합니다", + "nameRequiredDescription": "스토리 이름을 입력해 주세요", + "created": "스토리 생성됨", + "createdDescription": "\"{{name}}\"이(가) 생성되었습니다", + "createFailed": "스토리 생성 실패", + "updateFailed": "스토리 업데이트 실패", + "deleteFailed": "스토리 삭제 실패" + } + }, + "storyContent": { + "selectStory": { + "title": "스토리 선택", + "hint": "목록에서 스토리를 선택하여 내용을 확인하세요" + }, + "loading": "스토리 로딩 중…", + "notFound": { + "title": "스토리를 찾을 수 없습니다", + "hint": "선택한 스토리를 불러올 수 없습니다" + }, + "generatingCount_one": "오디오 {{count}}개 생성 중", + "generatingCount_other": "오디오 {{count}}개 생성 중", + "add": "추가", + "searchPlaceholder": "이름 또는 대본으로 검색…", + "searchNoMatches": "일치하는 생성물이 없습니다", + "searchNoAvailable": "사용 가능한 생성물이 없습니다", + "exportAudio": "오디오 내보내기", + "empty": { + "title": "이 스토리에 항목이 없습니다", + "hint": "아래 상자를 사용하여 음성을 생성하고 항목을 추가하세요" + }, + "itemActions": { + "playFromHere": "여기서부터 재생", + "regenerate": "다시 생성", + "removeFromStory": "스토리에서 제거" + }, + "importAudio": "오디오 가져오기…", + "importing": "가져오는 중…", + "dropToImport": "오디오를 드롭하여 가져오기", + "toast": { + "removeFailed": "항목 제거 실패", + "reorderFailed": "항목 순서 변경 실패", + "exportFailed": "오디오 내보내기 실패", + "addFailed": "생성물 추가 실패", + "regenerateFailed": "재생성 실패", + "importFailed": "오디오 가져오기 실패" + } + }, + "history": { + "empty": "아직 생성된 음성이 없습니다…", + "actions": { + "menu": "작업", + "play": "재생", + "exportAudio": "오디오 내보내기", + "exportPackage": "패키지 내보내기", + "applyEffects": "효과 적용", + "regenerate": "다시 생성" + }, + "deleteDialog": { + "title": "생성물 삭제", + "body": "\"{{name}}\"의 이 생성물을 삭제하시겠습니까? 되돌릴 수 없습니다.", + "deleting": "삭제 중…" + }, + "clearFailedDialog": { + "title": "실패한 생성물 지우기", + "body_one": "실패한 생성물 {{count}}개가 영구적으로 삭제됩니다. 되돌릴 수 없습니다.", + "body_other": "실패한 생성물 {{count}}개가 영구적으로 삭제됩니다. 되돌릴 수 없습니다.", + "clearing": "지우는 중…", + "clearAll": "모두 지우기" + }, + "importDialog": { + "title": "생성물 가져오기", + "body": "\"{{name}}\"의 생성물을 가져옵니다. 기록에 추가됩니다.", + "importing": "가져오는 중…", + "action": "가져오기" + }, + "effectsDialog": { + "title": "효과 적용", + "body": "이 생성물에 적용할 후처리 효과를 구성합니다. 새 버전이 생성됩니다.", + "sourceLabel": "소스", + "sourcePlaceholder": "소스 버전 선택", + "apply": "적용", + "applying": "적용 중…" + } + }, + "generation": { + "placeholder": { + "storyWithEffects": "\"{{name}}\"의 음성 생성… (/를 입력하면 효과)", + "story": "\"{{name}}\"의 음성 생성…", + "profile": "{{name}}로 음성 생성…", + "effectsHint": "/를 입력하여 [laugh], [sigh] 효과 추가…", + "selectVoice": "위에서 음성 프로필을 선택하세요…" + }, + "button": { + "generate": "음성 생성", + "generating": "생성 중…", + "selectFirst": "먼저 음성 프로필을 선택하세요" + }, + "instruct": { + "show": "전달 지시사항 보기", + "hide": "전달 지시사항 숨기기", + "tooltip": "전달 지시사항 (톤, 감정, 속도)", + "placeholder": "전달 지시사항 — 예: 천천히 따뜻하게 말하기, 권위적으로 명확하게…" + }, + "voiceSelector": { + "placeholder": "음성 선택…" + }, + "effects": { + "none": "효과 없음", + "profileDefault": "프로필 기본값" + }, + "compose": { + "tooltip": "작성", + "ariaLabel": "캐릭터에 맞는 대사 작성", + "failedTitle": "작성 실패", + "failedDescription": "이 성격으로 텍스트를 생성할 수 없습니다." + }, + "persona": { + "tooltipActive": "캐릭터로 말하는 중", + "tooltipInactive": "캐릭터로 말하기", + "ariaLabelActive": "캐릭터로 말하는 중", + "ariaLabelInactive": "캐릭터로 말하기" + } + }, + "main": { + "importVoice": "음성 가져오기", + "createVoice": "음성 만들기", + "import": { + "invalidTitle": "잘못된 파일 형식", + "invalidDescription": "올바른 .voicebox.zip 파일을 선택해 주세요", + "successTitle": "프로필 가져오기 완료", + "successDescription": "음성 프로필을 성공적으로 가져왔습니다", + "failedTitle": "프로필 가져오기 실패", + "dialogTitle": "프로필 가져오기", + "dialogDescription": "\"{{name}}\"의 프로필을 가져옵니다. 모든 샘플이 포함된 새 프로필이 생성됩니다.", + "importing": "가져오는 중…", + "action": "가져오기" + } + }, + "settings": { + "tabs": { + "general": "일반", + "generation": "생성", + "captures": "캡처", + "mcp": "MCP", + "gpu": "GPU", + "logs": "로그", + "changelog": "변경 내역", + "about": "정보" + }, + "language": { + "label": "언어", + "description": "Voicebox의 표시 언어를 선택하세요." + }, + "theme": { + "label": "테마", + "description": "시스템 설정을 따르거나 밝음/어두움 모드를 고정하세요.", + "options": { + "system": "시스템", + "light": "밝음", + "dark": "어두움" + } + }, + "general": { + "docs": { "title": "문서 읽기" }, + "discord": { "title": "Discord 참여하기", "subtitle": "도움말 & 음성 공유" }, + "serverUrl": { + "title": "서버 URL", + "description": "voicebox 백엔드 서버 주소입니다.", + "invalidUrl": "올바른 URL을 입력해 주세요", + "updatedTitle": "서버 URL 업데이트됨", + "updatedDescription": "{{url}}에 연결되었습니다" + }, + "keepServerRunning": { + "title": "앱 종료 시 서버 계속 실행", + "description": "앱을 닫은 후에도 서버가 백그라운드에서 계속 실행됩니다.", + "failedTitle": "설정 업데이트 실패", + "failedDescription": "설정을 백엔드에 동기화할 수 없습니다.", + "updatedTitle": "설정 업데이트됨", + "runningDescription": "앱 종료 시 서버가 계속 실행됩니다", + "stoppedDescription": "앱 종료 시 서버가 중지됩니다" + }, + "networkAccess": { + "title": "네트워크 액세스 허용", + "description": "같은 네트워크의 다른 기기에서 서버에 접근할 수 있게 합니다. 변경 후 앱을 재시작하세요.", + "updatedTitle": "설정 업데이트됨", + "enabled": "네트워크 액세스가 활성화되었습니다. 적용하려면 앱을 재시작하세요.", + "disabled": "네트워크 액세스가 비활성화되었습니다. 적용하려면 앱을 재시작하세요." + }, + "connection": { + "connecting": "연결 중", + "offline": "오프라인", + "online": "온라인" + }, + "updates": { + "title": "앱 업데이트", + "devSuffix": " (개발)", + "devMode": { + "title": "개발 모드", + "description": "개발 모드에서는 자동 업데이트가 비활성화됩니다." + }, + "check": { + "title": "업데이트 확인", + "available": "버전 {{version}} 사용 가능", + "checking": "확인 중…", + "upToDate": "최신 버전입니다", + "button": "확인" + }, + "error": "업데이트 오류", + "download": { + "title": "{{version}}(으)로 업데이트", + "description": "최신 버전을 다운로드하고 설치합니다.", + "button": "다운로드" + }, + "downloading": "업데이트 다운로드 중…", + "ready": { + "title": "업데이트 설치 준비 완료", + "description": "버전 {{version}}이(가) 다운로드되었습니다. 재시작하여 완료하세요.", + "button": "지금 재시작" + } + }, + "api": { + "title": "API 액세스", + "description": "{{url}}에서 REST API를 통해 Voicebox를 워크플로우에 통합하세요.", + "viewReference": "전체 API 참조 보기", + "endpoints": { + "generate": "음성 생성", + "health": "서버 상태", + "profiles": "음성 목록", + "history": "과거 생성물" + } + } + }, + "generation": { + "title": "생성", + "description": "긴 텍스트 생성 설정입니다. 모든 엔진에 적용됩니다.", + "chunkLimit": { + "title": "자동 청크 분할 한도", + "description": "긴 텍스트는 문장 경계에서 청크로 분할됩니다. 값을 낮추면 긴 출력의 품질이 향상될 수 있습니다.", + "value": "{{chars}}자" + }, + "crossfade": { + "title": "청크 크로스페이드", + "description": "청크 간 오디오를 블렌딩하여 전환을 부드럽게 합니다. 0으로 설정하면 하드 컷됩니다.", + "cut": "컷", + "ms": "{{ms}}ms" + }, + "normalize": { + "title": "오디오 노멀라이즈", + "description": "출력 볼륨을 생성물 간에 일관된 레벨로 조정합니다." + }, + "autoplay": { + "title": "생성 시 자동 재생", + "description": "생성이 완료되면 자동으로 오디오를 재생합니다." + }, + "folder": { + "title": "생성물 폴더", + "description": "생성된 오디오 파일이 저장되는 위치입니다.", + "open": "열기" + }, + "sidebar": { + "aboutTitle": "음성 생성 정보", + "aboutBody": "짧은 샘플로 음성을 복제하고, 모든 언어로 모든 음성으로 음성을 생성하세요. TTS를 AI 에이전트, 게임, 팟캐스트, 장편 내레이션에 적용하세요.", + "differencesTitle": "차이점", + "clone": { + "title": "몇 초 만에 음성 복제.", + "body": "몇 초의 참조 오디오면 충분합니다. 원할 경우 여러 샘플로 더 높은 품질 지원." + }, + "engines": { + "title": "7개 엔진, 23개 언어.", + "body": "품질, 속도, 다국어 지원 중 원하는 트레이드오프를 선택하세요." + }, + "agentReady": { + "title": "에이전트 지원.", + "body": "프로필별 제어가 가능한 REST API — 복제한 음성으로 모든 AI에 목소리를 주세요." + } + } + }, + "captures": { + "dictation": { + "title": "받아쓰기", + "description": "전역 단축키로 어디서든 캡처하세요.", + "globalShortcut": { + "title": "전역 단축키", + "description": "단축키를 길게 눌러 어디서든 녹음하세요. 놓으면 변환됩니다." + }, + "pushToTalk": { + "title": "푸시투톡 단축키", + "description": "시스템 어디서든 이 키를 길게 눌러 녹음하세요. 놓으면 중지되고 변환됩니다.", + "change": "변경" + }, + "toggle": { + "title": "토글 단축키", + "description": "한 번 누르면 핸즈프리 녹음이 시작됩니다. 다시 누르면 중지됩니다. 보통 푸시투톡 + Space입니다.", + "change": "변경" + }, + "chordPicker": { + "pttTitle": "푸시투톡 단축키 설정", + "pttDescription": "사용할 키를 길게 누른 후 놓고 저장을 클릭하세요. 오른쪽 modifier 배지는 키가 왼쪽인지 오른쪽인지 보여줍니다.", + "toggleTitle": "토글 단축키 설정", + "toggleDescription": "사용할 키를 길게 누른 후 놓고 저장을 클릭하세요. 푸시투톡과 다른 키를 선택하세요." + }, + "preview": { + "title": "미리보기", + "description": "단축키를 누르고 있는 동안 화면에 표시되는 내용입니다." + }, + "copyToClipboard": { + "title": "대본을 클립보드에 복사", + "description": "캡처가 완료되면 정제된 대본이 클립보드에 저장됩니다." + }, + "autoPaste": { + "title": "포커스된 텍스트 필드에 자동 붙여넣기", + "description": "다른 앱에서 텍스트 입력이 포커스되어 있으면 직접 붙여넣습니다. Voicebox가 클립보드 내용을 저장하고 복원합니다." + } + }, + "transcription": { + "title": "변환", + "description": "캡처에 사용할 음성-텍스트 모델을 선택하세요.", + "model": { + "title": "변환 모델", + "description": "Whisper가 Voicebox에 포함되어 있으며 기기에서 완전히 실행됩니다.", + "base": "Whisper Base · 74M · {{tail}}", + "small": "Whisper Small · 244M · {{tail}}", + "medium": "Whisper Medium · 769M · {{tail}}", + "large": "Whisper Large · 1.5B · {{tail}}", + "turbo": "Whisper Turbo · Pruned Large v3 · {{tail}}", + "tail": { + "fast": "빠름", + "balanced": "균형", + "higher": "높은 정확도", + "best": "최고 정확도", + "nearBest": "최고 수준, 빠름" + } + }, + "language": { + "title": "언어", + "description": "대부분의 캡처는 자동 감지가 작동합니다. 항상 같은 언어를 사용한다면 고정하세요.", + "auto": "자동 감지", + "en": "영어", + "es": "스페인어", + "fr": "프랑스어", + "de": "독일어", + "ja": "일본어", + "zh": "중국어", + "hi": "힌디어" + }, + "archive": { + "title": "오디오 보관", + "description": "모든 대본과 함께 원본 녹음을 보관합니다." + } + }, + "refinement": { + "title": "정제", + "description": "로컬 LLM으로 대본의 불필요한 말, 구두점, 자기 수정을 정리합니다. (선택사항)", + "auto": { + "title": "대본 자동 정제", + "description": "모든 캡처 후 실행됩니다. 캡처 탭에서 원본과 정제본 사이를 전환할 수 있습니다." + }, + "model": { + "title": "정제 모델", + "description": "더 큰 모델은 느리지만 미묘한 자기 수정과 기술 용어를 더 잘 처리합니다.", + "size06": "Qwen3 · 0.6B · 400 MB · {{tail}}", + "size17": "Qwen3 · 1.7B · 1.1 GB · {{tail}}", + "size40": "Qwen3 · 4B · 2.5 GB · {{tail}}", + "tail": { + "veryFast": "매우 빠름", + "fast": "빠름", + "fullQuality": "최고 품질" + } + }, + "smartCleanup": { + "title": "스마트 정리", + "description": "불필요한 말(음, 어)을 제거하고 구두점을 복원하며 대문자화를 수정합니다. 내용을 바꾸지 않습니다." + }, + "selfCorrection": { + "title": "자기 수정 제거", + "description": "문장 중간에 마음을 바꾼 경우(\\\"아니, 사실...\\\", \\\"잠깐, 그게 아니라...\\\"), 철회된 부분을 제거하고 최종 의도를 유지합니다." + }, + "preserveTechnical": { + "title": "기술 용어 보존", + "description": "코드 식별자, 명령 이름, 약어를 말한 그대로 유지합니다. 코드 프롬프트에 받아쓰기할 때 켜세요." + } + }, + "playback": { + "title": "재생", + "description": "캡처 탭의 \\\"Play as\\\" 작업에 사용할 기본 음성입니다.", + "defaultVoice": { + "title": "기본 음성", + "description": "음성을 먼저 선택하지 않고 Play as를 클릭할 때 사용됩니다. 캡처별로 변경할 수 있습니다.", + "noClonedVoices": "아직 복제된 음성이 없습니다", + "noneSelected": "선택 안 됨", + "clonedVoices": "복제된 음성" + } + }, + "storage": { + "title": "저장소", + "description": "캡처는 Voicebox 데이터 디렉토리에 오디오와 대본 파일 쌍으로 저장됩니다.", + "retention": { + "title": "보관 기간", + "description": "캡처 보관 기간입니다. 오디오와 대본 모두에 적용됩니다.", + "forever": "영구 보관", + "d90": "90일", + "d30": "30일", + "d7": "7일" + }, + "folder": { + "title": "캡처 폴더", + "description": "캡처 오디오와 대본이 디스크에 저장되는 위치입니다.", + "open": "열기" + } + }, + "sidebar": { + "aboutTitle": "캡처 정보", + "aboutBody": "어디서든 단축키를 누르고 말하면 Voicebox가 음성을 텍스트로 변환합니다. 복제된 음성으로 재생하고, 앱에 붙여넣고, 코딩 에이전트로 보내세요.", + "differencesTitle": "차이점", + "local": { + "title": "완전 로컬.", + "body": "Whisper와 정제 LLM이 기기에서 실행됩니다. 클라우드 없음, 계정 없음, 음성이 기기를 떠나지 않습니다." + }, + "playAs": { + "title": "모든 음성으로 재생.", + "body": "복제한 모든 프로필로 대본을 읽을 수 있습니다." + }, + "crossPlatform": { + "title": "크로스 플랫폼.", + "body": "macOS, Windows, Linux에서 동일한 단축키와 흐름." + }, + "windowsCaveat": { + "title": "Windows 참고사항", + "body": "Voicebox 자체 또는 관리자 권한으로 실행 중인 앱이 포커스된 경우 단축키가 작동하지 않습니다. 개선 중입니다." + } + } + }, + "mcp": { + "install": { + "title": "에이전트에 설치", + "description": "Voicebox는 앱이 열려 있을 때 로컬 MCP 서버를 제공합니다. 다음 중 하나를 에이전트의 MCP 설정에 붙여넣으세요.", + "http": { + "title": "HTTP (권장)", + "description": "HTTP MCP를 사용하는 클라이언트용 — Claude Code, Cursor, Windsurf, VS Code." + }, + "claudeCode": { + "title": "Claude Code 원라인", + "description": "Claude Code CLI를 통해 등록합니다." + }, + "stdio": { + "title": "Stdio (대체)", + "description": "stdio 프로세스만 실행하는 클라이언트용. shim 바이너리가 앱에 포함되어 있습니다." + }, + "copy": "복사", + "copied": "복사됨" + }, + "defaultVoice": { + "title": "기본 음성", + "description": "에이전트가 특정 프로필 없이 voicebox.speak를 호출하고 클라이언트별 바인딩도 없을 때 사용됩니다.", + "label": "기본 재생 음성", + "labelHint": "캡처 탭의 'Play as 음성' 드롭다운과 공유 — 수동 재생용 기본 음성입니다.", + "none": "(없음)" + }, + "bindings": { + "title": "에이전트별 음성", + "description": "특정 에이전트를 특정 음성에 바인딩하여 누가 말하는지 바로 알 수 있습니다. 에이전트는 X-Voicebox-Client-Id 헤더(또는 stdio용 VOICEBOX_CLIENT_ID 환경변수)로 자신을 식별합니다.", + "empty": "아직 바인딩이 없습니다. 아래에서 추가한 후 MCP 클라이언트가 일치하는 X-Voicebox-Client-Id를 보내도록 설정하세요.", + "lastSeen": "마지막 접속 {{when}}", + "lastSeenTitle": "마지막 접속 {{when}}", + "neverConnected": "연결된 적 없음", + "defaultOption": "(기본값)", + "removeAria": "{{client}} 바인딩 제거", + "add": { + "title": "바인딩 추가", + "clientIdPlaceholder": "클라이언트 ID (예: claude-code)", + "labelPlaceholder": "레이블 (선택사항)", + "action": "바인딩 추가" + } + }, + "sidebar": { + "aboutTitle": "MCP 정보", + "aboutBody": "Model Context Protocol을 통해 AI 코딩 에이전트(Claude Code, Cursor, Windsurf)가 Voicebox 도구를 호출할 수 있습니다. 복제된 음성으로 말하고, 오디오를 변환하고, 캡처를 탐색하세요.", + "toolsTitle": "사용 가능한 도구", + "tools": { + "speak": "음성 프로필로 텍스트 읽기.", + "transcribe": "Whisper STT로 클립 변환.", + "listCaptures": "최근 받아쓰기/녹음 목록.", + "listProfiles": "사용 가능한 음성 프로필 목록." + }, + "postSpeak": "쉘 스크립트, ACP, A2A용 POST /speak로도 제공됩니다." + } + }, + "gpu": { + "cpuOnly": "CPU 전용", + "vramUsed": "VRAM {{mb}} MB", + "noAcceleration": "GPU 가속이 감지되지 않았습니다", + "active": "활성", + "cuda": { + "title": "CUDA 백엔드", + "activeTitle": "CUDA 백엔드 활성", + "description": "다운로드 가능한 CUDA 백엔드를 통한 NVIDIA GPU 가속.", + "downloading": "CUDA 백엔드 다운로드 중…", + "downloadingShort": "다운로드 중…", + "updating": "업데이트 중…" + }, + "activeBackend": { + "description": "GPU 가속이 현재 활성화되어 있습니다." + }, + "restart": { + "ready": "서버가 성공적으로 재시작되었습니다", + "waiting": "서버 재시작 중…", + "stopping": "서버 중지 중…" + }, + "download": { + "title": "CUDA 백엔드 다운로드", + "description": "~2.4 GB 다운로드. NVIDIA GPU와 CUDA 지원이 필요합니다.", + "button": "다운로드" + }, + "switchToCuda": { + "title": "CUDA 백엔드로 전환", + "description": "CUDA 백엔드가 다운로드되어 준비되었습니다. 재시작하여 활성화하세요.", + "button": "재시작" + }, + "switchToCpu": { + "title": "CPU 백엔드로 전환", + "description": "GPU 가속을 비활성화합니다. 나중에 CUDA를 다시 다운로드할 수 있습니다.", + "button": "전환" + }, + "remove": { + "title": "CUDA 백엔드 제거", + "description": "다운로드된 CUDA 바이너리를 삭제하여 디스크 공간을 확보합니다.", + "button": "제거" + }, + "errors": { + "downloadFailed": "다운로드 실패", + "downloadStart": "다운로드 시작 실패", + "restartFailed": "재시작 실패", + "switchCpu": "CPU 전환 실패", + "deleteCuda": "CUDA 백엔드 삭제 실패", + "deleteRocm": "ROCm 백엔드 삭제 실패" + }, + "footer": "Voicebox는 시스템에서 사용 가능한 최고의 GPU를 자동으로 감지하여 사용합니다. Apple Silicon Mac에서는 Metal Performance Shaders(MPS)를 통해 MLX 백엔드가 Neural Engine과 GPU에서 기본 실행되며 추가 설정이 필요하지 않습니다. Windows에서는 선택적 CUDA(NVIDIA) 또는 ROCm(AMD) 백엔드를 다운로드하여 하드웨어 가속 추론을 사용할 수 있습니다. Intel XPU와 DirectML도 PyTorch를 통해 지원됩니다. GPU가 감지되지 않으면 Voicebox가 CPU로 대체됩니다 — 모든 엔진이 작동하지만 더 느립니다.", + "rocm": { + "title": "AMD ROCm 백엔드", + "activeTitle": "ROCm 백엔드 활성", + "description": "다운로드 가능한 ROCm 백엔드를 통한 AMD GPU 가속.", + "downloading": "ROCm 백엔드 다운로드 중…", + "downloadingShort": "다운로드 중…", + "updating": "업데이트 중…" + }, + "downloadRocm": { + "title": "AMD ROCm 백엔드 다운로드", + "description": "약 2~3GB를 다운로드합니다. ROCm을 지원하는 AMD Radeon GPU가 필요합니다.", + "button": "다운로드" + }, + "switchToRocm": { + "title": "ROCm 백엔드로 전환", + "description": "ROCm 백엔드가 다운로드되어 준비되었습니다. 활성화하려면 재시작하세요.", + "button": "재시작" + }, + "removeRocm": { + "title": "ROCm 백엔드 제거", + "description": "다운로드된 ROCm 바이너리를 삭제하여 디스크 공간을 확보합니다.", + "button": "제거" + } + }, + "logs": { + "title": "서버 로그", + "lineCount_one": "{{count}}줄", + "lineCount_other": "{{count}}줄", + "scrollToBottom": "맨 아래로 스크롤", + "clear": "지우기", + "empty": "아직 로그 출력이 없습니다.", + "devHint": "서버 로그는 앱이 서버 프로세스를 관리할 때만 캡처됩니다 (프로덕션 빌드)." + }, + "changelog": { + "devBadge": "개발", + "showLess": "간략히", + "showMore": "더 보기" + }, + "about": { + "tagline": "오픈소스 음성 합성 스튜디오. 음성 복제, 음성 생성, 효과 적용, 음성 기반 앱 구축 — 모두 기기에서 로컬로 실행됩니다.", + "createdBy": "만든 사람", + "buyCoffee": "커피 한 잔 사주기", + "license": "MIT 라이선스" + } + }, + "models": { + "title": "모델", + "subtitle": "음성 생성 및 변환을 위한 AI 모델 다운로드 및 관리", + "defaultName": "모델", + "unknownSize": "알 수 없는 크기", + "sections": { + "voiceGeneration": "음성 생성", + "transcription": "변환", + "languageModels": "언어 모델" + }, + "status": { + "loaded": "로드됨" + }, + "storage": { + "location": "저장 위치", + "open": "열기", + "change": "변경", + "migrating": "이동 중…", + "reset": "초기화", + "pickerTitle": "모델 저장 폴더 선택" + }, + "progress": { + "connecting": "연결 중…", + "connectingHf": "HuggingFace에 연결 중…" + }, + "problems": { + "title": "문제", + "clearAll": "모두 지우기", + "noDetails": "사용 가능한 오류 세부 정보가 없습니다. 다시 다운로드해 보세요.", + "startedAt": "{{time}}에 시작" + }, + "detail": { + "loadingInfo": "모델 정보 로딩 중…", + "byAuthor": "{{author}} 제작", + "downloads": "다운로드", + "likes": "좋아요", + "license": "라이선스", + "languagesCount": "{{count}}개 언어 지원", + "languagesList": "언어: {{list}}", + "onDisk": "디스크 {{size}}" + }, + "actions": { + "download": "다운로드", + "retry": "다시 다운로드", + "unload": "언로드", + "unloading": "언로드 중…", + "unloadFirst": "삭제 전에 모델을 언로드하세요", + "deleteModel": "모델 삭제" + }, + "deleteDialog": { + "title": "모델 삭제", + "body": "{{name}}을(를) 삭제하시겠습니까?", + "sizeNote": "디스크 공간 {{size}}이(가) 확보됩니다. 다시 사용하려면 모델을 다시 다운로드해야 합니다.", + "deleting": "삭제 중…" + }, + "migrateDialog": { + "title": "모델을 새 위치로 이동할까요?", + "description": "모델을 새 폴더로 이동하는 동안 서버가 종료됩니다. 마이그레이션이 완료되면 자동으로 재시작됩니다.", + "action": "모델 이동", + "preparing": "준비 중…", + "restartingServer": "서버 재시작 중…" + }, + "migrate": { + "title": "모델 이동 중", + "offline": "모델이 이동되는 동안 서버가 오프라인 상태입니다." + }, + "toast": { + "downloadFailed": "다운로드 실패", + "cancelFailed": "취소 실패", + "cancelFailedDescription": "다운로드 작업을 취소할 수 없습니다.", + "deleted": "모델 삭제됨", + "deletedDescription": "{{name}}이(가) 성공적으로 삭제되었습니다.", + "deleteFailed": "삭제 실패", + "unloaded": "모델 언로드됨", + "unloadedDescription": "{{name}}이(가) 메모리에서 언로드되었습니다.", + "unloadFailed": "언로드 실패", + "openFolderFailed": "모델 폴더 열기 실패", + "pickerFailed": "폴더 선택기 열기 실패", + "resetToDefault": "기본 위치로 초기화되었습니다. 서버 재시작 중…", + "noModelsToMigrate": "이동할 모델이 없습니다", + "noModelsToMigrateDescription": "저장 위치를 변경하기 전에 하나 이상의 모델을 다운로드하세요.", + "migrated": "모델 이동 완료", + "migrationFailed": "마이그레이션 실패", + "migrationFailedGeneric": "모델 이동에 실패했습니다", + "migrationConnectionLost": "마이그레이션 중 연결이 끊어졌습니다" + } + } +} From 258b92c9c0339adacbec2ea6a78351a5c3537bd4 Mon Sep 17 00:00:00 2001 From: Daniel Knoodle Date: Mon, 20 Jul 2026 20:47:10 -0500 Subject: [PATCH 7/9] fix(offline): remove process-global offline guard from Qwen3 LLM loads (#924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit force_offline_if_cached flips HF_HUB_OFFLINE (env + huggingface_hub constant + transformers._is_offline_mode) process-wide for the duration of a cached LLM load, silently switching every concurrent model download/load on other threads to offline mode. With default capture settings (whisper-turbo STT + Qwen3 refinement + auto_refine) a first run downloads several models concurrently, and a poisoned fetch surfaces as "Can't load feature extractor..." (whisper) or "Unrecognized model ... model_type" (Qwen3) rather than anything mentioning offline mode. These are the last two call sites of the guard — the same pattern was deliberately removed app-wide in #524/#530 after identical failures, and the 0.5.0 LLM backend reintroduced it. LLM loads now run with the process's default HF_HUB_OFFLINE state, matching every other backend (issue #462 precedent). Fixes #841 Claude-Session: https://claude.ai/code/session_011iwL9AyeAWgz2jpgcHxJpC Co-authored-by: Claude Fable 5 --- backend/backends/qwen_llm_backend.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/backend/backends/qwen_llm_backend.py b/backend/backends/qwen_llm_backend.py index e77a3540..5d29d892 100644 --- a/backend/backends/qwen_llm_backend.py +++ b/backend/backends/qwen_llm_backend.py @@ -19,7 +19,6 @@ from .base import ( manual_seed, model_load_progress, ) -from ..utils.hf_offline_patch import force_offline_if_cached logger = logging.getLogger(__name__) @@ -103,15 +102,19 @@ class PyTorchQwenLLMBackend: with model_load_progress(progress_model_name, is_cached): logger.info("Loading Qwen3 %s on %s...", model_size, self.device) - with force_offline_if_cached(is_cached, progress_model_name): - self.tokenizer = AutoTokenizer.from_pretrained(repo) - dtype = torch.float16 if self.device in ("cuda", "mps") else torch.float32 - self.model = AutoModelForCausalLM.from_pretrained( - repo, - dtype=dtype, - ) - self.model.to(self.device) - self.model.eval() + # Loads run with the process's default HF_HUB_OFFLINE state. + # Forcing offline for cached models flips process-global state + # and silently switches every concurrent download/load on other + # threads to offline mode (issue #841) — the same regression + # removed app-wide in #524/#530. + self.tokenizer = AutoTokenizer.from_pretrained(repo) + dtype = torch.float16 if self.device in ("cuda", "mps") else torch.float32 + self.model = AutoModelForCausalLM.from_pretrained( + repo, + dtype=dtype, + ) + self.model.to(self.device) + self.model.eval() self._current_model_size = model_size self.model_size = model_size @@ -223,8 +226,8 @@ class MLXQwenLLMBackend: with model_load_progress(progress_model_name, is_cached): logger.info("Loading Qwen3 %s via MLX...", model_size) - with force_offline_if_cached(is_cached, progress_model_name): - loaded = mlx_load(repo) + # See the PyTorch loader comment — no offline forcing (issue #841). + loaded = mlx_load(repo) # mlx_lm.load returns (model, tokenizer) by default and # (model, tokenizer, config) when return_config=True. From 71b51366bcef5ca78d33c0849f0431b426aadf9a Mon Sep 17 00:00:00 2001 From: Andrew Barnes Date: Mon, 20 Jul 2026 22:22:18 -0400 Subject: [PATCH 8/9] Fix CUDA downloads on unsupported platforms (#770) * Fix CUDA downloads on unsupported platforms * fix: align CUDA status nullability * fix: require CUDA download support flag --- README.md | 3 +- app/src/lib/api/types.ts | 5 ++- backend/routes/cuda.py | 4 +++ backend/services/cuda.py | 33 ++++++++++++++++++- backend/tests/test_cuda_download.py | 32 ++++++++++++++++++ .../docs/overview/gpu-acceleration.mdx | 4 +-- docs/content/docs/overview/introduction.mdx | 3 +- 7 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 backend/tests/test_cuda_download.py diff --git a/README.md b/README.md index 34ef1c0c..b1ee81fc 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,8 @@ Use cases: agent dev loops (dictate a question, hear the answer in a cloned voic | Platform | Backend | Notes | | ------------------------ | -------------- | ---------------------------------------------- | | macOS (Apple Silicon) | MLX (Metal) | 4-5x faster via Neural Engine | -| Windows / Linux (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app | +| Windows (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app | +| Linux (NVIDIA) | PyTorch (CUDA) | Use a local/remote Python backend with CUDA PyTorch | | Linux (AMD) | PyTorch (ROCm) | Auto-configures HSA_OVERRIDE_GFX_VERSION | | Windows (any GPU) | DirectML | Universal Windows GPU support | | Intel Arc | IPEX/XPU | Intel discrete GPU acceleration | diff --git a/app/src/lib/api/types.ts b/app/src/lib/api/types.ts index 4a970749..d360ed1c 100644 --- a/app/src/lib/api/types.ts +++ b/app/src/lib/api/types.ts @@ -287,7 +287,10 @@ export interface CudaDownloadProgress { export interface CudaStatus { available: boolean; // CUDA binary exists on disk active: boolean; // Currently running the CUDA binary - binary_path?: string; + binary_path: string | null; + cuda_libs_version: string | null; + download_supported: boolean; // Platform has a matching release asset + unsupported_reason: string | null; downloading: boolean; // Download in progress download_progress?: CudaDownloadProgress; } diff --git a/backend/routes/cuda.py b/backend/routes/cuda.py index cd9d5766..cb1104eb 100644 --- a/backend/routes/cuda.py +++ b/backend/routes/cuda.py @@ -26,6 +26,10 @@ async def download_cuda_backend(): """Download the CUDA backend binary.""" from ..services import cuda + unsupported_reason = cuda.get_cuda_download_unsupported_reason() + if unsupported_reason: + raise HTTPException(status_code=409, detail=unsupported_reason) + if cuda.get_cuda_binary_path() is not None: raise HTTPException(status_code=409, detail="CUDA backend already downloaded") diff --git a/backend/services/cuda.py b/backend/services/cuda.py index 87fd8fb3..d274a43a 100644 --- a/backend/services/cuda.py +++ b/backend/services/cuda.py @@ -21,9 +21,9 @@ import tarfile from pathlib import Path from typing import Optional +from .. import __version__ from ..config import get_data_dir from ..utils.progress import get_progress_manager -from .. import __version__ logger = logging.getLogger(__name__) @@ -31,6 +31,8 @@ GITHUB_RELEASES_URL = "https://github.com/jamiepine/voicebox/releases/download" PROGRESS_KEY = "cuda-backend" +CUDA_DOWNLOAD_UNSUPPORTED_REASON = "Downloadable CUDA backend releases are currently only published for Windows." + # The current expected CUDA libs version. Bump this when we change the # CUDA toolkit version or torch's CUDA dependency changes (e.g. cu126 -> cu128). CUDA_LIBS_VERSION = "cu128-v1" @@ -63,6 +65,25 @@ def get_cuda_exe_name() -> str: return "voicebox-server-cuda" +def is_cuda_download_supported() -> bool: + """Return whether this platform has a matching CUDA release asset.""" + return sys.platform == "win32" + + +def get_cuda_download_unsupported_reason() -> str | None: + """Explain why this platform cannot use the release-download flow.""" + if is_cuda_download_supported(): + return None + return CUDA_DOWNLOAD_UNSUPPORTED_REASON + + +def ensure_cuda_download_supported() -> None: + """Raise if downloading would fetch an asset built for another platform.""" + reason = get_cuda_download_unsupported_reason() + if reason: + raise RuntimeError(reason) + + def get_cuda_binary_path() -> Optional[Path]: """Return path to the CUDA executable if it exists inside the onedir.""" p = get_cuda_dir() / get_cuda_exe_name() @@ -103,12 +124,15 @@ def get_cuda_status() -> dict: cuda_path = get_cuda_binary_path() progress = progress_manager.get_progress(PROGRESS_KEY) cuda_libs_version = get_installed_cuda_libs_version() + unsupported_reason = get_cuda_download_unsupported_reason() return { "available": cuda_path is not None, "active": is_cuda_active(), "binary_path": str(cuda_path) if cuda_path else None, "cuda_libs_version": cuda_libs_version, + "download_supported": unsupported_reason is None, + "unsupported_reason": unsupported_reason, "downloading": progress is not None and progress.get("status") == "downloading", "download_progress": progress, } @@ -257,6 +281,8 @@ async def download_cuda_binary(version: Optional[str] = None): async def _download_cuda_binary_locked(version: Optional[str] = None): """Inner implementation of download_cuda_binary, called under _download_lock.""" + ensure_cuda_download_supported() + import httpx if version is None: @@ -387,6 +413,11 @@ async def check_and_update_cuda_binary(): if not cuda_path: return # No CUDA binary installed, nothing to update + unsupported_reason = get_cuda_download_unsupported_reason() + if unsupported_reason: + logger.info("Skipping CUDA backend auto-update: %s", unsupported_reason) + return + need_server = _needs_server_download() need_libs = _needs_cuda_libs_download() diff --git a/backend/tests/test_cuda_download.py b/backend/tests/test_cuda_download.py new file mode 100644 index 00000000..77115f06 --- /dev/null +++ b/backend/tests/test_cuda_download.py @@ -0,0 +1,32 @@ +import sys as py_sys +import types + +import pytest + +from backend.services import cuda + + +def test_cuda_status_reports_unsupported_linux_download(monkeypatch, tmp_path): + monkeypatch.setattr(cuda.sys, "platform", "linux") + monkeypatch.setattr(cuda, "get_data_dir", lambda: tmp_path) + + status = cuda.get_cuda_status() + + assert status["available"] is False + assert status["download_supported"] is False + assert status["unsupported_reason"] == cuda.CUDA_DOWNLOAD_UNSUPPORTED_REASON + + +@pytest.mark.asyncio +async def test_cuda_download_rejects_linux_before_network(monkeypatch, tmp_path): + monkeypatch.setattr(cuda.sys, "platform", "linux") + monkeypatch.setattr(cuda, "get_data_dir", lambda: tmp_path) + + class UnexpectedClient: + def __init__(self, *args, **kwargs): + raise AssertionError("unsupported platforms should not start a release download") + + monkeypatch.setitem(py_sys.modules, "httpx", types.SimpleNamespace(AsyncClient=UnexpectedClient)) + + with pytest.raises(RuntimeError, match="currently only published for Windows"): + await cuda._download_cuda_binary_locked("v0.5.0") diff --git a/docs/content/docs/overview/gpu-acceleration.mdx b/docs/content/docs/overview/gpu-acceleration.mdx index b9730b7d..8d5d1a3b 100644 --- a/docs/content/docs/overview/gpu-acceleration.mdx +++ b/docs/content/docs/overview/gpu-acceleration.mdx @@ -23,7 +23,7 @@ This page is for the cases where it doesn't: | **Windows + NVIDIA** | PyTorch CUDA (cu128) | Auto-downloads the CUDA backend binary on first use | | **Windows + Intel Arc** | PyTorch XPU (IPEX) | New in 0.4 — works with Arc A-series and B-series | | **Windows generic GPU** | DirectML | Universal Windows GPU support; slower than CUDA | -| **Linux + NVIDIA** | PyTorch CUDA (cu128) | Same auto-download flow as Windows | +| **Linux + NVIDIA** | PyTorch CUDA (cu128) | Use a local/remote Python backend with CUDA PyTorch | | **Linux + AMD** | PyTorch ROCm | Auto-configures `HSA_OVERRIDE_GFX_VERSION` | | **Linux + Intel Arc** | PyTorch XPU (IPEX) | | | **Any (no GPU)** | PyTorch CPU | Works everywhere; expect 5-50x slower than GPU | @@ -46,7 +46,7 @@ On M-series Macs, Voicebox ships an MLX-optimized backend that uses the Apple Ne The Whisper Turbo + MLX combo dropped transcription latency from ~20s to ~2-3s on M-series chips (see CHANGELOG entry for v0.1.10). -## Windows / Linux + NVIDIA — The CUDA Backend Swap +## Windows + NVIDIA — The CUDA Backend Swap Voicebox doesn't bundle CUDA into the main installer (it would balloon downloads to multi-gigabyte territory for users who don't have an NVIDIA GPU). Instead, when you first need it, the app downloads a separate **CUDA backend binary** that contains the PyTorch + CUDA runtime. diff --git a/docs/content/docs/overview/introduction.mdx b/docs/content/docs/overview/introduction.mdx index de16aa18..155bdb41 100644 --- a/docs/content/docs/overview/introduction.mdx +++ b/docs/content/docs/overview/introduction.mdx @@ -75,7 +75,8 @@ No cloud fallback, no bring-your-own-API-key. Local is the product. | Platform | Backend | Notes | |----------|---------|-------| | macOS (Apple Silicon) | MLX (Metal) | 4-5x faster via Neural Engine | -| Windows / Linux (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app | +| Windows (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app | +| Linux (NVIDIA) | PyTorch (CUDA) | Use a local/remote Python backend with CUDA PyTorch | | Linux (AMD) | PyTorch (ROCm) | Auto-configures HSA_OVERRIDE_GFX_VERSION | | Windows (any GPU) | DirectML | Universal Windows GPU support | | Intel Arc | IPEX/XPU | Intel discrete GPU acceleration | From 30db291b01f90d83ee3cc209307c0e5d71ef479a Mon Sep 17 00:00:00 2001 From: sidc124 Date: Tue, 21 Jul 2026 09:21:06 +0530 Subject: [PATCH 9/9] fix(kokoro): add missing male Mandarin voices (#788) Co-authored-by: Siddharth Chintawar Co-authored-by: Cursor --- backend/backends/kokoro_backend.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/backends/kokoro_backend.py b/backend/backends/kokoro_backend.py index efe91dfc..59005f0a 100644 --- a/backend/backends/kokoro_backend.py +++ b/backend/backends/kokoro_backend.py @@ -96,11 +96,16 @@ KOKORO_VOICES = [ ("pf_dora", "Dora", "female", "pt"), ("pm_alex", "Alex", "male", "pt"), ("pm_santa", "Santa", "male", "pt"), - # Chinese + # Chinese female ("zf_xiaobei", "Xiaobei", "female", "zh"), ("zf_xiaoni", "Xiaoni", "female", "zh"), ("zf_xiaoxiao", "Xiaoxiao", "female", "zh"), ("zf_xiaoyi", "Xiaoyi", "female", "zh"), + # Chinese male + ("zm_yunjian", "Yunjian", "male", "zh"), + ("zm_yunxi", "Yunxi", "male", "zh"), + ("zm_yunxia", "Yunxia", "male", "zh"), + ("zm_yunyang", "Yunyang", "male", "zh"), ] # Map our ISO language codes to Kokoro lang_code characters