From a16ef2346f459b08009f02e27800460af2bb546b Mon Sep 17 00:00:00 2001 From: claude_code Date: Mon, 22 Jun 2026 02:29:07 +0300 Subject: [PATCH] feat(ai/stt): add dictation language selection to STT settings Add a per-workspace `sttLanguage` setting (ISO-639-1 hint; empty = auto-detect) and a searchable language picker in the Voice / STT settings card. The hint is forwarded to the transcription endpoint: - multipart path via the AI SDK `providerOptions.openai.language` - JSON (OpenRouter) path via a top-level `language` body field only when non-empty, so auto-detect behaves exactly as before. Threaded through the whole stack: ai.types, update DTO, AiSettingsService (resolve/getMasked/update), the workspace.repo SQL allowlist, the client ai-settings service types, and the provider-settings form. Adds en-US source keys and ru-RU translations. Co-Authored-By: Claude Opus 4.8 --- .../public/locales/en-US/translation.json | 3 + .../public/locales/ru-RU/translation.json | 5 +- .../components/ai-provider-settings.tsx | 55 +++++++++++++++++++ .../workspace/services/ai-settings-service.ts | 4 ++ .../repos/workspace/workspace.repo.ts | 2 +- .../integrations/ai/ai-settings.service.ts | 6 ++ apps/server/src/integrations/ai/ai.service.ts | 27 ++++++++- apps/server/src/integrations/ai/ai.types.ts | 4 ++ .../ai/dto/update-ai-settings.dto.ts | 5 ++ 9 files changed, 106 insertions(+), 5 deletions(-) diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index fc39a8d9..f1dac8c2 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -1221,6 +1221,9 @@ "How transcription requests are sent to the endpoint": "How transcription requests are sent to the endpoint", "OpenAI-compatible (multipart/form-data)": "OpenAI-compatible (multipart/form-data)", "OpenRouter (JSON, base64 audio)": "OpenRouter (JSON, base64 audio)", + "Dictation language": "Dictation language", + "Auto-detect": "Auto-detect", + "Spoken language hint sent to the transcription model. Auto-detect lets the model decide.": "Spoken language hint sent to the transcription model. Auto-detect lets the model decide.", "Agent role": "Agent role", "Universal assistant": "Universal assistant", "Add role": "Add role", diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index 73a68aa4..6bb76aad 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -1123,5 +1123,8 @@ "Added {{name}} to favorites": "{{name}} добавлено в избранное", "Removed {{name}} from favorites": "{{name}} удалено из избранного", "Page menu for {{name}}": "Меню страницы для {{name}}", - "Create subpage of {{name}}": "Создать подстраницу для {{name}}" + "Create subpage of {{name}}": "Создать подстраницу для {{name}}", + "Dictation language": "Язык диктовки", + "Auto-detect": "Автоопределение", + "Spoken language hint sent to the transcription model. Auto-detect lets the model decide.": "Подсказка языка речи для модели транскрипции. «Автоопределение» оставляет выбор за моделью." } diff --git a/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx b/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx index 4726a0ef..63500797 100644 --- a/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx +++ b/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx @@ -42,6 +42,40 @@ import { useAiRolesQuery } from "@/features/ai-chat/queries/ai-chat-query.ts"; import { IAiRole } from "@/features/ai-chat/types/ai-chat.types.ts"; import AiMcpServers from "./ai-mcp-servers.tsx"; +// Curated ISO-639-1 dictation languages for the STT card. The empty-value +// "Auto-detect" entry is prepended in render (it needs translation). Values +// are sent verbatim to the transcription model as the language hint. +const STT_LANGUAGE_OPTIONS: { value: string; label: string }[] = [ + { value: "en", label: "English" }, + { value: "ru", label: "Russian — Русский" }, + { value: "uk", label: "Ukrainian — Українська" }, + { value: "de", label: "German — Deutsch" }, + { value: "fr", label: "French — Français" }, + { value: "es", label: "Spanish — Español" }, + { value: "it", label: "Italian — Italiano" }, + { value: "pt", label: "Portuguese — Português" }, + { value: "nl", label: "Dutch — Nederlands" }, + { value: "pl", label: "Polish — Polski" }, + { value: "tr", label: "Turkish — Türkçe" }, + { value: "cs", label: "Czech — Čeština" }, + { value: "sv", label: "Swedish — Svenska" }, + { value: "fi", label: "Finnish — Suomi" }, + { value: "da", label: "Danish — Dansk" }, + { value: "no", label: "Norwegian — Norsk" }, + { value: "ro", label: "Romanian — Română" }, + { value: "hu", label: "Hungarian — Magyar" }, + { value: "el", label: "Greek — Ελληνικά" }, + { value: "he", label: "Hebrew — עברית" }, + { value: "ar", label: "Arabic — العربية" }, + { value: "hi", label: "Hindi — हिन्दी" }, + { value: "id", label: "Indonesian — Bahasa Indonesia" }, + { value: "vi", label: "Vietnamese — Tiếng Việt" }, + { value: "th", label: "Thai — ไทย" }, + { value: "ja", label: "Japanese — 日本語" }, + { value: "ko", label: "Korean — 한국어" }, + { value: "zh", label: "Chinese — 中文" }, +]; + // No driver field: every endpoint is OpenAI-compatible, so the form carries only // the user-editable fields. `apiKey` / `embeddingApiKey` are write-only buffers // (empty means "leave unchanged" unless explicitly cleared). @@ -63,6 +97,8 @@ const formSchema = z.object({ sttModel: z.string(), sttBaseUrl: z.string(), sttApiStyle: z.enum(["multipart", "json"]), + // ISO-639-1 dictation language; empty = auto-detect. + sttLanguage: z.string(), sttApiKey: z.string(), }); @@ -233,6 +269,7 @@ export default function AiProviderSettings() { sttModel: "", sttBaseUrl: "", sttApiStyle: "multipart" as SttApiStyle, + sttLanguage: "", sttApiKey: "", }, }); @@ -254,6 +291,7 @@ export default function AiProviderSettings() { sttModel: settings.sttModel ?? "", sttBaseUrl: settings.sttBaseUrl ?? "", sttApiStyle: settings.sttApiStyle ?? "multipart", + sttLanguage: settings.sttLanguage ?? "", sttApiKey: "", }); form.resetDirty(); @@ -288,6 +326,7 @@ export default function AiProviderSettings() { sttModel: values.sttModel, sttBaseUrl: values.sttBaseUrl, sttApiStyle: values.sttApiStyle, + sttLanguage: values.sttLanguage, }; // Key semantics (never send the stored key back) — see resolveKeyField: @@ -923,6 +962,22 @@ export default function AiProviderSettings() { {...form.getInputProps("sttApiStyle")} /> +