diff --git a/preload.js b/preload.js index 32ce6c951..02af4c33c 100644 --- a/preload.js +++ b/preload.js @@ -355,6 +355,10 @@ contextBridge.exposeInMainWorld("electronAPI", { saveMistralKey: (key) => ipcRenderer.invoke("save-mistral-key", key), proxyMistralTranscription: (data) => ipcRenderer.invoke("proxy-mistral-transcription", data), + // Smallest AI API + getSmallestAiKey: () => ipcRenderer.invoke("get-smallest-ai-key"), + saveSmallestAiKey: (key) => ipcRenderer.invoke("save-smallest-ai-key", key), + // Custom endpoint API keys getCustomTranscriptionKey: () => ipcRenderer.invoke("get-custom-transcription-key"), saveCustomTranscriptionKey: (key) => ipcRenderer.invoke("save-custom-transcription-key", key), diff --git a/src/assets/icons/providers/smallest-ai.png b/src/assets/icons/providers/smallest-ai.png new file mode 100644 index 000000000..ec3992ba4 Binary files /dev/null and b/src/assets/icons/providers/smallest-ai.png differ diff --git a/src/components/OnboardingFlow.tsx b/src/components/OnboardingFlow.tsx index a82dafc9e..9c57af5b6 100644 --- a/src/components/OnboardingFlow.tsx +++ b/src/components/OnboardingFlow.tsx @@ -85,6 +85,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { openaiApiKey, groqApiKey, mistralApiKey, + smallestAiApiKey, customTranscriptionApiKey, setCustomTranscriptionApiKey, dictationKey, @@ -94,6 +95,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { setOpenaiApiKey, setGroqApiKey, setMistralApiKey, + setSmallestAiApiKey, updateTranscriptionSettings, preferredLanguage, } = useSettings(); @@ -510,6 +512,8 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { setGroqApiKey={setGroqApiKey} mistralApiKey={mistralApiKey} setMistralApiKey={setMistralApiKey} + smallestAiApiKey={smallestAiApiKey} + setSmallestAiApiKey={setSmallestAiApiKey} customTranscriptionApiKey={customTranscriptionApiKey} setCustomTranscriptionApiKey={setCustomTranscriptionApiKey} cloudTranscriptionBaseUrl={cloudTranscriptionBaseUrl} @@ -670,6 +674,8 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { return groqApiKey.trim().length > 0; } else if (cloudTranscriptionProvider === "mistral") { return mistralApiKey.trim().length > 0; + } else if (cloudTranscriptionProvider === "smallest") { + return smallestAiApiKey.trim().length > 0; } else if (cloudTranscriptionProvider === "custom") { // Custom can work without API key for local endpoints return true; diff --git a/src/components/SettingsPage.tsx b/src/components/SettingsPage.tsx index b758569cb..4ddb880bd 100644 --- a/src/components/SettingsPage.tsx +++ b/src/components/SettingsPage.tsx @@ -196,6 +196,8 @@ interface TranscriptionSectionProps { setGroqApiKey: (key: string) => void; mistralApiKey: string; setMistralApiKey: (key: string) => void; + smallestAiApiKey: string; + setSmallestAiApiKey: (key: string) => void; customTranscriptionApiKey: string; setCustomTranscriptionApiKey: (key: string) => void; cloudTranscriptionBaseUrl?: string; @@ -238,6 +240,8 @@ function TranscriptionSection({ setGroqApiKey, mistralApiKey, setMistralApiKey, + smallestAiApiKey, + setSmallestAiApiKey, customTranscriptionApiKey, setCustomTranscriptionApiKey, cloudTranscriptionBaseUrl, @@ -357,6 +361,8 @@ function TranscriptionSection({ setGroqApiKey={setGroqApiKey} mistralApiKey={mistralApiKey} setMistralApiKey={setMistralApiKey} + smallestAiApiKey={smallestAiApiKey} + setSmallestAiApiKey={setSmallestAiApiKey} customTranscriptionApiKey={customTranscriptionApiKey} setCustomTranscriptionApiKey={setCustomTranscriptionApiKey} cloudTranscriptionBaseUrl={cloudTranscriptionBaseUrl} @@ -789,6 +795,7 @@ export default function SettingsPage({ geminiApiKey, groqApiKey, mistralApiKey, + smallestAiApiKey, dictationKey, activationMode, setActivationMode, @@ -813,6 +820,7 @@ export default function SettingsPage({ setGeminiApiKey, setGroqApiKey, setMistralApiKey, + setSmallestAiApiKey, customTranscriptionApiKey, setCustomTranscriptionApiKey, customReasoningApiKey, @@ -3208,6 +3216,8 @@ EOF`, setGroqApiKey={setGroqApiKey} mistralApiKey={mistralApiKey} setMistralApiKey={setMistralApiKey} + smallestAiApiKey={smallestAiApiKey} + setSmallestAiApiKey={setSmallestAiApiKey} customTranscriptionApiKey={customTranscriptionApiKey} setCustomTranscriptionApiKey={setCustomTranscriptionApiKey} cloudTranscriptionBaseUrl={cloudTranscriptionBaseUrl} diff --git a/src/components/TranscriptionModelPicker.tsx b/src/components/TranscriptionModelPicker.tsx index 44592f671..e09712c4e 100644 --- a/src/components/TranscriptionModelPicker.tsx +++ b/src/components/TranscriptionModelPicker.tsx @@ -195,6 +195,8 @@ interface TranscriptionModelPickerProps { setGroqApiKey: (key: string) => void; mistralApiKey: string; setMistralApiKey: (key: string) => void; + smallestAiApiKey: string; + setSmallestAiApiKey: (key: string) => void; customTranscriptionApiKey?: string; setCustomTranscriptionApiKey?: (key: string) => void; cloudTranscriptionBaseUrl?: string; @@ -209,6 +211,7 @@ const CLOUD_PROVIDER_TABS = [ { id: "openai", name: "OpenAI" }, { id: "groq", name: "Groq" }, { id: "mistral", name: "Mistral" }, + { id: "smallest", name: "Smallest AI" }, { id: "custom", name: "Custom" }, ]; @@ -272,6 +275,8 @@ export default function TranscriptionModelPicker({ setGroqApiKey, mistralApiKey, setMistralApiKey, + smallestAiApiKey, + setSmallestAiApiKey, customTranscriptionApiKey = "", setCustomTranscriptionApiKey, cloudTranscriptionBaseUrl = "", @@ -878,6 +883,7 @@ export default function TranscriptionModelPicker({ groq: "https://console.groq.com/keys", mistral: "https://console.mistral.ai/api-keys", openai: "https://platform.openai.com/api-keys", + smallest: "https://app.smallest.ai/", }[selectedCloudProvider] || "https://platform.openai.com/api-keys" )} className="text-xs text-primary/70 hover:text-primary transition-colors cursor-pointer" @@ -887,14 +893,20 @@ export default function TranscriptionModelPicker({ { + return this.environmentManager.getSmallestAiKey(); + }); + + ipcMain.handle("save-smallest-ai-key", async (event, key) => { + return this.environmentManager.saveSmallestAiKey(key); + }); + ipcMain.handle( "proxy-mistral-transcription", async (event, { audioBuffer, model, language, contextBias }) => { @@ -3484,6 +3494,9 @@ class IPCHandlers { } else if (provider === "mistral") { apiKey = this.environmentManager.getMistralKey(); endpoint = MISTRAL_TRANSCRIPTION_URL; + } else if (provider === "smallest") { + apiKey = this.environmentManager.getSmallestAiKey(); + endpoint = SMALLEST_AI_TRANSCRIPTION_URL; } else if (provider === "custom") { apiKey = this.environmentManager.getCustomTranscriptionKey(); const base = (settings?.cloudTranscriptionBaseUrl || "").trim(); @@ -3500,24 +3513,47 @@ class IPCHandlers { throw new Error(`${provider} API key not configured`); } - const formData = new FormData(); - formData.append("file", new Blob([buffer], { type: "audio/webm" }), "audio.webm"); - formData.append("model", model); - const headers = {}; - if (provider === "mistral") { - headers["x-api-key"] = apiKey; - } else if (apiKey) { - headers.Authorization = `Bearer ${apiKey}`; - } + if (provider === "smallest") { + const language = settings?.language || "en"; + const params = new URLSearchParams({ language }); + const smallestHeaders = { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/octet-stream", + "X-Source": "openwhispr", + }; + const smallestResponse = await fetch(`${endpoint}?${params}`, { + method: "POST", + headers: smallestHeaders, + body: buffer, + }); + if (!smallestResponse.ok) { + const errorText = await smallestResponse.text(); + throw new Error(`Smallest AI API Error: ${smallestResponse.status} ${errorText}`); + } + const smallestData = await smallestResponse.json(); + if (smallestData?.transcription) { + result = { text: smallestData.transcription, source: provider, model }; + } + } else { + const formData = new FormData(); + formData.append("file", new Blob([buffer], { type: "audio/webm" }), "audio.webm"); + formData.append("model", model); + const headers = {}; + if (provider === "mistral") { + headers["x-api-key"] = apiKey; + } else if (apiKey) { + headers.Authorization = `Bearer ${apiKey}`; + } - const response = await fetch(endpoint, { method: "POST", headers, body: formData }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`${provider} API Error: ${response.status} ${errorText}`); - } - const data = await response.json(); - if (data?.text) { - result = { text: data.text, source: provider, model }; + const response = await fetch(endpoint, { method: "POST", headers, body: formData }); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`${provider} API Error: ${response.status} ${errorText}`); + } + const data = await response.json(); + if (data?.text) { + result = { text: data.text, source: provider, model }; + } } } diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 67c435548..c7acf5b09 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -54,6 +54,7 @@ export interface ApiKeySettings { geminiApiKey: string; groqApiKey: string; mistralApiKey: string; + smallestAiApiKey: string; customTranscriptionApiKey: string; customReasoningApiKey: string; } @@ -205,6 +206,7 @@ function useSettingsInternal() { geminiApiKey: store.geminiApiKey, groqApiKey: store.groqApiKey, mistralApiKey: store.mistralApiKey, + smallestAiApiKey: store.smallestAiApiKey, dictationKey: store.dictationKey, meetingKey: store.meetingKey, theme: store.theme, @@ -238,6 +240,7 @@ function useSettingsInternal() { setGeminiApiKey: store.setGeminiApiKey, setGroqApiKey: store.setGroqApiKey, setMistralApiKey: store.setMistralApiKey, + setSmallestAiApiKey: store.setSmallestAiApiKey, customTranscriptionApiKey: store.customTranscriptionApiKey, setCustomTranscriptionApiKey: store.setCustomTranscriptionApiKey, customReasoningApiKey: store.customReasoningApiKey, diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json index 2b8827656..3c64854fb 100644 --- a/src/locales/de/translation.json +++ b/src/locales/de/translation.json @@ -1572,7 +1572,8 @@ "openai_whisper_1": "Originales Whisper-Modell", "groq_whisper_large_v3": "Hochpräzise Spracherkennung", "groq_whisper_large_v3_turbo": "216x Echtzeitgeschwindigkeit", - "mistral_voxtral_mini_latest": "Schnelle mehrsprachige Transkription" + "mistral_voxtral_mini_latest": "Schnelle mehrsprachige Transkription", + "smallest_pulse": "Schnelle und präzise Spracherkennung" }, "cloud": { "openai_gpt_5_4": "Frontier-Modell für komplexes Reasoning", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index de7078fcb..5abf33b91 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1672,7 +1672,8 @@ "openai_whisper_1": "Original Whisper model", "groq_whisper_large_v3": "High accuracy speech recognition", "groq_whisper_large_v3_turbo": "216x real-time speed", - "mistral_voxtral_mini_latest": "Fast multilingual transcription" + "mistral_voxtral_mini_latest": "Fast multilingual transcription", + "smallest_pulse": "Fast and accurate speech-to-text" }, "cloud": { "openai_gpt_5_4": "Frontier model for complex reasoning", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 45a8b6495..33e9d904b 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -1620,7 +1620,8 @@ "openai_whisper_1": "Modelo Whisper original", "groq_whisper_large_v3": "Reconocimiento de voz de alta precisión", "groq_whisper_large_v3_turbo": "Velocidad 216x en tiempo real", - "mistral_voxtral_mini_latest": "Transcripción multilingüe rápida" + "mistral_voxtral_mini_latest": "Transcripción multilingüe rápida", + "smallest_pulse": "Voz a texto rápido y preciso" }, "cloud": { "openai_gpt_5_4": "Modelo frontier para razonamiento complejo", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index e93234ccd..1b9ea9ec7 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -1620,7 +1620,8 @@ "openai_whisper_1": "Modèle Whisper original", "groq_whisper_large_v3": "Reconnaissance vocale haute précision", "groq_whisper_large_v3_turbo": "Vitesse 216x en temps réel", - "mistral_voxtral_mini_latest": "Transcription multilingue rapide" + "mistral_voxtral_mini_latest": "Transcription multilingue rapide", + "smallest_pulse": "Transcription vocale rapide et précise" }, "cloud": { "openai_gpt_5_4": "Modèle frontier pour le raisonnement complexe", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index fd93a0c00..d82096a69 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1572,7 +1572,8 @@ "openai_whisper_1": "Modello Whisper originale", "groq_whisper_large_v3_turbo": "Velocità 216x in tempo reale", "groq_whisper_large_v3": "Modello Large v3, veloce e preciso", - "mistral_voxtral_mini_latest": "Trascrizione multilingue veloce" + "mistral_voxtral_mini_latest": "Trascrizione multilingue veloce", + "smallest_pulse": "Trascrizione vocale rapida e precisa" }, "cloud": { "openai_gpt_5_4": "Modello frontier per ragionamento complesso", diff --git a/src/locales/ja/translation.json b/src/locales/ja/translation.json index 03055cfee..33ef38295 100644 --- a/src/locales/ja/translation.json +++ b/src/locales/ja/translation.json @@ -1572,7 +1572,8 @@ "openai_whisper_1": "オリジナル Whisper モデル", "groq_whisper_large_v3_turbo": "リアルタイムの 216 倍速", "groq_whisper_large_v3": "Large v3 モデル、高速かつ高精度", - "mistral_voxtral_mini_latest": "高速多言語文字起こし" + "mistral_voxtral_mini_latest": "高速多言語文字起こし", + "smallest_pulse": "高速で正確な音声テキスト変換" }, "cloud": { "openai_gpt_5_4": "複雑な推論のためのフロンティアモデル", diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index b15fc6191..85f6fdde4 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -1544,7 +1544,8 @@ "openai_whisper_1": "Modelo Whisper original", "groq_whisper_large_v3_turbo": "Velocidade 216x em tempo real", "groq_whisper_large_v3": "Modelo Large v3, rápido e preciso", - "mistral_voxtral_mini_latest": "Transcrição multilíngue rápida" + "mistral_voxtral_mini_latest": "Transcrição multilíngue rápida", + "smallest_pulse": "Fala para texto rápido e preciso" }, "cloud": { "openai_gpt_5_4": "Modelo frontier para raciocínio complexo", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 80bf4f665..da3f28e6b 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -1572,7 +1572,8 @@ "openai_whisper_1": "Оригинальная модель Whisper", "groq_whisper_large_v3_turbo": "Скорость в 216 раз быстрее реального времени", "groq_whisper_large_v3": "Модель Large v3, быстрая и точная", - "mistral_voxtral_mini_latest": "Быстрая многоязычная транскрипция" + "mistral_voxtral_mini_latest": "Быстрая многоязычная транскрипция", + "smallest_pulse": "Быстрое и точное распознавание речи" }, "cloud": { "openai_gpt_5_4": "Фронтирная модель для сложного рассуждения", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index dbd6874e0..327ab6be0 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -1567,7 +1567,8 @@ "openai_whisper_1": "原始 Whisper 模型", "groq_whisper_large_v3_turbo": "216 倍实时速度", "groq_whisper_large_v3": "Large v3 模型,快速且精准", - "mistral_voxtral_mini_latest": "快速多语言转录" + "mistral_voxtral_mini_latest": "快速多语言转录", + "smallest_pulse": "快速准确的语音转文字" }, "cloud": { "openai_gpt_5_4": "复杂推理前沿模型", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index 8b076c4cb..fd6aa3094 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -1567,7 +1567,8 @@ "openai_whisper_1": "原始 Whisper 模型", "groq_whisper_large_v3_turbo": "216 倍即時速度", "groq_whisper_large_v3": "Large v3 模型,快速且精準", - "mistral_voxtral_mini_latest": "快速多語言轉錄" + "mistral_voxtral_mini_latest": "快速多語言轉錄", + "smallest_pulse": "快速準確的語音轉文字" }, "cloud": { "openai_gpt_5_4": "複雜推理前沿模型", diff --git a/src/models/modelRegistryData.json b/src/models/modelRegistryData.json index 7cda8adc5..31cbaa575 100644 --- a/src/models/modelRegistryData.json +++ b/src/models/modelRegistryData.json @@ -180,6 +180,19 @@ "descriptionKey": "models.descriptions.transcription.mistral_voxtral_mini_latest" } ] + }, + { + "id": "smallest", + "name": "Smallest AI", + "baseUrl": "https://api.smallest.ai/waves/v1", + "models": [ + { + "id": "pulse", + "name": "Pulse", + "description": "Fast and accurate speech-to-text", + "descriptionKey": "models.descriptions.transcription.smallest_pulse" + } + ] } ], "cloudProviders": [ diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index d7e14a094..cf85ca04a 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -329,6 +329,7 @@ export interface SettingsState setGeminiApiKey: (key: string) => void; setGroqApiKey: (key: string) => void; setMistralApiKey: (key: string) => void; + setSmallestAiApiKey: (key: string) => void; setCustomTranscriptionApiKey: (key: string) => void; setCustomReasoningApiKey: (key: string) => void; @@ -489,6 +490,7 @@ export const useSettingsStore = create()((set, get) => ({ geminiApiKey: readString("geminiApiKey", ""), groqApiKey: readString("groqApiKey", ""), mistralApiKey: readString("mistralApiKey", ""), + smallestAiApiKey: readString("smallestAiApiKey", ""), customTranscriptionApiKey: readString("customTranscriptionApiKey", ""), customReasoningApiKey: readString("customReasoningApiKey", ""), @@ -785,6 +787,12 @@ export const useSettingsStore = create()((set, get) => ({ window.electronAPI?.saveMistralKey?.(key); invalidateApiKeyCaches("mistral"); }, + setSmallestAiApiKey: (key: string) => { + if (isBrowser) localStorage.setItem("smallestAiApiKey", key); + set({ smallestAiApiKey: key }); + window.electronAPI?.saveSmallestAiKey?.(key); + invalidateApiKeyCaches("smallest"); + }, setCustomTranscriptionApiKey: (key: string) => { if (isBrowser) localStorage.setItem("customTranscriptionApiKey", key); set({ customTranscriptionApiKey: key }); @@ -1089,6 +1097,7 @@ export const useSettingsStore = create()((set, get) => ({ if (keys.geminiApiKey !== undefined) s.setGeminiApiKey(keys.geminiApiKey); if (keys.groqApiKey !== undefined) s.setGroqApiKey(keys.groqApiKey); if (keys.mistralApiKey !== undefined) s.setMistralApiKey(keys.mistralApiKey); + if (keys.smallestAiApiKey !== undefined) s.setSmallestAiApiKey(keys.smallestAiApiKey); if (keys.customTranscriptionApiKey !== undefined) s.setCustomTranscriptionApiKey(keys.customTranscriptionApiKey); if (keys.customReasoningApiKey !== undefined) @@ -1231,6 +1240,10 @@ export async function initializeSettings(): Promise { const envKey = await window.electronAPI.getMistralKey?.(); if (envKey) createStringSetter("mistralApiKey")(envKey); } + if (!state.smallestAiApiKey) { + const envKey = await window.electronAPI.getSmallestAiKey?.(); + if (envKey) createStringSetter("smallestAiApiKey")(envKey); + } if (!state.customTranscriptionApiKey) { const envKey = await window.electronAPI.getCustomTranscriptionKey?.(); if (envKey) createStringSetter("customTranscriptionApiKey")(envKey); diff --git a/src/utils/byokDetection.ts b/src/utils/byokDetection.ts index a7b989d51..eb1f0718a 100644 --- a/src/utils/byokDetection.ts +++ b/src/utils/byokDetection.ts @@ -3,5 +3,6 @@ export const hasStoredByokKey = () => localStorage.getItem("openaiApiKey") || localStorage.getItem("groqApiKey") || localStorage.getItem("mistralApiKey") || + localStorage.getItem("smallestAiApiKey") || localStorage.getItem("customTranscriptionApiKey") ); diff --git a/src/utils/providerIcons.ts b/src/utils/providerIcons.ts index faa6e7421..5fa4d5d35 100644 --- a/src/utils/providerIcons.ts +++ b/src/utils/providerIcons.ts @@ -11,6 +11,7 @@ import gemmaIcon from "@/assets/icons/providers/gemma.svg"; import bedrockIcon from "@/assets/icons/providers/bedrock.svg"; import azureIcon from "@/assets/icons/providers/azure.svg"; import vertexIcon from "@/assets/icons/providers/vertex.svg"; +import smallestAiIcon from "@/assets/icons/providers/smallest-ai.png"; export const PROVIDER_ICONS: Record = { openai: openaiIcon, @@ -27,6 +28,7 @@ export const PROVIDER_ICONS: Record = { bedrock: bedrockIcon, azure: azureIcon, vertex: vertexIcon, + smallest: smallestAiIcon, }; export function getProviderIcon(provider: string): string | undefined {