+
+
+
+
+ {hasLabels ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {dictationSupported ? (
+
+ ) : (
+
+ )}
+
+
+
+ {dictionary.map((entry, index) => (
+
+ updateDictionaryEntry(index, e.target.value)}
+ // Skip the empty-row commit-splice when focus moves to this row's
+ // Remove button (keyboard Tab), so its index stays valid and its
+ // activation deletes this row instead of the next one.
+ onBlur={(e) => {
+ if (
+ (e.relatedTarget as HTMLElement | null)?.dataset.dictRemove ===
+ String(index)
+ ) {
+ return;
+ }
+ commitDictionaryEntry(index);
+ }}
+ className="h-8 flex-1 text-sm"
+ aria-label={`Dictionary entry ${index + 1}`}
+ />
+
+
+ ))}
+
+ setNewEntry(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ handleAddEntry();
+ }
+ }}
+ placeholder="Jane Doe"
+ className="h-8 flex-1 text-sm"
+ aria-label="New dictionary entry"
+ />
+
+
+
+
+
+ {recentDictations.length === 0 ? (
+
+ {t("settings.voice.recents.empty")}
+
+ ) : (
+ <>
+ {recentDictations.map((item) => (
+
+
+
+ {item.text}
+
+
+ {new Date(item.at).toLocaleString()}
+
+
+
+
+ ))}
+
+
+
+ >
+ )}
+
+
+
+ {ttsSupported ? (
+ <>
+
+
+
+
+
+
+
+
+ {effectiveTtsEngine === "studio" ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ v !== undefined && setTtsRate(v)}
+ className="w-48"
+ aria-label="Speaking rate"
+ />
+
+
+ {effectiveTtsEngine === "system" && (
+
+ v !== undefined && setTtsPitch(v)}
+ className="w-48"
+ aria-label="Voice pitch"
+ />
+
+ )}
+
+
+ v !== undefined && setTtsVolume(v)}
+ className="w-48"
+ aria-label="Playback volume"
+ />
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/studio/frontend/src/i18n/locales/en.ts b/studio/frontend/src/i18n/locales/en.ts
index 3e351c22bc..fe3a6f8542 100644
--- a/studio/frontend/src/i18n/locales/en.ts
+++ b/studio/frontend/src/i18n/locales/en.ts
@@ -97,10 +97,81 @@ export const en = {
appearance: "Appearance",
resources: "System",
chat: "Chat",
+ voice: "Voice",
connections: "Connections",
apiKeys: "API",
about: "About",
},
+ voice: {
+ title: "Voice",
+ description: "Microphone, dictation, and read-aloud",
+ dictation: {
+ sectionTitle: "Dictation",
+ microphoneLabel: "Microphone",
+ microphoneDescription: "Used for dictation",
+ microphoneFallbackHint:
+ "Used for dictation. Falls back to the system default if the browser speech engine cannot use this device",
+ microphoneGrantDescription: "Allow mic access to show device names",
+ allowMicrophone: "Allow microphone",
+ micAccessBlocked:
+ "Microphone access was blocked. Allow microphone access for this Unsloth page, then try again.",
+ micAccessUnsupported:
+ "Microphone access is not supported in this browser or context.",
+ micOpenFailed:
+ "Could not open the selected microphone. Check permissions or pick another device.",
+ systemDefault: "System default",
+ savedMicDisconnected: "Saved microphone (not connected)",
+ languageLabel: "Dictation language",
+ languageDescription: "Language to recognize",
+ languageAuto: "Auto (browser language)",
+ testLabel: "Test dictation",
+ testDescription: "Speak to check your mic and settings",
+ startTest: "Start test",
+ stopTest: "Stop test",
+ listening: "Listening…",
+ testSaved: "Saved to recent dictations",
+ notSupported: "Not supported in this browser",
+ },
+ dictionary: {
+ sectionTitle: "Dictation dictionary",
+ sectionDescription:
+ "Apply the spelling entered here when dictation recognizes the same words or phrase",
+ addEntry: "Add entry",
+ },
+ recents: {
+ sectionTitle: "Recent dictations",
+ sectionDescription:
+ "Your recent dictations will appear here so you can recover text",
+ empty: "No dictations yet",
+ copied: "Copied to clipboard",
+ copyFailed: "Could not copy to clipboard",
+ clear: "Clear recent dictations",
+ },
+ readAloud: {
+ sectionTitle: "Read aloud",
+ buttonLabel: "Read aloud button",
+ buttonDescription: "Show on assistant responses",
+ engineLabel: "TTS engine",
+ engineSystemDescription: "Built-in device voices",
+ engineStudioDescription: "Uses the loaded audio model (e.g. Orpheus)",
+ engineSystem: "System voices",
+ engineStudio: "Load TTS model",
+ modelLabel: "TTS model",
+ modelDescription:
+ "Load an audio model from the model selector (e.g. Orpheus TTS)",
+ voiceLabel: "Voice",
+ voiceDescription: "Best voices on this device",
+ speedLabel: "Speed",
+ pitchLabel: "Pitch",
+ volumeLabel: "Volume",
+ previewLabel: "Preview voice",
+ previewDescription: "Play a short sample",
+ previewAction: "Preview",
+ stopAction: "Stop",
+ ttsLabel: "Text to speech",
+ notSupported: "Not supported in this browser",
+ },
+ },
general: {
title: "General",
description: "Global preferences for Unsloth.",
@@ -549,7 +620,8 @@ export const en = {
codingAgents: "Coding agents",
codingAgentsHint:
"Launch a coding agent against this server. It uses the loaded model; a local server mints an API key automatically, a remote one includes it in the command.",
- codingAgentsSwap: "Swap claude for codex, openclaw, opencode, hermes, or pi.",
+ codingAgentsSwap:
+ "Swap claude for codex, openclaw, opencode, hermes, or pi.",
codingAgentDetected: "Installed on this machine",
codingAgentsDetectedHint: "Detected on this machine: {agents}.",
relativeNever: "never",
diff --git a/studio/frontend/src/i18n/locales/ja.ts b/studio/frontend/src/i18n/locales/ja.ts
index 44bb617512..23752b6c26 100644
--- a/studio/frontend/src/i18n/locales/ja.ts
+++ b/studio/frontend/src/i18n/locales/ja.ts
@@ -104,6 +104,7 @@ export const ja = {
connections: "接続",
apiKeys: "API",
about: "情報",
+ voice: "音声",
},
general: {
title: "一般",
diff --git a/studio/frontend/src/i18n/locales/pt-br.ts b/studio/frontend/src/i18n/locales/pt-br.ts
index d689b96428..07832ef1d0 100644
--- a/studio/frontend/src/i18n/locales/pt-br.ts
+++ b/studio/frontend/src/i18n/locales/pt-br.ts
@@ -103,6 +103,7 @@ export const ptBR = {
connections: "Conexões",
apiKeys: "API",
about: "Sobre",
+ voice: "Voz",
},
general: {
title: "Geral",
diff --git a/studio/frontend/src/i18n/locales/zh-CN.ts b/studio/frontend/src/i18n/locales/zh-CN.ts
index fc4e126d72..4c51755244 100644
--- a/studio/frontend/src/i18n/locales/zh-CN.ts
+++ b/studio/frontend/src/i18n/locales/zh-CN.ts
@@ -103,6 +103,7 @@ export const zhCN = {
connections: "连接",
apiKeys: "API",
about: "关于",
+ voice: "语音",
},
general: {
title: "通用",
diff --git a/studio/frontend/src/lib/mic-icon.tsx b/studio/frontend/src/lib/mic-icon.tsx
new file mode 100644
index 0000000000..9ff6817845
--- /dev/null
+++ b/studio/frontend/src/lib/mic-icon.tsx
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+
+import type { FC } from "react";
+
+/** Microphone icon used by the chat composer and the Voice settings tab. */
+export const MicIcon: FC<{ className?: string }> = ({ className }) => (
+