diff --git a/Cargo.lock b/Cargo.lock index 6babc0bf17..52d6821222 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,7 @@ dependencies = [ "reqwest 0.12.22", "serde", "serde_json", + "specta", "thiserror 2.0.12", "tokio", ] diff --git a/apps/desktop/src/components/license.tsx b/apps/desktop/src/components/license.tsx index 6ad472389c..9d850f1c84 100644 --- a/apps/desktop/src/components/license.tsx +++ b/apps/desktop/src/components/license.tsx @@ -1,26 +1,42 @@ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useLicense } from "@/hooks/use-license"; +const REFRESH_INTERVAL = 30 * 60 * 1000; +const INITIAL_DELAY = 5000; +const RATE_LIMIT = 60 * 60 * 1000; + export function LicenseRefreshProvider({ children }: { children: React.ReactNode }) { - const { shouldRefresh, refreshLicense, getLicense } = useLicense(); + const { getLicenseStatus, refreshLicense, getLicense } = useLicense(); + const lastRefreshAttempt = useRef(0); useEffect(() => { if (getLicense.isLoading) { return; } - const checkAndRefresh = () => { - if (shouldRefresh() && !refreshLicense.isPending) { + const attemptRefresh = () => { + const status = getLicenseStatus(); + const now = Date.now(); + + if (refreshLicense.isPending || now - lastRefreshAttempt.current < RATE_LIMIT) { + return; + } + + if (!status.isValid || status.needsRefresh) { + lastRefreshAttempt.current = now; refreshLicense.mutate(); } }; - checkAndRefresh(); - const interval = setInterval(checkAndRefresh, 1000 * 60 * 10); // 10min + const timeout = setTimeout(attemptRefresh, INITIAL_DELAY); + const interval = setInterval(attemptRefresh, REFRESH_INTERVAL); - return () => clearInterval(interval); - }, [shouldRefresh, refreshLicense, getLicense.isLoading, refreshLicense.isPending]); + return () => { + clearTimeout(timeout); + clearInterval(interval); + }; + }, [getLicense.isLoading, getLicenseStatus, refreshLicense.isPending]); return <>{children}; } diff --git a/apps/desktop/src/components/settings/components/ai/llm-custom-view.tsx b/apps/desktop/src/components/settings/components/ai/llm-custom-view.tsx index 4934d706d3..3e6b634310 100644 --- a/apps/desktop/src/components/settings/components/ai/llm-custom-view.tsx +++ b/apps/desktop/src/components/settings/components/ai/llm-custom-view.tsx @@ -1,4 +1,7 @@ import { Trans } from "@lingui/react/macro"; +import { useQuery } from "@tanstack/react-query"; +import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; +import useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback"; import { useEffect } from "react"; import { @@ -13,9 +16,6 @@ import { import { Input } from "@hypr/ui/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; import { cn } from "@hypr/ui/lib/utils"; -import { useQuery } from "@tanstack/react-query"; -import { fetch as tauriFetch } from "@tauri-apps/plugin-http"; -import useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback"; import { useState } from "react"; import { SharedCustomEndpointProps } from "./shared"; @@ -48,14 +48,11 @@ const openrouterModels = [ export function LLMCustomView({ customLLMEnabled, - selectedLLMModel, setSelectedLLMModel, setCustomLLMEnabledMutation, configureCustomEndpoint, openAccordion, setOpenAccordion, - customLLMConnection, - getCustomLLMModel, openaiForm, geminiForm, openrouterForm, @@ -230,12 +227,6 @@ export function LLMCustomView({ return (
-
-

- Custom Endpoints -

-
-
{/* OpenAI Accordion */}
localLlmCommands.getCurrentModel(), }); + const handleShowFileLocation = async () => { + localLlmCommands.modelsDir().then((path) => openPath(path)); + }; + useEffect(() => { if (currentLLMModel.data && !customLLMEnabled.data) { setSelectedLLMModel(currentLLMModel.data); } }, [currentLLMModel.data, customLLMEnabled.data, setSelectedLLMModel]); + const handleLocalModelSelection = (model: LLMModel) => { + if (model.available && model.downloaded) { + setSelectedLLMModel(model.key); + localLlmCommands.setCurrentModel(model.key as SupportedModel); + // CRITICAL: Disable custom LLM when local model is selected + setCustomLLMEnabledMutation.mutate(false); + localLlmCommands.restartServer(); + } + }; + return (
-
-

- Local Models -

-
-
{llmModelsState.map((model) => (
handleLocalModelSelection(model)} className={cn( "group relative p-3 rounded-lg border-2 transition-all flex items-center justify-between", selectedLLMModel === model.key && model.available && model.downloaded && !customLLMEnabled.data @@ -51,15 +58,6 @@ export function LLMLocalView({ ? "border-dashed border-gray-300 hover:border-gray-400 bg-white cursor-pointer" : "border-dashed border-gray-200 bg-gray-50 cursor-not-allowed", )} - onClick={() => { - if (model.available && model.downloaded) { - setSelectedLLMModel(model.key); - localLlmCommands.setCurrentModel(model.key as SupportedModel); - // CRITICAL: Disable custom LLM when local model is selected - setCustomLLMEnabledMutation.mutate(false); - localLlmCommands.restartServer(); - } - }} >
@@ -96,10 +94,7 @@ export function LLMLocalView({ )}
- - {!model.available && ( -
-
-
Coming Soon
-
Feature in development
-
-
- )}
))}
diff --git a/apps/desktop/src/components/settings/components/ai/shared.tsx b/apps/desktop/src/components/settings/components/ai/shared.tsx index b0b7d8a0f3..ef7424808c 100644 --- a/apps/desktop/src/components/settings/components/ai/shared.tsx +++ b/apps/desktop/src/components/settings/components/ai/shared.tsx @@ -1,8 +1,10 @@ import { Connection } from "@hypr/plugin-connector"; -import { cn } from "@hypr/ui/lib/utils"; +import { type SupportedModel } from "@hypr/plugin-local-llm"; import { UseMutationResult, UseQueryResult } from "@tanstack/react-query"; import { UseFormReturn } from "react-hook-form"; +import { cn } from "@hypr/ui/lib/utils"; + export const RatingDisplay = ( { label, rating, maxRating = 3, icon: Icon }: { label: string; @@ -42,7 +44,7 @@ export const LanguageDisplay = ({ support }: { support: "multilingual" | "englis }; export interface LLMModel { - key: string; + key: SupportedModel; name: string; description: string; available: boolean; @@ -53,8 +55,6 @@ export interface LLMModel { export interface STTModel { key: string; name: string; - accuracy: number; - speed: number; size: string; downloaded: boolean; fileName: string; @@ -95,7 +95,6 @@ export interface SharedSTTProps { setSttModels: React.Dispatch>; downloadingModels: Set; handleModelDownload: (modelKey: string) => Promise; - handleShowFileLocation: (modelType: "stt" | "llm") => Promise; } export interface SharedLLMProps { @@ -113,7 +112,6 @@ export interface SharedLLMProps { // Functions handleModelDownload: (modelKey: string) => Promise; - handleShowFileLocation: (modelType: "stt" | "llm") => Promise; } export interface SharedCustomEndpointProps extends SharedLLMProps { diff --git a/apps/desktop/src/components/settings/components/ai/stt-view-local.tsx b/apps/desktop/src/components/settings/components/ai/stt-view-local.tsx new file mode 100644 index 0000000000..dd1e574c7b --- /dev/null +++ b/apps/desktop/src/components/settings/components/ai/stt-view-local.tsx @@ -0,0 +1,393 @@ +import { useQuery } from "@tanstack/react-query"; +import { openPath } from "@tauri-apps/plugin-opener"; +import { arch, platform } from "@tauri-apps/plugin-os"; +import { DownloadIcon, FolderIcon } from "lucide-react"; +import { useEffect, useMemo } from "react"; + +import { commands as localSttCommands, type WhisperModel } from "@hypr/plugin-local-stt"; +import { Button } from "@hypr/ui/components/ui/button"; +import { cn } from "@hypr/ui/lib/utils"; +import { SharedSTTProps, STTModel } from "./shared"; + +export const sttModelMetadata: Record = { + "QuantizedTiny": { + name: "Tiny", + description: "Fastest, lowest accuracy. Good for offline, low-resource use.", + intelligence: 1, + speed: 3, + size: "44 MB", + inputType: ["audio"], + outputType: ["text"], + languageSupport: "multilingual", + huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-tiny-q8_0.bin", + }, + "QuantizedTinyEn": { + name: "Tiny - English", + description: "Fastest, English-only. Optimized for speed on English audio.", + intelligence: 1, + speed: 3, + size: "44 MB", + inputType: ["audio"], + outputType: ["text"], + languageSupport: "english-only", + huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-tiny.en-q8_0.bin", + }, + "QuantizedBase": { + name: "Base", + description: "Good balance of speed and accuracy for multilingual use.", + intelligence: 2, + speed: 2, + size: "82 MB", + inputType: ["audio"], + outputType: ["text"], + languageSupport: "multilingual", + huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-base-q8_0.bin", + }, + "QuantizedBaseEn": { + name: "Base - English", + description: "Balanced speed and accuracy, optimized for English audio.", + intelligence: 2, + speed: 2, + size: "82 MB", + inputType: ["audio"], + outputType: ["text"], + languageSupport: "english-only", + huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-base.en-q8_0.bin", + }, + "QuantizedSmall": { + name: "Small", + description: "Higher accuracy, moderate speed for multilingual transcription.", + intelligence: 2, + speed: 2, + size: "264 MB", + inputType: ["audio"], + outputType: ["text"], + languageSupport: "multilingual", + huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-small-q8_0.bin", + }, + "QuantizedSmallEn": { + name: "Small - English", + description: "Higher accuracy, moderate speed, optimized for English audio.", + intelligence: 3, + speed: 2, + size: "264 MB", + inputType: ["audio"], + outputType: ["text"], + languageSupport: "english-only", + huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-small.en-q8_0.bin", + }, + "QuantizedLargeTurbo": { + name: "Large", + description: "Highest accuracy, resource intensive. Only for Mac Pro M4 and above.", + intelligence: 3, + speed: 1, + size: "874 MB", + inputType: ["audio"], + outputType: ["text"], + languageSupport: "multilingual", + huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-large-v3-turbo-q8_0.bin", + }, +}; + +interface STTViewProps extends SharedSTTProps { + isWerModalOpen: boolean; + setIsWerModalOpen: (open: boolean) => void; +} + +export function STTViewLocal({ + selectedSTTModel, + setSelectedSTTModel, + sttModels, + setSttModels, + downloadingModels, + handleModelDownload, +}: STTViewProps) { + const servers = useQuery({ + queryKey: ["local-stt-servers"], + queryFn: () => localSttCommands.getServers(), + refetchInterval: 1000, + }); + + const currentSTTModel = useQuery({ + queryKey: ["current-stt-model"], + queryFn: () => localSttCommands.getCurrentModel(), + }); + + useEffect(() => { + if (currentSTTModel.data) { + setSelectedSTTModel(currentSTTModel.data); + } + }, [currentSTTModel.data, setSelectedSTTModel]); + + const amAvailable = useMemo(() => platform() === "macos" && arch() === "aarch64", []); + + const sttModelDownloadStatus = useQuery({ + queryKey: ["stt-model-download-status"], + queryFn: async () => { + const statusChecks = await Promise.all([ + localSttCommands.isModelDownloaded("QuantizedTiny"), + localSttCommands.isModelDownloaded("QuantizedTinyEn"), + localSttCommands.isModelDownloaded("QuantizedBase"), + localSttCommands.isModelDownloaded("QuantizedBaseEn"), + localSttCommands.isModelDownloaded("QuantizedSmall"), + localSttCommands.isModelDownloaded("QuantizedSmallEn"), + localSttCommands.isModelDownloaded("QuantizedLargeTurbo"), + ]); + return { + "QuantizedTiny": statusChecks[0], + "QuantizedTinyEn": statusChecks[1], + "QuantizedBase": statusChecks[2], + "QuantizedBaseEn": statusChecks[3], + "QuantizedSmall": statusChecks[4], + "QuantizedSmallEn": statusChecks[5], + "QuantizedLargeTurbo": statusChecks[6], + } as Record; + }, + refetchInterval: 3000, + }); + + useEffect(() => { + if (sttModelDownloadStatus.data) { + setSttModels(prev => + prev.map(model => ({ + ...model, + downloaded: sttModelDownloadStatus.data[model.key] || false, + })) + ); + } + }, [sttModelDownloadStatus.data, setSttModels]); + + const defaultModelKeys = ["QuantizedSmall"]; + const otherModelKeys = [ + "QuantizedTiny", + "QuantizedTinyEn", + "QuantizedBase", + "QuantizedBaseEn", + "QuantizedSmallEn", + "QuantizedLargeTurbo", + ]; + + const modelsToShow = sttModels.filter(model => { + if (defaultModelKeys.includes(model.key)) { + return true; + } + + if (otherModelKeys.includes(model.key) && model.downloaded) { + return true; + } + + return false; + }); + + return ( +
+ + {amAvailable && ( + + )} +
+ ); +} + +function BasicModelsManagement({ + on, + modelsToShow, + selectedSTTModel, + setSelectedSTTModel, + downloadingModels, + handleModelDownload, +}: { + on: boolean; + modelsToShow: STTModel[]; + selectedSTTModel: string; + setSelectedSTTModel: (model: string) => void; + downloadingModels: Set; + handleModelDownload: (model: string) => void; +}) { + const handleShowFileLocation = async () => { + localSttCommands.modelsDir().then((path) => openPath(path)); + }; + + return ( +
+
+
+

Basic Models

+ +
+

Default inference mode powered by Whisper.cpp.

+
+ +
+ {modelsToShow.map((model) => ( + + ))} +
+
+ ); +} + +function ProModelsManagement({ on }: { on: boolean }) { + const proModels = useQuery({ + queryKey: ["pro-models"], + queryFn: () => localSttCommands.listProModels(), + }); + + return ( +
+
+
+
+

Pro Models

+ +
+

+ Only for pro plan users. Latency and resource optimized. (will be shipped in next few days) +

+
+ +
+ {proModels.data?.map((model) => ( + {}} + downloadingModels={new Set()} + handleModelDownload={() => {}} + handleShowFileLocation={() => {}} + /> + ))} +
+
+
+ ); +} + +function ModelEntry({ + model, + selectedSTTModel, + setSelectedSTTModel, + downloadingModels, + handleModelDownload, + handleShowFileLocation, + disabled, +}: { + model: STTModel; + selectedSTTModel: string; + setSelectedSTTModel: (model: string) => void; + downloadingModels: Set; + handleModelDownload: (model: string) => void; + handleShowFileLocation: () => void; + disabled?: boolean; +}) { + return ( +
{ + if (model.downloaded) { + setSelectedSTTModel(model.key as WhisperModel); + localSttCommands.setCurrentModel(model.key as WhisperModel); + localSttCommands.stopServer(null); + localSttCommands.startServer(null); + } + }} + > +
+
+

+ {model.name} +

+
+
+ +
+ {model.downloaded + ? ( + + ) + : downloadingModels.has(model.key) + ? ( + + ) + : ( + + )} +
+
+ ); +} diff --git a/apps/desktop/src/components/settings/components/ai/stt-view-remote.tsx b/apps/desktop/src/components/settings/components/ai/stt-view-remote.tsx new file mode 100644 index 0000000000..f309e0b10f --- /dev/null +++ b/apps/desktop/src/components/settings/components/ai/stt-view-remote.tsx @@ -0,0 +1,41 @@ +import { CloudIcon, ExternalLinkIcon } from "lucide-react"; + +export function STTViewRemote() { + return ( +
+
+ +

+ Custom Transcription +

+

+ Coming Soon +

+
+ +
+

+ Powered by{" "} + + Owhisper + + +

+

+ Interested in team features?{" "} + + Contact help@hyprnote.com + +

+
+
+ ); +} diff --git a/apps/desktop/src/components/settings/components/ai/stt-view.tsx b/apps/desktop/src/components/settings/components/ai/stt-view.tsx deleted file mode 100644 index 28139b775c..0000000000 --- a/apps/desktop/src/components/settings/components/ai/stt-view.tsx +++ /dev/null @@ -1,551 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { Trans } from "@lingui/react/macro"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { DownloadIcon, FolderIcon, InfoIcon } from "lucide-react"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -import { commands as dbCommands } from "@hypr/plugin-db"; -import { commands as localSttCommands, type WhisperModel } from "@hypr/plugin-local-stt"; -import { Button } from "@hypr/ui/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@hypr/ui/components/ui/form"; -import { Slider } from "@hypr/ui/components/ui/slider"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@hypr/ui/components/ui/tooltip"; -import { cn } from "@hypr/ui/lib/utils"; -import { WERPerformanceModal } from "../wer-modal"; -import { SharedSTTProps } from "./shared"; - -export const sttModelMetadata: Record = { - "QuantizedTiny": { - name: "Tiny", - description: "Fastest, lowest accuracy. Good for offline, low-resource use.", - intelligence: 1, - speed: 3, - size: "44 MB", - inputType: ["audio"], - outputType: ["text"], - languageSupport: "multilingual", - huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-tiny-q8_0.bin", - }, - "QuantizedTinyEn": { - name: "Tiny - English", - description: "Fastest, English-only. Optimized for speed on English audio.", - intelligence: 1, - speed: 3, - size: "44 MB", - inputType: ["audio"], - outputType: ["text"], - languageSupport: "english-only", - huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-tiny.en-q8_0.bin", - }, - "QuantizedBase": { - name: "Base", - description: "Good balance of speed and accuracy for multilingual use.", - intelligence: 2, - speed: 2, - size: "82 MB", - inputType: ["audio"], - outputType: ["text"], - languageSupport: "multilingual", - huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-base-q8_0.bin", - }, - "QuantizedBaseEn": { - name: "Base - English", - description: "Balanced speed and accuracy, optimized for English audio.", - intelligence: 2, - speed: 2, - size: "82 MB", - inputType: ["audio"], - outputType: ["text"], - languageSupport: "english-only", - huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-base.en-q8_0.bin", - }, - "QuantizedSmall": { - name: "Small", - description: "Higher accuracy, moderate speed for multilingual transcription.", - intelligence: 2, - speed: 2, - size: "264 MB", - inputType: ["audio"], - outputType: ["text"], - languageSupport: "multilingual", - huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-small-q8_0.bin", - }, - "QuantizedSmallEn": { - name: "Small - English", - description: "Higher accuracy, moderate speed, optimized for English audio.", - intelligence: 3, - speed: 2, - size: "264 MB", - inputType: ["audio"], - outputType: ["text"], - languageSupport: "english-only", - huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-small.en-q8_0.bin", - }, - "QuantizedLargeTurbo": { - name: "Large", - description: "Highest accuracy, resource intensive. Only for Mac Pro M4 and above.", - intelligence: 3, - speed: 1, - size: "874 MB", - inputType: ["audio"], - outputType: ["text"], - languageSupport: "multilingual", - huggingface: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-large-v3-turbo-q8_0.bin", - }, -}; - -interface STTViewProps extends SharedSTTProps { - isWerModalOpen: boolean; - setIsWerModalOpen: (open: boolean) => void; -} - -const aiConfigSchema = z.object({ - redemptionTimeMs: z.number().int().min(300).max(1200), -}); -type AIConfigValues = z.infer; - -export function STTView({ - selectedSTTModel, - setSelectedSTTModel, - sttModels, - setSttModels, - downloadingModels, - handleModelDownload, - handleShowFileLocation, - isWerModalOpen, - setIsWerModalOpen, -}: STTViewProps) { - const queryClient = useQueryClient(); - - // Add drag and drop state - // const [isDragOver, setIsDragOver] = useState(false); - // const [isUploading, setIsUploading] = useState(false); - - const currentSTTModel = useQuery({ - queryKey: ["current-stt-model"], - queryFn: () => localSttCommands.getCurrentModel(), - }); - - useEffect(() => { - if (currentSTTModel.data) { - setSelectedSTTModel(currentSTTModel.data); - } - }, [currentSTTModel.data, setSelectedSTTModel]); - - // call backend for the download status of the STT models and sets it - const sttModelDownloadStatus = useQuery({ - queryKey: ["stt-model-download-status"], - queryFn: async () => { - const statusChecks = await Promise.all([ - localSttCommands.isModelDownloaded("QuantizedTiny"), - localSttCommands.isModelDownloaded("QuantizedTinyEn"), - localSttCommands.isModelDownloaded("QuantizedBase"), - localSttCommands.isModelDownloaded("QuantizedBaseEn"), - localSttCommands.isModelDownloaded("QuantizedSmall"), - localSttCommands.isModelDownloaded("QuantizedSmallEn"), - localSttCommands.isModelDownloaded("QuantizedLargeTurbo"), - ]); - return { - "QuantizedTiny": statusChecks[0], - "QuantizedTinyEn": statusChecks[1], - "QuantizedBase": statusChecks[2], - "QuantizedBaseEn": statusChecks[3], - "QuantizedSmall": statusChecks[4], - "QuantizedSmallEn": statusChecks[5], - "QuantizedLargeTurbo": statusChecks[6], - } as Record; - }, - refetchInterval: 3000, - }); - - useEffect(() => { - if (sttModelDownloadStatus.data) { - setSttModels(prev => - prev.map(model => ({ - ...model, - downloaded: sttModelDownloadStatus.data[model.key] || false, - })) - ); - } - }, [sttModelDownloadStatus.data, setSttModels]); - - const defaultModelKeys = ["QuantizedTiny", "QuantizedSmall", "QuantizedLargeTurbo"]; - const otherModelKeys = ["QuantizedTinyEn", "QuantizedBase", "QuantizedBaseEn", "QuantizedSmallEn"]; - - const modelsToShow = sttModels.filter(model => { - if (defaultModelKeys.includes(model.key)) { - return true; - } - - if (otherModelKeys.includes(model.key) && model.downloaded) { - return true; - } - - return false; - }); - - const config = useQuery({ - queryKey: ["config", "ai"], - queryFn: async () => { - const result = await dbCommands.getConfig(); - return result; - }, - }); - - const aiConfigForm = useForm({ - resolver: zodResolver(aiConfigSchema), - defaultValues: { - redemptionTimeMs: 500, - }, - }); - - useEffect(() => { - if (config.data) { - aiConfigForm.reset({ - redemptionTimeMs: config.data.ai.redemption_time_ms ?? 500, - }); - } - }, [config.data, aiConfigForm]); - - const aiConfigMutation = useMutation({ - mutationFn: async (values: AIConfigValues) => { - if (!config.data) { - return; - } - - await dbCommands.setConfig({ - ...config.data, - ai: { - ...config.data.ai, - redemption_time_ms: values.redemptionTimeMs ?? 500, - }, - }); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["config", "ai"] }); - }, - onError: console.error, - }); - - /* - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOver(true); - }, []); - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOver(false); - }, []); - - const handleFileDrop = useCallback(async (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOver(false); - - const file = e.dataTransfer.files[0]; - if (!file) { - return; - } - - // Validate file extension - const fileName = file.name.toLowerCase(); - if (!fileName.endsWith(".bin") && !fileName.endsWith(".ggml")) { - await message( - "Please drop a valid STT model file (Comming Soon)", - { title: "Invalid File Type", kind: "error" }, - ); - return; - } - - setIsUploading(true); - try { - // Get the STT models directory - const modelsDir = await localSttCommands.modelsDir(); - const targetPath = `${modelsDir}/${file.name}`; - - // Read the file content as array buffer - const fileContent = await file.arrayBuffer(); - const uint8Array = new Uint8Array(fileContent); - - // Write the file to the models directory - await writeFile(targetPath, uint8Array); - - await message(`Model file "${file.name}" copied successfully!`, { - title: "File Copied", - kind: "info", - }); - - // This invalidation will trigger the automatic refresh - queryClient.invalidateQueries({ queryKey: ["stt-model-download-status"] }); - } catch (error) { - console.error("Error copying model file:", error); - await message(`Failed to copy model file: ${error instanceof Error ? error.message : String(error)}`, { - title: "Copy Failed", - kind: "error", - }); - } finally { - setIsUploading(false); - } - }, [queryClient]); - */ - - return ( -
-
-

- Transcribing -

- - - - - - Performance difference between languages - - -
- -
-

- Default -

-
- {modelsToShow.map((model) => ( -
{ - if (model.downloaded) { - setSelectedSTTModel(model.key); - localSttCommands.setCurrentModel(model.key as WhisperModel); - localSttCommands.stopServer(null); - localSttCommands.startServer(null); - } - }} - > -
-
-

- {model.name} -

-
- -
-
- - Accuracy - -
- {[1, 2, 3].map((step) => ( -
= step - ? "bg-green-500" - : "bg-gray-200", - )} - /> - ))} -
-
- -
- - Speed - -
- {[1, 2, 3].map((step) => ( -
= step - ? "bg-blue-500" - : "bg-gray-200", - )} - /> - ))} -
-
-
-
- -
- {model.downloaded - ? ( - - ) - : downloadingModels.has(model.key) - ? ( - - ) - : ( - - )} -
-
- ))} -
-
- - { - /* -
-

- Custom -

-
- {isUploading - ? ( -
-
-

- Copying model file... -

-
- ) - : ( -

- Drag and drop your own STT mode file (.ggml or .bin format) -

- )} -
-
- */ - } -
-
-

- Configuration -

-
- ( - - - Redemption Time ({field.value ?? 500}ms) - - - Lower value will cause model to output text more often, but may cause performance issues. - - -
- { - const newValue = value[0]; - field.onChange(newValue); - aiConfigMutation.mutate({ redemptionTimeMs: newValue }); - }} - className="w-[100%] [&>.relative>.absolute]:bg-gray-400 [&>.relative>span[data-state='active']]:bg-gray-300 [&>.relative>span]:border-gray-200" - /> -
-
- -
- )} - /> - -
-
- - setIsWerModalOpen(false)} - /> -
- ); -} diff --git a/apps/desktop/src/components/settings/components/index.ts b/apps/desktop/src/components/settings/components/index.ts index d3a04738cb..c36203e24b 100644 --- a/apps/desktop/src/components/settings/components/index.ts +++ b/apps/desktop/src/components/settings/components/index.ts @@ -1,4 +1,3 @@ export * from "./tab-icon"; export * from "./templates-sidebar"; export * from "./types"; -export * from "./wer-modal"; diff --git a/apps/desktop/src/components/settings/components/tab-icon.tsx b/apps/desktop/src/components/settings/components/tab-icon.tsx index 285b2ef41c..d2cd8a8e87 100644 --- a/apps/desktop/src/components/settings/components/tab-icon.tsx +++ b/apps/desktop/src/components/settings/components/tab-icon.tsx @@ -1,6 +1,7 @@ import { AudioLinesIcon, BellIcon, + BirdIcon, BlocksIcon, CalendarIcon, CreditCardIcon, @@ -22,8 +23,10 @@ export function TabIcon({ tab }: { tab: Tab }) { return ; case "feedback": return ; - case "ai": + case "ai-llm": return ; + case "ai-stt": + return ; case "calendar": return ; case "templates": diff --git a/apps/desktop/src/components/settings/components/types.ts b/apps/desktop/src/components/settings/components/types.ts index c9349191f6..175fd6d95b 100644 --- a/apps/desktop/src/components/settings/components/types.ts +++ b/apps/desktop/src/components/settings/components/types.ts @@ -14,7 +14,8 @@ import { export type Tab = | "general" | "calendar" - | "ai" + | "ai-llm" + | "ai-stt" | "notifications" | "sound" | "templates" @@ -25,7 +26,8 @@ export type Tab = export const TABS: { name: Tab; icon: LucideIcon }[] = [ { name: "general", icon: Settings }, { name: "calendar", icon: Calendar }, - { name: "ai", icon: Sparkles }, + { name: "ai-llm", icon: Sparkles }, + { name: "ai-stt", icon: Sparkles }, { name: "notifications", icon: Bell }, { name: "sound", icon: Volume2 }, { name: "templates", icon: LayoutTemplate }, diff --git a/apps/desktop/src/components/settings/components/wer-modal.tsx b/apps/desktop/src/components/settings/components/wer-modal.tsx deleted file mode 100644 index 65c349d452..0000000000 --- a/apps/desktop/src/components/settings/components/wer-modal.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Button } from "@hypr/ui/components/ui/button"; -import { Modal, ModalBody, ModalDescription, ModalFooter, ModalHeader, ModalTitle } from "@hypr/ui/components/ui/modal"; -import { cn } from "@hypr/ui/lib/utils"; -import { Trans } from "@lingui/react/macro"; -import { useState } from "react"; - -export const werPerformanceData = { - "Excellent": [ - { "language": "Spanish", "WER": 2.8 }, - { "language": "Italian", "WER": 3.0 }, - { "language": "Korean", "WER": 3.1 }, - { "language": "Portuguese", "WER": 4.0 }, - { "language": "English", "WER": 4.1 }, - { "language": "Polish", "WER": 4.6 }, - { "language": "Catalan", "WER": 4.8 }, - { "language": "Japanese", "WER": 4.8 }, - { "language": "German", "WER": 4.9 }, - { "language": "Russian", "WER": 5.0 }, - ], - "Good": [ - { "language": "Dutch", "WER": 5.2 }, - { "language": "French", "WER": 5.3 }, - { "language": "Indonesian", "WER": 6.0 }, - { "language": "Ukrainian", "WER": 6.4 }, - { "language": "Turkish", "WER": 6.7 }, - { "language": "Malay", "WER": 7.3 }, - { "language": "Swedish", "WER": 7.6 }, - { "language": "Mandarin", "WER": 7.7 }, - { "language": "Finnish", "WER": 7.7 }, - { "language": "Norwegian", "WER": 7.8 }, - ], - "Moderate": [ - { "language": "Romanian", "WER": 8.2 }, - { "language": "Thai", "WER": 8.4 }, - { "language": "Vietnamese", "WER": 8.7 }, - { "language": "Slovak", "WER": 9.2 }, - { "language": "Arabic", "WER": 9.6 }, - { "language": "Czech", "WER": 10.1 }, - { "language": "Croatian", "WER": 10.8 }, - { "language": "Greek", "WER": 10.9 }, - ], - "Weak": [ - { "language": "Serbian", "WER": 11.6 }, - { "language": "Danish", "WER": 12.0 }, - { "language": "Bulgarian", "WER": 12.5 }, - { "language": "Hungarian", "WER": 12.9 }, - { "language": "Filipino", "WER": 13.0 }, - { "language": "Bosnian", "WER": 13.0 }, - { "language": "Galician", "WER": 13.0 }, - { "language": "Macedonian", "WER": 14.8 }, - ], - "Poor": [ - { "language": "Hindi", "WER": 17.0 }, - { "language": "Estonian", "WER": 18.1 }, - { "language": "Slovenian", "WER": 18.4 }, - { "language": "Tamil", "WER": 18.2 }, - { "language": "Latvian", "WER": 19.4 }, - { "language": "Azerbaijani", "WER": 19.7 }, - ], -}; - -export function WERPerformanceModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { - const [selectedCategory, setSelectedCategory] = useState("Excellent"); - - return ( - - - - Whisper Model Language Performance (WER) - - - - Word Error Rate (WER) indicates transcription accuracy (lower is better). Data based on the FLEURS dataset, - measured with OpenAI's Whisper{" "} - large-v3-turbo model.{" "} - - More info - - - - - - -
-
- {(Object.keys(werPerformanceData) as Array).map((category) => ( - - ))} -
-
- -
- {werPerformanceData[selectedCategory].map((lang) => ( -
- - {lang.language} - - {lang.WER.toFixed(1)}% -
- ))} -
-
- - - - -
- ); -} diff --git a/apps/desktop/src/components/settings/views/ai.tsx b/apps/desktop/src/components/settings/views/ai-llm.tsx similarity index 78% rename from apps/desktop/src/components/settings/views/ai.tsx rename to apps/desktop/src/components/settings/views/ai-llm.tsx index 4ebf11ecd8..778904c8f8 100644 --- a/apps/desktop/src/components/settings/views/ai.tsx +++ b/apps/desktop/src/components/settings/views/ai-llm.tsx @@ -1,7 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Trans } from "@lingui/react/macro"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { openPath } from "@tauri-apps/plugin-opener"; import { open } from "@tauri-apps/plugin-shell"; import { InfoIcon } from "lucide-react"; import { useEffect, useState } from "react"; @@ -14,7 +13,6 @@ import { commands as connectorCommands, type Connection } from "@hypr/plugin-con import { commands as dbCommands } from "@hypr/plugin-db"; import { commands as localLlmCommands, SupportedModel } from "@hypr/plugin-local-llm"; -import { commands as localSttCommands } from "@hypr/plugin-local-stt"; import { Button } from "@hypr/ui/components/ui/button"; import { Form, @@ -28,9 +26,8 @@ import { import { Tabs, TabsList, TabsTrigger } from "@hypr/ui/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@hypr/ui/components/ui/tooltip"; import { cn } from "@hypr/ui/lib/utils"; -import { showLlmModelDownloadToast, showSttModelDownloadToast } from "../../toast/shared"; +import { showLlmModelDownloadToast } from "../../toast/shared"; -// Import the new components import { LLMCustomView } from "../components/ai/llm-custom-view"; import { LLMLocalView } from "../components/ai/llm-local-view"; import { @@ -42,12 +39,8 @@ import { OpenRouterFormValues, SharedCustomEndpointProps, SharedLLMProps, - SharedSTTProps, - STTModel, } from "../components/ai/shared"; -import { STTView } from "../components/ai/stt-view"; -// Schema for OpenAI form const openaiSchema = z.object({ api_key: z.string().min(1, { message: "API key is required" }).refine( (value) => value.startsWith("sk-"), @@ -56,7 +49,6 @@ const openaiSchema = z.object({ model: z.string().min(1, { message: "Model is required" }), }); -// Schema for Gemini form const geminiSchema = z.object({ api_key: z.string().min(1, { message: "API key is required" }).refine( (value) => value.startsWith("AIza"), @@ -65,7 +57,6 @@ const geminiSchema = z.object({ model: z.string().min(1, { message: "Model is required" }), }); -// Schema for OpenRouter form const openrouterSchema = z.object({ api_key: z.string().min(1, { message: "API key is required" }).refine( (value) => value.startsWith("sk-"), @@ -74,7 +65,6 @@ const openrouterSchema = z.object({ model: z.string().min(1, { message: "Model is required" }), }); -// Schema for Custom endpoint form const customSchema = z.object({ model: z.string().min(1, { message: "Model is required" }), api_base: z.string().url({ message: "Please enter a valid URL" }).min(1, { message: "URL is required" }).refine( @@ -93,115 +83,6 @@ const customSchema = z.object({ api_key: z.string().optional(), }); -const initialSttModels: STTModel[] = [ - { - key: "QuantizedTiny", - name: "Tiny", - accuracy: 1, - speed: 3, - size: "44 MB", - downloaded: true, - fileName: "ggml-tiny-q8_0.bin", - }, - { - key: "QuantizedTinyEn", - name: "Tiny - English", - accuracy: 1, - speed: 3, - size: "44 MB", - downloaded: false, - fileName: "ggml-tiny.en-q8_0.bin", - }, - { - key: "QuantizedBase", - name: "Base", - accuracy: 2, - speed: 2, - size: "82 MB", - downloaded: false, - fileName: "ggml-base-q8_0.bin", - }, - { - key: "QuantizedBaseEn", - name: "Base - English", - accuracy: 2, - speed: 2, - size: "82 MB", - downloaded: false, - fileName: "ggml-base.en-q8_0.bin", - }, - { - key: "QuantizedSmall", - name: "Small", - accuracy: 2, - speed: 2, - size: "264 MB", - downloaded: false, - fileName: "ggml-small-q8_0.bin", - }, - { - key: "QuantizedSmallEn", - name: "Small - English", - accuracy: 2, - speed: 2, - size: "264 MB", - downloaded: false, - fileName: "ggml-small.en-q8_0.bin", - }, - { - key: "QuantizedLargeTurbo", - name: "Large", - accuracy: 3, - speed: 1, - size: "874 MB", - downloaded: false, - fileName: "ggml-large-v3-turbo-q8_0.bin", - }, -]; - -const initialLlmModels: LLMModel[] = [ - { - key: "Llama3p2_3bQ4", - name: "Llama 3 (3B, Q4)", - description: "Basic", - available: true, - downloaded: false, - size: "2.0 GB", - }, - { - key: "HyprLLM", - name: "HyprLLM v1", - description: "English only", - available: true, - downloaded: false, - size: "1.1 GB", - }, - { - key: "HyprLLMv2", - name: "HyprLLM v2", - description: "Multilingual support", - available: false, - downloaded: false, - size: "1.1 GB", - }, - { - key: "HyprLLMv3", - name: "HyprLLM v3", - description: "Cross-language support", - available: false, - downloaded: false, - size: "1.1 GB", - }, - { - key: "HyprLLMv4", - name: "HyprLLM v4", - description: "Professional domains", - available: false, - downloaded: false, - size: "1.1 GB", - }, -]; - const aiConfigSchema = z.object({ aiSpecificity: z.number().int().min(1).max(4), }); @@ -230,52 +111,33 @@ const specificityLevels = { }, } as const; -export default function LocalAI() { +export default function LlmAI() { const queryClient = useQueryClient(); - const [activeTab, setActiveTab] = useState<"transcription" | "local" | "custom">("transcription"); - - // STT State - const [isWerModalOpen, setIsWerModalOpen] = useState(false); - const [selectedSTTModel, setSelectedSTTModel] = useState("QuantizedTiny"); - const [sttModels, setSttModels] = useState(initialSttModels); + const [activeTab, setActiveTab] = useState<"local" | "custom">("local"); - // LLM State const [selectedLLMModel, setSelectedLLMModel] = useState("HyprLLM"); const [downloadingModels, setDownloadingModels] = useState>(new Set()); - const [llmModelsState, setLlmModels] = useState(initialLlmModels); + const [llmModelsState, setLlmModels] = useState([]); + + useEffect(() => { + localLlmCommands.listSupportedModel().then((ms) => { + const models: LLMModel[] = ms.map((model) => ({ + key: model.key as SupportedModel, + name: model.name, + description: model.description, + available: true, + downloaded: false, + size: `${(model.size_bytes / 1024 / 1024 / 1024).toFixed(2)} GB`, + })); + + setLlmModels(models); + }); + }, []); - // Custom Endpoint State const [openAccordion, setOpenAccordion] = useState<"others" | "openai" | "gemini" | "openrouter" | null>(null); const { userId } = useHypr(); - // Shared Model Download Function - const handleModelDownload = async (modelKey: string) => { - if (!modelKey.startsWith("Quantized")) { - await handleLlmModelDownload(modelKey); - return; - } - setDownloadingModels(prev => new Set([...prev, modelKey])); - - showSttModelDownloadToast(modelKey as any, () => { - setSttModels(prev => - prev.map(model => - model.key === modelKey - ? { ...model, downloaded: true } - : model - ) - ); - setDownloadingModels(prev => { - const newSet = new Set(prev); - newSet.delete(modelKey); - return newSet; - }); - - setSelectedSTTModel(modelKey); - localSttCommands.setCurrentModel(modelKey as any); - }, queryClient); - }; - const handleLlmModelDownload = async (modelKey: string) => { setDownloadingModels((prev) => new Set([...prev, modelKey])); @@ -294,12 +156,10 @@ export default function LocalAI() { }, queryClient); }; - const handleShowFileLocation = async (modelType: "stt" | "llm") => { - const path = await (modelType === "stt" ? localSttCommands.modelsDir() : localLlmCommands.modelsDir()); - await openPath(path); + const handleModelDownload = async (modelKey: string) => { + await handleLlmModelDownload(modelKey); }; - // Queries and Mutations const customLLMEnabled = useQuery({ queryKey: ["custom-llm-enabled"], queryFn: () => connectorCommands.getCustomLlmEnabled(), @@ -322,27 +182,20 @@ export default function LocalAI() { queryFn: () => connectorCommands.getCustomLlmModel(), }); - /* - const availableLLMModels = useQuery({ - queryKey: ["available-llm-models"], - queryFn: async () => { - console.log("available models being loaded"); - return await connectorCommands.listCustomLlmModels(); - }, - }); - */ - const modelDownloadStatus = useQuery({ queryKey: ["llm-model-download-status"], queryFn: async () => { const statusChecks = await Promise.all([ - localLlmCommands.isModelDownloaded("Llama3p2_3bQ4" as SupportedModel), - localLlmCommands.isModelDownloaded("HyprLLM" as SupportedModel), + localLlmCommands.isModelDownloaded("Llama3p2_3bQ4" satisfies SupportedModel), + localLlmCommands.isModelDownloaded("HyprLLM" satisfies SupportedModel), + localLlmCommands.isModelDownloaded("Gemma3_4bQ4" satisfies SupportedModel), ]); + return { "Llama3p2_3bQ4": statusChecks[0], "HyprLLM": statusChecks[1], - } as Record; + "Gemma3_4bQ4": statusChecks[2], + } satisfies Record; }, refetchInterval: 3000, }); @@ -370,7 +223,6 @@ export default function LocalAI() { }, }); - // OpenAI and Gemini API key queries/mutations const openaiApiKeyQuery = useQuery({ queryKey: ["openai-api-key"], queryFn: () => connectorCommands.getOpenaiApiKey(), @@ -395,7 +247,6 @@ export default function LocalAI() { }, }); - // NEW: Others provider queries/mutations const othersApiBaseQuery = useQuery({ queryKey: ["others-api-base"], queryFn: () => connectorCommands.getOthersApiBase(), @@ -444,7 +295,6 @@ export default function LocalAI() { }, }); - // NEW: OpenAI and Gemini model queries/mutations const openaiModelQuery = useQuery({ queryKey: ["openai-model"], queryFn: () => connectorCommands.getOpenaiModel(), @@ -469,7 +319,6 @@ export default function LocalAI() { }, }); - // OpenRouter queries/mutations const openrouterApiKeyQuery = useQuery({ queryKey: ["openrouter-api-key"], queryFn: () => connectorCommands.getOpenrouterApiKey(), @@ -494,20 +343,16 @@ export default function LocalAI() { }, }); - // MIGRATION LOGIC - Run once on component mount useEffect(() => { const handleMigration = async () => { - // Skip if no store exists at all if (!customLLMConnection.data && !customLLMEnabled.data) { return; } - // Check if migration needed (no providerSource exists) if (!providerSourceQuery.data && customLLMConnection.data) { console.log("Migrating existing user to new provider system..."); try { - // Copy existing custom* fields to others* fields if (customLLMConnection.data.api_base) { await setOthersApiBaseMutation.mutateAsync(customLLMConnection.data.api_base); } @@ -518,7 +363,6 @@ export default function LocalAI() { await setOthersModelMutation.mutateAsync(getCustomLLMModel.data); } - // Set provider source to 'others' await setProviderSourceMutation.mutateAsync("others"); console.log("Migration completed successfully"); @@ -528,7 +372,6 @@ export default function LocalAI() { } }; - // Run migration when all queries have loaded if ( providerSourceQuery.data !== undefined && customLLMConnection.data !== undefined && getCustomLLMModel.data !== undefined @@ -537,18 +380,16 @@ export default function LocalAI() { } }, [providerSourceQuery.data, customLLMConnection.data, getCustomLLMModel.data]); - // ACCORDION DISPLAY - Based on providerSource, not URL useEffect(() => { if (providerSourceQuery.data) { setOpenAccordion(providerSourceQuery.data as "openai" | "gemini" | "openrouter" | "others"); } else if (customLLMEnabled.data) { - setOpenAccordion("others"); // Fallback during migration + setOpenAccordion("others"); } else { setOpenAccordion(null); } }, [providerSourceQuery.data, customLLMEnabled.data, setOpenAccordion]); - // CRITICAL: Centralized function to configure custom endpoint const configureCustomEndpoint = (config: ConfigureEndpointConfig) => { const finalApiBase = config.provider === "openai" ? "https://api.openai.com/v1" @@ -558,10 +399,8 @@ export default function LocalAI() { ? "https://openrouter.ai/api/v1" : config.api_base; - // Enable custom LLM setCustomLLMEnabledMutation.mutate(true); - // Store in provider-specific storage if (config.provider === "openai" && config.api_key) { setOpenaiApiKeyMutation.mutate(config.api_key); setOpenaiModelMutation.mutate(config.model); @@ -577,10 +416,8 @@ export default function LocalAI() { setOthersModelMutation.mutate(config.model); } - // Set provider source setProviderSourceMutation.mutate(config.provider); - // Set as currently active (custom* fields) setCustomLLMModel.mutate(config.model); setCustomLLMConnection.mutate({ api_base: finalApiBase, @@ -588,7 +425,6 @@ export default function LocalAI() { }); }; - // Create form instances for each provider const openaiForm = useForm({ resolver: zodResolver(openaiSchema), mode: "onChange", @@ -626,7 +462,6 @@ export default function LocalAI() { }, }); - // Set form values from stored data useEffect(() => { if (openaiApiKeyQuery.data) { openaiForm.setValue("api_key", openaiApiKeyQuery.data); @@ -655,7 +490,6 @@ export default function LocalAI() { }, [openrouterApiKeyQuery.data, openrouterModelQuery.data, openrouterForm]); useEffect(() => { - // Others form gets populated from Others-specific storage using setValue to trigger watch if (othersApiBaseQuery.data) { customForm.setValue("api_base", othersApiBaseQuery.data); } @@ -667,7 +501,6 @@ export default function LocalAI() { } }, [othersApiBaseQuery.data, othersApiKeyQuery.data, othersModelQuery.data, customForm]); - // Set selected models from stored model for OpenAI and Gemini useEffect(() => { if (openaiModelQuery.data && openAccordion === "openai") { openaiForm.setValue("model", openaiModelQuery.data); @@ -686,7 +519,6 @@ export default function LocalAI() { } }, [openrouterModelQuery.data, openAccordion, openrouterForm]); - // ADD THIS: Set stored values for Others when accordion opens useEffect(() => { if (openAccordion === "others") { if (othersApiBaseQuery.data) { @@ -701,7 +533,6 @@ export default function LocalAI() { } }, [openAccordion, othersApiBaseQuery.data, othersApiKeyQuery.data, othersModelQuery.data, customForm]); - // AI Configuration const config = useQuery({ queryKey: ["config", "ai"], queryFn: async () => { @@ -750,19 +581,6 @@ export default function LocalAI() { return Boolean(apiBase && (apiBase.includes("localhost") || apiBase.includes("127.0.0.1"))); }; - // Prepare props for child components - const sttProps: SharedSTTProps & { isWerModalOpen: boolean; setIsWerModalOpen: (open: boolean) => void } = { - selectedSTTModel, - setSelectedSTTModel, - sttModels, - setSttModels, - downloadingModels, - handleModelDownload, - handleShowFileLocation, - isWerModalOpen, - setIsWerModalOpen, - }; - const localLlmProps: SharedLLMProps = { customLLMEnabled, selectedLLMModel, @@ -771,7 +589,6 @@ export default function LocalAI() { downloadingModels, llmModelsState, handleModelDownload, - handleShowFileLocation, }; const customEndpointProps: SharedCustomEndpointProps = { @@ -781,7 +598,6 @@ export default function LocalAI() { setOpenAccordion, customLLMConnection, getCustomLLMModel, - // availableLLMModels, openaiForm, geminiForm, openrouterForm, @@ -791,33 +607,26 @@ export default function LocalAI() { return (
- {/* Tab Navigation */} setActiveTab(value as "transcription" | "local" | "custom")} + onValueChange={(value) => setActiveTab(value as "local" | "custom")} className="w-full" > - - - Transcription - + - LLM - Local + Local - LLM - Custom + Custom - {/* Tab Content */} - {activeTab === "transcription" && } {activeTab === "local" && } {activeTab === "custom" && (
- {/* AI Configuration - only show in custom tab */} {customLLMEnabled.data && (
diff --git a/apps/desktop/src/components/settings/views/ai-stt.tsx b/apps/desktop/src/components/settings/views/ai-stt.tsx new file mode 100644 index 0000000000..16a01d7848 --- /dev/null +++ b/apps/desktop/src/components/settings/views/ai-stt.tsx @@ -0,0 +1,130 @@ +import { Trans } from "@lingui/react/macro"; +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; + +import { commands as localSttCommands } from "@hypr/plugin-local-stt"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@hypr/ui/components/ui/tabs"; +import { showSttModelDownloadToast } from "../../toast/shared"; +import { SharedSTTProps, STTModel } from "../components/ai/shared"; +import { STTViewLocal } from "../components/ai/stt-view-local"; +import { STTViewRemote } from "../components/ai/stt-view-remote"; + +export default function SttAI() { + const queryClient = useQueryClient(); + const [activeTab, setActiveTab] = useState<"local" | "custom">("local"); + + const [isWerModalOpen, setIsWerModalOpen] = useState(false); + const [selectedSTTModel, setSelectedSTTModel] = useState("QuantizedTiny"); + const [sttModels, setSttModels] = useState(initialSttModels); + const [downloadingModels, setDownloadingModels] = useState>(new Set()); + + const handleModelDownload = async (modelKey: string) => { + setDownloadingModels(prev => new Set([...prev, modelKey])); + + showSttModelDownloadToast(modelKey as any, () => { + setSttModels(prev => + prev.map(model => + model.key === modelKey + ? { ...model, downloaded: true } + : model + ) + ); + setDownloadingModels(prev => { + const newSet = new Set(prev); + newSet.delete(modelKey); + return newSet; + }); + + setSelectedSTTModel(modelKey); + localSttCommands.setCurrentModel(modelKey as any); + }, queryClient); + }; + + const sttProps: SharedSTTProps & { isWerModalOpen: boolean; setIsWerModalOpen: (open: boolean) => void } = { + selectedSTTModel, + setSelectedSTTModel, + sttModels, + setSttModels, + downloadingModels, + handleModelDownload, + isWerModalOpen, + setIsWerModalOpen, + }; + + return ( +
+ setActiveTab(value as "local" | "custom")} + className="w-full" + > + + + Local + + + Custom + + + + + + + + + +
+ ); +} + +const initialSttModels: STTModel[] = [ + { + key: "QuantizedTiny", + name: "Whisper Tiny (Multilingual)", + size: "44 MB", + downloaded: true, + fileName: "ggml-tiny-q8_0.bin", + }, + { + key: "QuantizedTinyEn", + name: "WhisperTiny (English)", + size: "44 MB", + downloaded: false, + fileName: "ggml-tiny.en-q8_0.bin", + }, + { + key: "QuantizedBase", + name: "Whisper Base (Multilingual)", + size: "82 MB", + downloaded: false, + fileName: "ggml-base-q8_0.bin", + }, + { + key: "QuantizedBaseEn", + name: "WhisperBase (English)", + size: "82 MB", + downloaded: false, + fileName: "ggml-base.en-q8_0.bin", + }, + { + key: "QuantizedSmall", + name: "Whisper Small (Multilingual)", + size: "264 MB", + downloaded: false, + fileName: "ggml-small-q8_0.bin", + }, + { + key: "QuantizedSmallEn", + name: "WhisperSmall (English)", + size: "264 MB", + downloaded: false, + fileName: "ggml-small.en-q8_0.bin", + }, + { + key: "QuantizedLargeTurbo", + name: "WhisperLarge (Multilingual)", + size: "874 MB", + downloaded: false, + fileName: "ggml-large-v3-turbo-q8_0.bin", + }, +]; diff --git a/apps/desktop/src/components/settings/views/index.ts b/apps/desktop/src/components/settings/views/index.ts index f356cc3b68..5915287c0c 100644 --- a/apps/desktop/src/components/settings/views/index.ts +++ b/apps/desktop/src/components/settings/views/index.ts @@ -1,4 +1,5 @@ -export { default as LocalAI } from "./ai"; +export { default as AILLM } from "./ai-llm"; +export { default as AISTT } from "./ai-stt"; export { default as Billing } from "./billing"; export { default as Calendar } from "./calendar"; export { default as General } from "./general"; diff --git a/apps/desktop/src/components/toast/model-download.tsx b/apps/desktop/src/components/toast/model-download.tsx index 198fb6d840..c947199f15 100644 --- a/apps/desktop/src/components/toast/model-download.tsx +++ b/apps/desktop/src/components/toast/model-download.tsx @@ -83,6 +83,7 @@ export default function ModelDownloadNotification() { const results = await Promise.all([ localLlmCommands.isModelDownloaded("Llama3p2_3bQ4"), localLlmCommands.isModelDownloaded("HyprLLM"), + localLlmCommands.isModelDownloaded("Gemma3_4bQ4"), ]); return results.some(Boolean); diff --git a/apps/desktop/src/components/toast/model-select.tsx b/apps/desktop/src/components/toast/model-select.tsx index daee646ba1..e8f8339a31 100644 --- a/apps/desktop/src/components/toast/model-select.tsx +++ b/apps/desktop/src/components/toast/model-select.tsx @@ -14,7 +14,7 @@ export async function showModelSelectToast(language: string) { } const handleClick = () => { - const url = { to: "/app/settings", search: { tab: "ai" } } as const satisfies LinkProps; + const url = { to: "/app/settings", search: { tab: "ai-stt" } } as const satisfies LinkProps; windowsCommands.windowShow({ type: "settings" }).then(() => { setTimeout(() => { diff --git a/apps/desktop/src/components/welcome-modal/download-progress-view.tsx b/apps/desktop/src/components/welcome-modal/download-progress-view.tsx index c84ae8f25f..a8047037e9 100644 --- a/apps/desktop/src/components/welcome-modal/download-progress-view.tsx +++ b/apps/desktop/src/components/welcome-modal/download-progress-view.tsx @@ -8,7 +8,7 @@ import { commands as localSttCommands, type WhisperModel } from "@hypr/plugin-lo import { Progress } from "@hypr/ui/components/ui/progress"; import PushableButton from "@hypr/ui/components/ui/pushable-button"; import { cn } from "@hypr/ui/lib/utils"; -import { sttModelMetadata } from "../settings/components/ai/stt-view"; +import { sttModelMetadata } from "../settings/components/ai/stt-view-local"; interface ModelDownloadProgress { channel: Channel; diff --git a/apps/desktop/src/components/welcome-modal/model-selection-view.tsx b/apps/desktop/src/components/welcome-modal/model-selection-view.tsx index b8d01ce6ae..365e5ed083 100644 --- a/apps/desktop/src/components/welcome-modal/model-selection-view.tsx +++ b/apps/desktop/src/components/welcome-modal/model-selection-view.tsx @@ -9,7 +9,7 @@ import { type WhisperModel } from "@hypr/plugin-local-stt"; import { commands as localSttCommands } from "@hypr/plugin-local-stt"; import PushableButton from "@hypr/ui/components/ui/pushable-button"; import { cn } from "@hypr/ui/lib/utils"; -import { sttModelMetadata } from "../settings/components/ai/stt-view"; +import { sttModelMetadata } from "../settings/components/ai/stt-view-local"; interface ModelInfo { model: string; diff --git a/apps/desktop/src/hooks/use-license.ts b/apps/desktop/src/hooks/use-license.ts index 38cd7cefd5..d5360de488 100644 --- a/apps/desktop/src/hooks/use-license.ts +++ b/apps/desktop/src/hooks/use-license.ts @@ -1,9 +1,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useCallback } from "react"; import * as keygen from "tauri-plugin-keygen-api"; const LICENSE_QUERY_KEY = ["license"] as const; +const LICENSE_TTL_SECONDS = 60 * 60 * 24 * 7; +const REFRESH_THRESHOLD_DAYS = 3; -// https://github.com/bagindo/tauri-plugin-keygen export function useLicense() { const queryClient = useQueryClient(); @@ -16,8 +18,7 @@ export function useLicense() { } return null; }, - refetchInterval: 1000 * 60 * 1, - // This is important for immediate refresh + refetchInterval: 1000 * 60 * 5, refetchIntervalInBackground: true, }); @@ -31,7 +32,7 @@ export function useLicense() { const license = await keygen.validateCheckoutKey({ key: cachedKey, entitlements: [], - ttlSeconds: 60 * 60 * 24 * 7, // 7 days + ttlSeconds: LICENSE_TTL_SECONDS, ttlForever: false, }); @@ -46,29 +47,29 @@ export function useLicense() { }, }); - const shouldRefresh = () => { + const getLicenseStatus = useCallback(() => { const license = getLicense.data; - if (!license || !license.valid) { - return false; + if (!license?.valid || !license.expiry) { + return { needsRefresh: false, isValid: false }; } - if (!license.expiry) { - throw new Error("license.expiry is null"); - } - - const daysUntilExpiry = Math.floor( - (new Date(license.expiry).getTime() - Date.now()) / (1000 * 60 * 60 * 24), - ); + const now = Date.now(); + const expiryTime = new Date(license.expiry).getTime(); + const msUntilExpiry = expiryTime - now; - return daysUntilExpiry <= 3 && daysUntilExpiry > 0; - }; + return { + needsRefresh: msUntilExpiry > 0 + && msUntilExpiry <= REFRESH_THRESHOLD_DAYS * 24 * 60 * 60 * 1000, + isValid: msUntilExpiry > 0, + }; + }, [getLicense.data]); const activateLicense = useMutation({ mutationFn: async (key: string) => { const license = await keygen.validateCheckoutKey({ key, entitlements: [], - ttlSeconds: 60 * 60 * 24 * 7, // 7 days + ttlSeconds: LICENSE_TTL_SECONDS, ttlForever: false, }); return license; @@ -97,7 +98,7 @@ export function useLicense() { getLicense, activateLicense, deactivateLicense, - shouldRefresh, + getLicenseStatus, refreshLicense, }; } diff --git a/apps/desktop/src/locales/en/messages.po b/apps/desktop/src/locales/en/messages.po index 1d445906b1..76506350d6 100644 --- a/apps/desktop/src/locales/en/messages.po +++ b/apps/desktop/src/locales/en/messages.po @@ -256,7 +256,7 @@ msgstr "(Beta) Detect meetings automatically" msgid "(Beta) Upcoming meeting notifications" msgstr "(Beta) Upcoming meeting notifications" -#: src/components/settings/components/ai/llm-custom-view.tsx:603 +#: src/components/settings/components/ai/llm-custom-view.tsx:594 msgid "(Optional for localhost)" msgstr "(Optional for localhost)" @@ -265,10 +265,9 @@ msgid "(Optional)" msgstr "(Optional)" #. placeholder {0}: isViewingTemplate ? "Back" : "Save and close" -#. placeholder {0}: lang.language #. placeholder {0}: disabled ? "Wait..." : isHovered ? "Resume" : "Ended" +#. placeholder {0}: disabled ? "Wait..." : "Play video" #: src/components/settings/views/templates.tsx:217 -#: src/components/settings/components/wer-modal.tsx:116 #: src/components/editor-area/note-header/listen-button.tsx:216 #: src/components/editor-area/note-header/listen-button.tsx:238 #: src/components/editor-area/note-header/listen-button.tsx:258 @@ -286,8 +285,8 @@ msgid "{buttonText}" msgstr "{buttonText}" #: src/components/settings/components/wer-modal.tsx:103 -msgid "{category}" -msgstr "{category}" +#~ msgid "{category}" +#~ msgstr "{category}" #: src/components/settings/views/lab.tsx:77 msgid "{description}" @@ -324,7 +323,7 @@ msgstr "Access granted" msgid "Access Granted" msgstr "Access Granted" -#: src/components/settings/components/ai/llm-custom-view.tsx:468 +#: src/components/settings/components/ai/llm-custom-view.tsx:459 msgid "Access multiple AI models through OpenRouter with your API key" msgstr "Access multiple AI models through OpenRouter with your API key" @@ -370,8 +369,8 @@ msgid "After you grant system audio access, app will restart to apply the change msgstr "After you grant system audio access, app will restart to apply the changes" #: src/routes/app.settings.tsx:68 -msgid "AI" -msgstr "AI" +#~ msgid "AI" +#~ msgstr "AI" #: src/components/login-modal.tsx:97 #~ msgid "AI notepad for meetings" @@ -399,7 +398,7 @@ msgid "Anyone with the link can view this page" msgstr "Anyone with the link can view this page" #: src/components/welcome-modal/custom-endpoint-view.tsx:498 -#: src/components/settings/components/ai/llm-custom-view.tsx:578 +#: src/components/settings/components/ai/llm-custom-view.tsx:569 msgid "API Base URL" msgstr "API Base URL" @@ -408,10 +407,10 @@ msgstr "API Base URL" #: src/components/welcome-modal/custom-endpoint-view.tsx:438 #: src/components/welcome-modal/custom-endpoint-view.tsx:518 #: src/components/settings/views/integrations.tsx:197 -#: src/components/settings/components/ai/llm-custom-view.tsx:286 -#: src/components/settings/components/ai/llm-custom-view.tsx:382 -#: src/components/settings/components/ai/llm-custom-view.tsx:488 -#: src/components/settings/components/ai/llm-custom-view.tsx:600 +#: src/components/settings/components/ai/llm-custom-view.tsx:277 +#: src/components/settings/components/ai/llm-custom-view.tsx:373 +#: src/components/settings/components/ai/llm-custom-view.tsx:479 +#: src/components/settings/components/ai/llm-custom-view.tsx:591 msgid "API Key" msgstr "API Key" @@ -440,7 +439,7 @@ msgstr "Audio Permissions" #~ msgid "Auto (Default)" #~ msgstr "Auto (Default)" -#: src/components/settings/views/ai.tsx:833 +#: src/components/settings/views/ai-llm.tsx:642 msgid "Autonomy Selector" msgstr "Autonomy Selector" @@ -470,7 +469,7 @@ msgstr "Base URL" msgid "Built-in Templates" msgstr "Built-in Templates" -#: src/routes/app.settings.tsx:70 +#: src/routes/app.settings.tsx:58 msgid "Calendar" msgstr "Calendar" @@ -535,8 +534,8 @@ msgstr "Choose Your LLM" #~ msgstr "Choose your preferred language of use" #: src/components/settings/components/wer-modal.tsx:126 -msgid "Close" -msgstr "Close" +#~ msgid "Close" +#~ msgstr "Close" #: src/components/settings/views/billing.tsx:52 #~ msgid "Collaborate with others in meetings" @@ -566,7 +565,7 @@ msgstr "Configure Your LLM" #~ msgid "Connect" #~ msgstr "Connect" -#: src/components/settings/components/ai/llm-custom-view.tsx:558 +#: src/components/settings/components/ai/llm-custom-view.tsx:549 msgid "Connect to a self-hosted or third-party LLM endpoint (OpenAI API compatible)" msgstr "Connect to a self-hosted or third-party LLM endpoint (OpenAI API compatible)" @@ -615,7 +614,7 @@ msgstr "Continue" #~ msgid "Continue Setup" #~ msgstr "Continue Setup" -#: src/components/settings/views/ai.tsx:852 +#: src/components/settings/views/ai-llm.tsx:661 msgid "Control how autonomous the AI enhancement should be" msgstr "Control how autonomous the AI enhancement should be" @@ -656,13 +655,18 @@ msgstr "Create your first template to get started" #~ msgid "Current Plan" #~ msgstr "Current Plan" +#: src/components/settings/views/ai-stt.tsx:66 +#: src/components/settings/views/ai-llm.tsx:620 +msgid "Custom" +msgstr "Custom" + #: src/components/settings/views/ai.tsx:747 #~ msgid "Custom Endpoint" #~ msgstr "Custom Endpoint" #: src/components/settings/components/ai/llm-custom-view.tsx:235 -msgid "Custom Endpoints" -msgstr "Custom Endpoints" +#~ msgid "Custom Endpoints" +#~ msgstr "Custom Endpoints" #: src/components/settings/views/general.tsx:350 msgid "Custom Vocabulary" @@ -758,7 +762,7 @@ msgstr "Enter a section title" #~ msgid "Enter the API key for your custom LLM endpoint" #~ msgstr "Enter the API key for your custom LLM endpoint" -#: src/components/settings/components/ai/llm-custom-view.tsx:581 +#: src/components/settings/components/ai/llm-custom-view.tsx:572 msgid "Enter the base URL for your custom LLM endpoint" msgstr "Enter the base URL for your custom LLM endpoint" @@ -786,8 +790,8 @@ msgstr "Enter the base URL for your custom LLM endpoint" msgid "Extract action items" msgstr "Extract action items" -#: src/routes/app.settings.tsx:80 -#: src/routes/app.settings.tsx:115 +#: src/routes/app.settings.tsx:68 +#: src/routes/app.settings.tsx:103 msgid "Feedback" msgstr "Feedback" @@ -823,7 +827,7 @@ msgstr "Finish Onboarding" msgid "Full name" msgstr "Full name" -#: src/routes/app.settings.tsx:66 +#: src/routes/app.settings.tsx:52 msgid "General" msgstr "General" @@ -835,7 +839,7 @@ msgstr "Generating title..." msgid "Get Started" msgstr "Get Started" -#: src/components/settings/components/ai/llm-custom-view.tsx:358 +#: src/components/settings/components/ai/llm-custom-view.tsx:349 msgid "Google Gemini" msgstr "Google Gemini" @@ -891,11 +895,15 @@ msgstr "Important Q&As" #~ msgid "Integration with other apps like Notion and Google Calendar" #~ msgstr "Integration with other apps like Notion and Google Calendar" -#: src/routes/app.settings.tsx:78 +#: src/routes/app.settings.tsx:66 #: src/components/settings/views/integrations.tsx:118 msgid "Integrations" msgstr "Integrations" +#: src/routes/app.settings.tsx:54 +msgid "Intelligence" +msgstr "Intelligence" + #: src/components/share-and-permission/invite-list.tsx:30 msgid "Invite" msgstr "Invite" @@ -932,7 +940,11 @@ msgstr "Join meeting" #~ msgid "Language" #~ msgstr "Language" -#: src/components/settings/views/ai.tsx:847 +#: src/routes/app.settings.tsx:54 +#~ msgid "Language Model" +#~ msgstr "Language Model" + +#: src/components/settings/views/ai-llm.tsx:656 msgid "Learn more about AI autonomy" msgstr "Learn more about AI autonomy" @@ -944,7 +956,7 @@ msgstr "Learn more about AI autonomy" msgid "Learn more about templates" msgstr "Learn more about templates" -#: src/routes/app.settings.tsx:82 +#: src/routes/app.settings.tsx:70 msgid "License" msgstr "License" @@ -957,14 +969,14 @@ msgstr "LinkedIn username" #~ msgstr "Live summary of the meeting" #: src/components/settings/views/ai.tsx:808 -msgid "LLM - Custom" -msgstr "LLM - Custom" +#~ msgid "LLM - Custom" +#~ msgstr "LLM - Custom" #: src/components/settings/views/ai.tsx:805 -msgid "LLM - Local" -msgstr "LLM - Local" +#~ msgid "LLM - Local" +#~ msgstr "LLM - Local" -#: src/components/settings/components/ai/llm-custom-view.tsx:637 +#: src/components/settings/components/ai/llm-custom-view.tsx:628 msgid "Loading available models..." msgstr "Loading available models..." @@ -984,13 +996,18 @@ msgstr "Loading templates..." msgid "Loading..." msgstr "Loading..." +#: src/components/settings/views/ai-stt.tsx:63 +#: src/components/settings/views/ai-llm.tsx:617 +msgid "Local" +msgstr "Local" + #: src/components/left-sidebar/top-area/settings-button.tsx:87 #~ msgid "Local mode" #~ msgstr "Local mode" #: src/components/settings/components/ai/llm-local-view.tsx:37 -msgid "Local Models" -msgstr "Local Models" +#~ msgid "Local Models" +#~ msgstr "Local Models" #: src/components/settings/views/billing.tsx:39 #~ msgid "Long-term memory for past meetings and attendees" @@ -1021,14 +1038,14 @@ msgstr "Microphone Access" #: src/components/welcome-modal/custom-endpoint-view.tsx:315 #: src/components/welcome-modal/custom-endpoint-view.tsx:382 #: src/components/welcome-modal/custom-endpoint-view.tsx:459 -#: src/components/settings/components/ai/llm-custom-view.tsx:306 -#: src/components/settings/components/ai/llm-custom-view.tsx:402 -#: src/components/settings/components/ai/llm-custom-view.tsx:508 +#: src/components/settings/components/ai/llm-custom-view.tsx:297 +#: src/components/settings/components/ai/llm-custom-view.tsx:393 +#: src/components/settings/components/ai/llm-custom-view.tsx:499 msgid "Model" msgstr "Model" #: src/components/welcome-modal/custom-endpoint-view.tsx:544 -#: src/components/settings/components/ai/llm-custom-view.tsx:625 +#: src/components/settings/components/ai/llm-custom-view.tsx:616 msgid "Model Name" msgstr "Model Name" @@ -1121,7 +1138,7 @@ msgstr "No upcoming events for this organization" msgid "No upcoming events with this contact" msgstr "No upcoming events with this contact" -#: src/routes/app.settings.tsx:72 +#: src/routes/app.settings.tsx:60 msgid "Notifications" msgstr "Notifications" @@ -1156,11 +1173,11 @@ msgstr "Open in new window" #~ msgid "Open Note" #~ msgstr "Open Note" -#: src/components/settings/components/ai/llm-custom-view.tsx:262 +#: src/components/settings/components/ai/llm-custom-view.tsx:253 msgid "OpenAI" msgstr "OpenAI" -#: src/components/settings/components/ai/llm-custom-view.tsx:464 +#: src/components/settings/components/ai/llm-custom-view.tsx:455 msgid "OpenRouter" msgstr "OpenRouter" @@ -1172,7 +1189,7 @@ msgstr "Optional base folder path within your Obsidian vault." msgid "Optional for participant suggestions" msgstr "Optional for participant suggestions" -#: src/components/settings/components/ai/llm-custom-view.tsx:555 +#: src/components/settings/components/ai/llm-custom-view.tsx:546 msgid "Others" msgstr "Others" @@ -1189,9 +1206,9 @@ msgstr "Pause" msgid "people" msgstr "people" -#: src/components/settings/components/ai/stt-view.tsx:328 -msgid "Performance difference between languages" -msgstr "Performance difference between languages" +#: src/components/settings/components/ai/stt-view-local.tsx:258 +#~ msgid "Performance difference between languages" +#~ msgstr "Performance difference between languages" #: src/components/welcome-modal/audio-permissions-view.tsx:180 #~ msgid "Permission granted! Detecting changes..." @@ -1237,6 +1254,11 @@ msgstr "Recent Notes" #~ msgid "Redemption Time" #~ msgstr "Redemption Time" +#: src/components/settings/views/ai-stt.tsx:66 +#: src/components/settings/views/ai-llm.tsx:620 +#~ msgid "Remote" +#~ msgstr "Remote" + #: src/components/editor-area/note-header/chips/participants-chip.tsx:222 #~ msgid "Remove {0} from list" #~ msgstr "Remove {0} from list" @@ -1313,7 +1335,7 @@ msgstr "Search..." msgid "Sections" msgstr "Sections" -#: src/components/settings/components/ai/llm-custom-view.tsx:628 +#: src/components/settings/components/ai/llm-custom-view.tsx:619 msgid "Select a model from the dropdown (if available) or manually enter the model name required by your endpoint." msgstr "Select a model from the dropdown (if available) or manually enter the model name required by your endpoint." @@ -1393,10 +1415,14 @@ msgstr "Show notifications when you join a meeting." msgid "Some downloads failed, but you can continue" msgstr "Some downloads failed, but you can continue" -#: src/routes/app.settings.tsx:76 +#: src/routes/app.settings.tsx:64 msgid "Sound" msgstr "Sound" +#: src/routes/app.settings.tsx:56 +#~ msgid "Speech to Text Model" +#~ msgstr "Speech to Text Model" + #: src/components/settings/views/general.tsx:269 msgid "Spoken languages" msgstr "Spoken languages" @@ -1421,6 +1447,10 @@ msgstr "Stop" #~ msgid "Submit Feedback" #~ msgstr "Submit Feedback" +#: src/routes/app.settings.tsx:54 +#~ msgid "Summarization" +#~ msgstr "Summarization" + #: src/components/right-panel/components/chat/empty-chat-state.tsx:61 #~ msgid "Summarize meeting" #~ msgstr "Summarize meeting" @@ -1458,7 +1488,7 @@ msgstr "Teamspace" msgid "Template" msgstr "Template" -#: src/routes/app.settings.tsx:74 +#: src/routes/app.settings.tsx:62 msgid "Templates" msgstr "Templates" @@ -1510,11 +1540,11 @@ msgstr "Toggle left sidebar" msgid "Toggle widget panel" msgstr "Toggle widget panel" -#: src/components/settings/components/ai/stt-view.tsx:319 -msgid "Transcribing" -msgstr "Transcribing" +#: src/components/settings/components/ai/stt-view-local.tsx:249 +#~ msgid "Transcribing" +#~ msgstr "Transcribing" -#: src/components/settings/views/ai.tsx:802 +#: src/routes/app.settings.tsx:56 msgid "Transcription" msgstr "Transcription" @@ -1554,11 +1584,11 @@ msgstr "Upcoming Events" #~ msgid "Upgrade" #~ msgstr "Upgrade" -#: src/components/settings/components/ai/llm-custom-view.tsx:362 +#: src/components/settings/components/ai/llm-custom-view.tsx:353 msgid "Use Google's Gemini models with your API key" msgstr "Use Google's Gemini models with your API key" -#: src/components/settings/components/ai/llm-custom-view.tsx:266 +#: src/components/settings/components/ai/llm-custom-view.tsx:257 msgid "Use OpenAI's GPT models with your API key" msgstr "Use OpenAI's GPT models with your API key" @@ -1624,12 +1654,12 @@ msgid "Where Conversations Stay Yours" msgstr "Where Conversations Stay Yours" #: src/components/settings/components/wer-modal.tsx:69 -msgid "Whisper Model Language Performance (WER)" -msgstr "Whisper Model Language Performance (WER)" +#~ msgid "Whisper Model Language Performance (WER)" +#~ msgstr "Whisper Model Language Performance (WER)" #: src/components/settings/components/wer-modal.tsx:72 -msgid "Word Error Rate (WER) indicates transcription accuracy (lower is better). Data based on the FLEURS dataset, measured with OpenAI's Whisper <0>large-v3-turbo model. <1>More info" -msgstr "Word Error Rate (WER) indicates transcription accuracy (lower is better). Data based on the FLEURS dataset, measured with OpenAI's Whisper <0>large-v3-turbo model. <1>More info" +#~ msgid "Word Error Rate (WER) indicates transcription accuracy (lower is better). Data based on the FLEURS dataset, measured with OpenAI's Whisper <0>large-v3-turbo model. <1>More info" +#~ msgstr "Word Error Rate (WER) indicates transcription accuracy (lower is better). Data based on the FLEURS dataset, measured with OpenAI's Whisper <0>large-v3-turbo model. <1>More info" #: src/components/settings/views/billing.tsx:25 #~ msgid "Works both in-person and remotely" diff --git a/apps/desktop/src/locales/ko/messages.po b/apps/desktop/src/locales/ko/messages.po index 1f5df285e6..fc93d881a5 100644 --- a/apps/desktop/src/locales/ko/messages.po +++ b/apps/desktop/src/locales/ko/messages.po @@ -256,7 +256,7 @@ msgstr "" msgid "(Beta) Upcoming meeting notifications" msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:603 +#: src/components/settings/components/ai/llm-custom-view.tsx:594 msgid "(Optional for localhost)" msgstr "" @@ -265,10 +265,9 @@ msgid "(Optional)" msgstr "" #. placeholder {0}: isViewingTemplate ? "Back" : "Save and close" -#. placeholder {0}: lang.language #. placeholder {0}: disabled ? "Wait..." : isHovered ? "Resume" : "Ended" +#. placeholder {0}: disabled ? "Wait..." : "Play video" #: src/components/settings/views/templates.tsx:217 -#: src/components/settings/components/wer-modal.tsx:116 #: src/components/editor-area/note-header/listen-button.tsx:216 #: src/components/editor-area/note-header/listen-button.tsx:238 #: src/components/editor-area/note-header/listen-button.tsx:258 @@ -286,8 +285,8 @@ msgid "{buttonText}" msgstr "" #: src/components/settings/components/wer-modal.tsx:103 -msgid "{category}" -msgstr "" +#~ msgid "{category}" +#~ msgstr "" #: src/components/settings/views/lab.tsx:77 msgid "{description}" @@ -324,7 +323,7 @@ msgstr "" msgid "Access Granted" msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:468 +#: src/components/settings/components/ai/llm-custom-view.tsx:459 msgid "Access multiple AI models through OpenRouter with your API key" msgstr "" @@ -370,8 +369,8 @@ msgid "After you grant system audio access, app will restart to apply the change msgstr "" #: src/routes/app.settings.tsx:68 -msgid "AI" -msgstr "" +#~ msgid "AI" +#~ msgstr "" #: src/components/login-modal.tsx:97 #~ msgid "AI notepad for meetings" @@ -399,7 +398,7 @@ msgid "Anyone with the link can view this page" msgstr "" #: src/components/welcome-modal/custom-endpoint-view.tsx:498 -#: src/components/settings/components/ai/llm-custom-view.tsx:578 +#: src/components/settings/components/ai/llm-custom-view.tsx:569 msgid "API Base URL" msgstr "" @@ -408,10 +407,10 @@ msgstr "" #: src/components/welcome-modal/custom-endpoint-view.tsx:438 #: src/components/welcome-modal/custom-endpoint-view.tsx:518 #: src/components/settings/views/integrations.tsx:197 -#: src/components/settings/components/ai/llm-custom-view.tsx:286 -#: src/components/settings/components/ai/llm-custom-view.tsx:382 -#: src/components/settings/components/ai/llm-custom-view.tsx:488 -#: src/components/settings/components/ai/llm-custom-view.tsx:600 +#: src/components/settings/components/ai/llm-custom-view.tsx:277 +#: src/components/settings/components/ai/llm-custom-view.tsx:373 +#: src/components/settings/components/ai/llm-custom-view.tsx:479 +#: src/components/settings/components/ai/llm-custom-view.tsx:591 msgid "API Key" msgstr "" @@ -440,7 +439,7 @@ msgstr "" #~ msgid "Auto (Default)" #~ msgstr "" -#: src/components/settings/views/ai.tsx:833 +#: src/components/settings/views/ai-llm.tsx:642 msgid "Autonomy Selector" msgstr "" @@ -470,7 +469,7 @@ msgstr "" msgid "Built-in Templates" msgstr "" -#: src/routes/app.settings.tsx:70 +#: src/routes/app.settings.tsx:58 msgid "Calendar" msgstr "" @@ -535,8 +534,8 @@ msgstr "" #~ msgstr "" #: src/components/settings/components/wer-modal.tsx:126 -msgid "Close" -msgstr "" +#~ msgid "Close" +#~ msgstr "" #: src/components/settings/views/billing.tsx:52 #~ msgid "Collaborate with others in meetings" @@ -566,7 +565,7 @@ msgstr "" #~ msgid "Connect" #~ msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:558 +#: src/components/settings/components/ai/llm-custom-view.tsx:549 msgid "Connect to a self-hosted or third-party LLM endpoint (OpenAI API compatible)" msgstr "" @@ -615,7 +614,7 @@ msgstr "" #~ msgid "Continue Setup" #~ msgstr "" -#: src/components/settings/views/ai.tsx:852 +#: src/components/settings/views/ai-llm.tsx:661 msgid "Control how autonomous the AI enhancement should be" msgstr "" @@ -656,13 +655,18 @@ msgstr "" #~ msgid "Current Plan" #~ msgstr "" +#: src/components/settings/views/ai-stt.tsx:66 +#: src/components/settings/views/ai-llm.tsx:620 +msgid "Custom" +msgstr "" + #: src/components/settings/views/ai.tsx:747 #~ msgid "Custom Endpoint" #~ msgstr "" #: src/components/settings/components/ai/llm-custom-view.tsx:235 -msgid "Custom Endpoints" -msgstr "" +#~ msgid "Custom Endpoints" +#~ msgstr "" #: src/components/settings/views/general.tsx:350 msgid "Custom Vocabulary" @@ -758,7 +762,7 @@ msgstr "" #~ msgid "Enter the API key for your custom LLM endpoint" #~ msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:581 +#: src/components/settings/components/ai/llm-custom-view.tsx:572 msgid "Enter the base URL for your custom LLM endpoint" msgstr "" @@ -786,8 +790,8 @@ msgstr "" msgid "Extract action items" msgstr "" -#: src/routes/app.settings.tsx:80 -#: src/routes/app.settings.tsx:115 +#: src/routes/app.settings.tsx:68 +#: src/routes/app.settings.tsx:103 msgid "Feedback" msgstr "" @@ -823,7 +827,7 @@ msgstr "" msgid "Full name" msgstr "" -#: src/routes/app.settings.tsx:66 +#: src/routes/app.settings.tsx:52 msgid "General" msgstr "" @@ -835,7 +839,7 @@ msgstr "" msgid "Get Started" msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:358 +#: src/components/settings/components/ai/llm-custom-view.tsx:349 msgid "Google Gemini" msgstr "" @@ -891,11 +895,15 @@ msgstr "" #~ msgid "Integration with other apps like Notion and Google Calendar" #~ msgstr "" -#: src/routes/app.settings.tsx:78 +#: src/routes/app.settings.tsx:66 #: src/components/settings/views/integrations.tsx:118 msgid "Integrations" msgstr "" +#: src/routes/app.settings.tsx:54 +msgid "Intelligence" +msgstr "" + #: src/components/share-and-permission/invite-list.tsx:30 msgid "Invite" msgstr "" @@ -932,7 +940,11 @@ msgstr "" #~ msgid "Language" #~ msgstr "" -#: src/components/settings/views/ai.tsx:847 +#: src/routes/app.settings.tsx:54 +#~ msgid "Language Model" +#~ msgstr "" + +#: src/components/settings/views/ai-llm.tsx:656 msgid "Learn more about AI autonomy" msgstr "" @@ -944,7 +956,7 @@ msgstr "" msgid "Learn more about templates" msgstr "" -#: src/routes/app.settings.tsx:82 +#: src/routes/app.settings.tsx:70 msgid "License" msgstr "" @@ -957,14 +969,14 @@ msgstr "" #~ msgstr "" #: src/components/settings/views/ai.tsx:808 -msgid "LLM - Custom" -msgstr "" +#~ msgid "LLM - Custom" +#~ msgstr "" #: src/components/settings/views/ai.tsx:805 -msgid "LLM - Local" -msgstr "" +#~ msgid "LLM - Local" +#~ msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:637 +#: src/components/settings/components/ai/llm-custom-view.tsx:628 msgid "Loading available models..." msgstr "" @@ -984,13 +996,18 @@ msgstr "" msgid "Loading..." msgstr "" +#: src/components/settings/views/ai-stt.tsx:63 +#: src/components/settings/views/ai-llm.tsx:617 +msgid "Local" +msgstr "" + #: src/components/left-sidebar/top-area/settings-button.tsx:87 #~ msgid "Local mode" #~ msgstr "" #: src/components/settings/components/ai/llm-local-view.tsx:37 -msgid "Local Models" -msgstr "" +#~ msgid "Local Models" +#~ msgstr "" #: src/components/settings/views/billing.tsx:39 #~ msgid "Long-term memory for past meetings and attendees" @@ -1021,14 +1038,14 @@ msgstr "" #: src/components/welcome-modal/custom-endpoint-view.tsx:315 #: src/components/welcome-modal/custom-endpoint-view.tsx:382 #: src/components/welcome-modal/custom-endpoint-view.tsx:459 -#: src/components/settings/components/ai/llm-custom-view.tsx:306 -#: src/components/settings/components/ai/llm-custom-view.tsx:402 -#: src/components/settings/components/ai/llm-custom-view.tsx:508 +#: src/components/settings/components/ai/llm-custom-view.tsx:297 +#: src/components/settings/components/ai/llm-custom-view.tsx:393 +#: src/components/settings/components/ai/llm-custom-view.tsx:499 msgid "Model" msgstr "" #: src/components/welcome-modal/custom-endpoint-view.tsx:544 -#: src/components/settings/components/ai/llm-custom-view.tsx:625 +#: src/components/settings/components/ai/llm-custom-view.tsx:616 msgid "Model Name" msgstr "" @@ -1121,7 +1138,7 @@ msgstr "" msgid "No upcoming events with this contact" msgstr "" -#: src/routes/app.settings.tsx:72 +#: src/routes/app.settings.tsx:60 msgid "Notifications" msgstr "" @@ -1156,11 +1173,11 @@ msgstr "" #~ msgid "Open Note" #~ msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:262 +#: src/components/settings/components/ai/llm-custom-view.tsx:253 msgid "OpenAI" msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:464 +#: src/components/settings/components/ai/llm-custom-view.tsx:455 msgid "OpenRouter" msgstr "" @@ -1172,7 +1189,7 @@ msgstr "" msgid "Optional for participant suggestions" msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:555 +#: src/components/settings/components/ai/llm-custom-view.tsx:546 msgid "Others" msgstr "" @@ -1189,9 +1206,9 @@ msgstr "" msgid "people" msgstr "" -#: src/components/settings/components/ai/stt-view.tsx:328 -msgid "Performance difference between languages" -msgstr "" +#: src/components/settings/components/ai/stt-view-local.tsx:258 +#~ msgid "Performance difference between languages" +#~ msgstr "" #: src/components/welcome-modal/audio-permissions-view.tsx:180 #~ msgid "Permission granted! Detecting changes..." @@ -1237,6 +1254,11 @@ msgstr "" #~ msgid "Redemption Time" #~ msgstr "" +#: src/components/settings/views/ai-stt.tsx:66 +#: src/components/settings/views/ai-llm.tsx:620 +#~ msgid "Remote" +#~ msgstr "" + #: src/components/editor-area/note-header/chips/participants-chip.tsx:222 #~ msgid "Remove {0} from list" #~ msgstr "" @@ -1313,7 +1335,7 @@ msgstr "" msgid "Sections" msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:628 +#: src/components/settings/components/ai/llm-custom-view.tsx:619 msgid "Select a model from the dropdown (if available) or manually enter the model name required by your endpoint." msgstr "" @@ -1393,10 +1415,14 @@ msgstr "" msgid "Some downloads failed, but you can continue" msgstr "" -#: src/routes/app.settings.tsx:76 +#: src/routes/app.settings.tsx:64 msgid "Sound" msgstr "" +#: src/routes/app.settings.tsx:56 +#~ msgid "Speech to Text Model" +#~ msgstr "" + #: src/components/settings/views/general.tsx:269 msgid "Spoken languages" msgstr "" @@ -1421,6 +1447,10 @@ msgstr "" #~ msgid "Submit Feedback" #~ msgstr "" +#: src/routes/app.settings.tsx:54 +#~ msgid "Summarization" +#~ msgstr "" + #: src/components/right-panel/components/chat/empty-chat-state.tsx:61 #~ msgid "Summarize meeting" #~ msgstr "" @@ -1458,7 +1488,7 @@ msgstr "" msgid "Template" msgstr "" -#: src/routes/app.settings.tsx:74 +#: src/routes/app.settings.tsx:62 msgid "Templates" msgstr "" @@ -1510,11 +1540,11 @@ msgstr "" msgid "Toggle widget panel" msgstr "" -#: src/components/settings/components/ai/stt-view.tsx:319 -msgid "Transcribing" -msgstr "" +#: src/components/settings/components/ai/stt-view-local.tsx:249 +#~ msgid "Transcribing" +#~ msgstr "" -#: src/components/settings/views/ai.tsx:802 +#: src/routes/app.settings.tsx:56 msgid "Transcription" msgstr "" @@ -1554,11 +1584,11 @@ msgstr "" #~ msgid "Upgrade" #~ msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:362 +#: src/components/settings/components/ai/llm-custom-view.tsx:353 msgid "Use Google's Gemini models with your API key" msgstr "" -#: src/components/settings/components/ai/llm-custom-view.tsx:266 +#: src/components/settings/components/ai/llm-custom-view.tsx:257 msgid "Use OpenAI's GPT models with your API key" msgstr "" @@ -1624,12 +1654,12 @@ msgid "Where Conversations Stay Yours" msgstr "" #: src/components/settings/components/wer-modal.tsx:69 -msgid "Whisper Model Language Performance (WER)" -msgstr "" +#~ msgid "Whisper Model Language Performance (WER)" +#~ msgstr "" #: src/components/settings/components/wer-modal.tsx:72 -msgid "Word Error Rate (WER) indicates transcription accuracy (lower is better). Data based on the FLEURS dataset, measured with OpenAI's Whisper <0>large-v3-turbo model. <1>More info" -msgstr "" +#~ msgid "Word Error Rate (WER) indicates transcription accuracy (lower is better). Data based on the FLEURS dataset, measured with OpenAI's Whisper <0>large-v3-turbo model. <1>More info" +#~ msgstr "" #: src/components/settings/views/billing.tsx:25 #~ msgid "Works both in-person and remotely" diff --git a/apps/desktop/src/routes/app.settings.tsx b/apps/desktop/src/routes/app.settings.tsx index 8b134ff0e8..4c225fef1a 100644 --- a/apps/desktop/src/routes/app.settings.tsx +++ b/apps/desktop/src/routes/app.settings.tsx @@ -3,22 +3,21 @@ import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router" import { zodValidator } from "@tanstack/zod-adapter"; import { openUrl } from "@tauri-apps/plugin-opener"; import { ExternalLinkIcon } from "lucide-react"; -import { useEffect } from "react"; import { z } from "zod"; import { TabIcon } from "@/components/settings/components/tab-icon"; import { type Tab, TABS } from "@/components/settings/components/types"; import { + AILLM, + AISTT, Billing, Calendar, General, Integrations, - LocalAI, Notifications, Sound, TemplatesView, } from "@/components/settings/views"; -import { commands as connectorCommands } from "@hypr/plugin-connector"; import { cn } from "@hypr/ui/lib/utils"; const schema = z.object({ @@ -39,19 +38,6 @@ function Component() { const navigate = useNavigate(); const search = useSearch({ from: PATH }); - // TODO: this is a hack - useEffect(() => { - if (search.baseUrl && search.apiKey) { - connectorCommands.setCustomLlmConnection({ - api_base: search.baseUrl, - api_key: search.apiKey, - }).then(() => { - connectorCommands.setCustomLlmEnabled(true); - navigate({ to: PATH, search: { tab: "ai" } }); - }); - } - }, [search.baseUrl, search.apiKey]); - const handleClickTab = (tab: Tab) => { if (tab === "feedback") { openUrl("https://hyprnote.canny.io/feature-requests"); @@ -64,8 +50,10 @@ function Component() { switch (tab) { case "general": return t`General`; - case "ai": - return t`AI`; + case "ai-llm": + return t`Intelligence`; + case "ai-stt": + return t`Transcription`; case "calendar": return t`Calendar`; case "notifications": @@ -143,7 +131,8 @@ function Component() { {search.tab === "calendar" && } {search.tab === "notifications" && } {search.tab === "sound" && } - {search.tab === "ai" && } + {search.tab === "ai-stt" && } + {search.tab === "ai-llm" && } {search.tab === "templates" && } {search.tab === "integrations" && } {search.tab === "billing" && } diff --git a/crates/am/Cargo.toml b/crates/am/Cargo.toml index 787e8c80b1..dcc41772a7 100644 --- a/crates/am/Cargo.toml +++ b/crates/am/Cargo.toml @@ -8,6 +8,8 @@ tokio = { workspace = true, features = ["rt", "macros"] } [dependencies] reqwest = { workspace = true, features = ["json"] } + serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +specta = { workspace = true, features = ["derive"] } thiserror = { workspace = true } diff --git a/crates/am/src/client.rs b/crates/am/src/client.rs index 06f217eca8..b1fcfd462f 100644 --- a/crates/am/src/client.rs +++ b/crates/am/src/client.rs @@ -4,12 +4,12 @@ use crate::{ use reqwest::{Response, StatusCode}; #[derive(Clone)] -pub struct AmClient { +pub struct Client { client: reqwest::Client, base_url: String, } -impl AmClient { +impl Client { pub fn new(base_url: impl Into) -> Self { Self { client: reqwest::Client::new(), @@ -198,7 +198,7 @@ impl InitRequest { } } -impl Default for AmClient { +impl Default for Client { fn default() -> Self { Self::new("http://localhost:50060") } diff --git a/crates/am/src/lib.rs b/crates/am/src/lib.rs index 654b52e354..ab3cec1ffb 100644 --- a/crates/am/src/lib.rs +++ b/crates/am/src/lib.rs @@ -1,9 +1,11 @@ mod client; mod error; +mod model; mod types; pub use client::*; pub use error::*; +pub use model::*; pub use types::*; #[cfg(test)] @@ -12,7 +14,7 @@ mod tests { #[tokio::test] async fn test_client_creation() { - let client = AmClient::new("http://localhost:50060"); + let client = Client::new("http://localhost:50060"); let status = client.status().await; println!("{:?}", status); assert!(true); diff --git a/crates/am/src/model.rs b/crates/am/src/model.rs new file mode 100644 index 0000000000..86e6baebe7 --- /dev/null +++ b/crates/am/src/model.rs @@ -0,0 +1,54 @@ +#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum Model { + ParakeetV2, + WhisperLargeV3, + WhisperSmallEn, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)] +pub struct ModelInfo { + pub key: String, + pub name: String, + pub size_bytes: u64, +} + +impl Model { + pub fn info(&self) -> ModelInfo { + ModelInfo { + key: self.model_key().to_string(), + name: self.display_name().to_string(), + size_bytes: self.model_size(), + } + } + + pub fn repo_name(&self) -> &str { + match self { + Model::ParakeetV2 => "argmaxinc/parakeetkit-pro", + Model::WhisperLargeV3 => "argmaxinc/whisperkit-pro", + Model::WhisperSmallEn => "argmaxinc/whisperkit-pro", + } + } + pub fn model_key(&self) -> &str { + match self { + Model::ParakeetV2 => "parakeet-v2_476MB", + Model::WhisperLargeV3 => "large-v3-v20240930_626MB", + Model::WhisperSmallEn => "small.en_217MB", + } + } + + pub fn display_name(&self) -> &str { + match self { + Model::ParakeetV2 => "Parakeet V2 (English)", + Model::WhisperLargeV3 => "Whisper Large V3 (English)", + Model::WhisperSmallEn => "Whisper Small (English)", + } + } + + pub fn model_size(&self) -> u64 { + match self { + Model::ParakeetV2 => 476 * 1024 * 1024, + Model::WhisperLargeV3 => 626 * 1024 * 1024, + Model::WhisperSmallEn => 217 * 1024 * 1024, + } + } +} diff --git a/owhisper/owhisper-server/src/commands/run/realtime.rs b/owhisper/owhisper-server/src/commands/run/realtime.rs index 438e6b8248..6bbc2d77c9 100644 --- a/owhisper/owhisper-server/src/commands/run/realtime.rs +++ b/owhisper/owhisper-server/src/commands/run/realtime.rs @@ -148,7 +148,7 @@ async fn run_audio_stream_with_stop( data.update(rms); } - hypr_audio_utils::f32_to_i16_bytes(samples) + hypr_audio_utils::f32_to_i16_bytes(samples.into_iter()) }) }; diff --git a/plugins/listener/src/fsm.rs b/plugins/listener/src/fsm.rs index edc29daea5..1e194b5a09 100644 --- a/plugins/listener/src/fsm.rs +++ b/plugins/listener/src/fsm.rs @@ -207,7 +207,7 @@ impl Session { let user_id = self.app.db_user_id().await?.unwrap(); self.session_id = Some(session_id.clone()); - let (record, languages, jargons, redemption_time_ms) = { + let (record, languages, jargons) = { let config = self.app.db_get_config(&user_id).await?; let record = config @@ -223,11 +223,7 @@ impl Session { .as_ref() .map_or_else(Vec::new, |c| c.general.jargons.clone()); - let redemption_time_ms = config - .as_ref() - .map_or_else(|| 500, |c| c.ai.redemption_time_ms.unwrap_or(500)); - - (record, languages, jargons, redemption_time_ms) + (record, languages, jargons) }; let (mic_muted_tx, mic_muted_rx_main) = tokio::sync::watch::channel(false); @@ -248,7 +244,6 @@ impl Session { languages, jargons, session_id == onboarding_session_id, - redemption_time_ms, ) .await?; @@ -572,7 +567,6 @@ async fn setup_listen_client( languages: Vec, _jargons: Vec, is_onboarding: bool, - redemption_time_ms: u32, ) -> Result { let api_base = { use tauri_plugin_connector::{Connection, ConnectorPluginExt}; @@ -598,11 +592,7 @@ async fn setup_listen_client( .params(owhisper_interface::ListenParams { languages, static_prompt, - redemption_time_ms: if is_onboarding { - 70 - } else { - redemption_time_ms.into() - }, + redemption_time_ms: if is_onboarding { 70 } else { 500 }, ..Default::default() }) .build_dual()) diff --git a/plugins/local-llm/build.rs b/plugins/local-llm/build.rs index 6a34ca67de..51169c4d3e 100644 --- a/plugins/local-llm/build.rs +++ b/plugins/local-llm/build.rs @@ -10,6 +10,7 @@ const COMMANDS: &[&str] = &[ "get_current_model", "set_current_model", "list_downloaded_model", + "list_supported_model", ]; fn main() { diff --git a/plugins/local-llm/js/bindings.gen.ts b/plugins/local-llm/js/bindings.gen.ts index 0abb196503..cb4dcc6399 100644 --- a/plugins/local-llm/js/bindings.gen.ts +++ b/plugins/local-llm/js/bindings.gen.ts @@ -10,8 +10,8 @@ export const commands = { async modelsDir() : Promise { return await TAURI_INVOKE("plugin:local-llm|models_dir"); }, -async listSupportedModels() : Promise { - return await TAURI_INVOKE("plugin:local-llm|list_supported_models"); +async listSupportedModel() : Promise { + return await TAURI_INVOKE("plugin:local-llm|list_supported_model"); }, async isServerRunning() : Promise { return await TAURI_INVOKE("plugin:local-llm|is_server_running"); @@ -55,7 +55,8 @@ async listDownloadedModel() : Promise { /** user-defined types **/ -export type SupportedModel = "Llama3p2_3bQ4" | "HyprLLM" +export type ModelInfo = { key: SupportedModel; name: string; description: string; size_bytes: number } +export type SupportedModel = "Llama3p2_3bQ4" | "HyprLLM" | "Gemma3_4bQ4" export type TAURI_CHANNEL = null /** tauri-specta globals **/ diff --git a/plugins/local-llm/permissions/autogenerated/commands/list_supported_model.toml b/plugins/local-llm/permissions/autogenerated/commands/list_supported_model.toml new file mode 100644 index 0000000000..5d601ebe7a --- /dev/null +++ b/plugins/local-llm/permissions/autogenerated/commands/list_supported_model.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-list-supported-model" +description = "Enables the list_supported_model command without any pre-configured scope." +commands.allow = ["list_supported_model"] + +[[permission]] +identifier = "deny-list-supported-model" +description = "Denies the list_supported_model command without any pre-configured scope." +commands.deny = ["list_supported_model"] diff --git a/plugins/local-llm/permissions/autogenerated/reference.md b/plugins/local-llm/permissions/autogenerated/reference.md index dbd98ee19a..b8e0c9cbbf 100644 --- a/plugins/local-llm/permissions/autogenerated/reference.md +++ b/plugins/local-llm/permissions/autogenerated/reference.md @@ -15,6 +15,7 @@ Default permissions for the plugin - `allow-get-current-model` - `allow-set-current-model` - `allow-list-downloaded-model` +- `allow-list-supported-model` ## Permission Table @@ -210,6 +211,32 @@ Denies the list_downloaded_model command without any pre-configured scope. +`local-llm:allow-list-supported-model` + + + + +Enables the list_supported_model command without any pre-configured scope. + + + + + + + +`local-llm:deny-list-supported-model` + + + + +Denies the list_supported_model command without any pre-configured scope. + + + + + + + `local-llm:allow-models-dir` diff --git a/plugins/local-llm/permissions/default.toml b/plugins/local-llm/permissions/default.toml index 484fd43a4b..bf50b933fa 100644 --- a/plugins/local-llm/permissions/default.toml +++ b/plugins/local-llm/permissions/default.toml @@ -12,4 +12,5 @@ permissions = [ "allow-get-current-model", "allow-set-current-model", "allow-list-downloaded-model", + "allow-list-supported-model", ] diff --git a/plugins/local-llm/permissions/schemas/schema.json b/plugins/local-llm/permissions/schemas/schema.json index 65c7de939e..fdbb3e3217 100644 --- a/plugins/local-llm/permissions/schemas/schema.json +++ b/plugins/local-llm/permissions/schemas/schema.json @@ -378,6 +378,18 @@ "const": "deny-list-downloaded-model", "markdownDescription": "Denies the list_downloaded_model command without any pre-configured scope." }, + { + "description": "Enables the list_supported_model command without any pre-configured scope.", + "type": "string", + "const": "allow-list-supported-model", + "markdownDescription": "Enables the list_supported_model command without any pre-configured scope." + }, + { + "description": "Denies the list_supported_model command without any pre-configured scope.", + "type": "string", + "const": "deny-list-supported-model", + "markdownDescription": "Denies the list_supported_model command without any pre-configured scope." + }, { "description": "Enables the models_dir command without any pre-configured scope.", "type": "string", @@ -439,10 +451,10 @@ "markdownDescription": "Denies the stop_server command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-models-dir`\n- `allow-is-server-running`\n- `allow-is-model-downloading`\n- `allow-is-model-downloaded`\n- `allow-download-model`\n- `allow-start-server`\n- `allow-stop-server`\n- `allow-restart-server`\n- `allow-get-current-model`\n- `allow-set-current-model`\n- `allow-list-downloaded-model`", + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-models-dir`\n- `allow-is-server-running`\n- `allow-is-model-downloading`\n- `allow-is-model-downloaded`\n- `allow-download-model`\n- `allow-start-server`\n- `allow-stop-server`\n- `allow-restart-server`\n- `allow-get-current-model`\n- `allow-set-current-model`\n- `allow-list-downloaded-model`\n- `allow-list-supported-model`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-models-dir`\n- `allow-is-server-running`\n- `allow-is-model-downloading`\n- `allow-is-model-downloaded`\n- `allow-download-model`\n- `allow-start-server`\n- `allow-stop-server`\n- `allow-restart-server`\n- `allow-get-current-model`\n- `allow-set-current-model`\n- `allow-list-downloaded-model`" + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-models-dir`\n- `allow-is-server-running`\n- `allow-is-model-downloading`\n- `allow-is-model-downloaded`\n- `allow-download-model`\n- `allow-start-server`\n- `allow-stop-server`\n- `allow-restart-server`\n- `allow-get-current-model`\n- `allow-set-current-model`\n- `allow-list-downloaded-model`\n- `allow-list-supported-model`" } ] } diff --git a/plugins/local-llm/src/commands.rs b/plugins/local-llm/src/commands.rs index de9816a674..28a4b4e2f6 100644 --- a/plugins/local-llm/src/commands.rs +++ b/plugins/local-llm/src/commands.rs @@ -1,4 +1,4 @@ -use crate::LocalLlmPluginExt; +use crate::{LocalLlmPluginExt, ModelInfo, SupportedModel}; use tauri::ipc::Channel; @@ -10,8 +10,27 @@ pub async fn models_dir(app: tauri::AppHandle) -> Result Result, String> { - Ok(crate::SUPPORTED_MODELS.to_vec()) +pub async fn list_supported_model() -> Result, String> { + Ok(vec![ + ModelInfo { + key: SupportedModel::HyprLLM, + name: "HyprLLM".to_string(), + description: "Experimental model trained by the Hyprnote team.".to_string(), + size_bytes: SupportedModel::HyprLLM.model_size(), + }, + ModelInfo { + key: SupportedModel::Gemma3_4bQ4, + name: "Gemma 3 4B Q4".to_string(), + description: "General purpose model. Heavier than HyprLLM.".to_string(), + size_bytes: SupportedModel::Gemma3_4bQ4.model_size(), + }, + ModelInfo { + key: SupportedModel::Llama3p2_3bQ4, + name: "Llama 3.2 3B Q4".to_string(), + description: "Not recommended. Exist only for backward compatibility.".to_string(), + size_bytes: SupportedModel::Llama3p2_3bQ4.model_size(), + }, + ]) } #[tauri::command] diff --git a/plugins/local-llm/src/lib.rs b/plugins/local-llm/src/lib.rs index 8ad43ce6dc..4430766db7 100644 --- a/plugins/local-llm/src/lib.rs +++ b/plugins/local-llm/src/lib.rs @@ -37,7 +37,7 @@ fn make_specta_builder() -> tauri_specta::Builder { .plugin_name(PLUGIN_NAME) .commands(tauri_specta::collect_commands![ commands::models_dir::, - commands::list_supported_models, + commands::list_supported_model, commands::is_server_running::, commands::is_model_downloaded::, commands::is_model_downloading::, diff --git a/plugins/local-llm/src/model.rs b/plugins/local-llm/src/model.rs index 0c1d132bad..855e5369b7 100644 --- a/plugins/local-llm/src/model.rs +++ b/plugins/local-llm/src/model.rs @@ -1,10 +1,22 @@ -pub static SUPPORTED_MODELS: &[SupportedModel] = - &[SupportedModel::Llama3p2_3bQ4, SupportedModel::HyprLLM]; +pub static SUPPORTED_MODELS: &[SupportedModel] = &[ + SupportedModel::Llama3p2_3bQ4, + SupportedModel::HyprLLM, + SupportedModel::Gemma3_4bQ4, +]; + +#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +pub struct ModelInfo { + pub key: SupportedModel, + pub name: String, + pub description: String, + pub size_bytes: u64, +} #[derive(Debug, Eq, Hash, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)] pub enum SupportedModel { Llama3p2_3bQ4, HyprLLM, + Gemma3_4bQ4, } impl SupportedModel { @@ -12,13 +24,15 @@ impl SupportedModel { match self { SupportedModel::Llama3p2_3bQ4 => "llm.gguf", SupportedModel::HyprLLM => "hypr-llm.gguf", + SupportedModel::Gemma3_4bQ4 => "gemma-3-4b-it-Q4_K_M.gguf", } } pub fn model_url(&self) -> &str { match self { SupportedModel::Llama3p2_3bQ4 => "https://storage2.hyprnote.com/v0/lmstudio-community/Llama-3.2-3B-Instruct-GGUF/main/Llama-3.2-3B-Instruct-Q4_K_M.gguf", - SupportedModel::HyprLLM => "https://storage2.hyprnote.com/v0/yujonglee/hypr-llm-sm/model_q4_k_m.gguf" + SupportedModel::HyprLLM => "https://storage2.hyprnote.com/v0/yujonglee/hypr-llm-sm/model_q4_k_m.gguf", + SupportedModel::Gemma3_4bQ4 => "https://storage2.hyprnote.com/v0/unsloth/gemma-3-4b-it-GGUF/gemma-3-4b-it-Q4_K_M.gguf", } } @@ -26,6 +40,15 @@ impl SupportedModel { match self { SupportedModel::Llama3p2_3bQ4 => 2019377440, SupportedModel::HyprLLM => 1107409056, + SupportedModel::Gemma3_4bQ4 => 2489894016, + } + } + + pub fn model_checksum(&self) -> u64 { + match self { + SupportedModel::Llama3p2_3bQ4 => 2831308098, + SupportedModel::HyprLLM => 4037351144, + SupportedModel::Gemma3_4bQ4 => 2760830291, } } } diff --git a/plugins/local-stt/build.rs b/plugins/local-stt/build.rs index 5242dce590..342aed2412 100644 --- a/plugins/local-stt/build.rs +++ b/plugins/local-stt/build.rs @@ -6,9 +6,11 @@ const COMMANDS: &[&str] = &[ "download_model", "start_server", "stop_server", + "get_servers", "get_current_model", "set_current_model", "list_supported_models", + "list_pro_models", ]; fn main() { diff --git a/plugins/local-stt/js/bindings.gen.ts b/plugins/local-stt/js/bindings.gen.ts index fc4ce83aab..4ee3f326ce 100644 --- a/plugins/local-stt/js/bindings.gen.ts +++ b/plugins/local-stt/js/bindings.gen.ts @@ -13,9 +13,6 @@ async modelsDir() : Promise { async listGgmlBackends() : Promise { return await TAURI_INVOKE("plugin:local-stt|list_ggml_backends"); }, -async isServerRunning() : Promise { - return await TAURI_INVOKE("plugin:local-stt|is_server_running"); -}, async isModelDownloaded(model: WhisperModel) : Promise { return await TAURI_INVOKE("plugin:local-stt|is_model_downloaded", { model }); }, @@ -34,11 +31,17 @@ async getCurrentModel() : Promise { async setCurrentModel(model: WhisperModel) : Promise { return await TAURI_INVOKE("plugin:local-stt|set_current_model", { model }); }, +async getServers() : Promise> { + return await TAURI_INVOKE("plugin:local-stt|get_servers"); +}, async startServer(serverType: ServerType | null) : Promise { return await TAURI_INVOKE("plugin:local-stt|start_server", { serverType }); }, async stopServer(serverType: ServerType | null) : Promise { return await TAURI_INVOKE("plugin:local-stt|stop_server", { serverType }); +}, +async listProModels() : Promise { + return await TAURI_INVOKE("plugin:local-stt|list_pro_models"); } } @@ -58,6 +61,7 @@ recordedProcessingEvent: "plugin:local-stt:recorded-processing-event" /** user-defined types **/ export type GgmlBackend = { kind: string; name: string; description: string; total_memory_mb: number; free_memory_mb: number } +export type ModelInfo = { key: string; name: string; size_bytes: number } export type RecordedProcessingEvent = { type: "progress"; current: number; total: number; word: Word2 } export type ServerType = "internal" | "external" export type SpeakerIdentity = { type: "unassigned"; value: { index: number } } | { type: "assigned"; value: { id: string; label: string } } diff --git a/plugins/local-stt/permissions/autogenerated/commands/get_servers.toml b/plugins/local-stt/permissions/autogenerated/commands/get_servers.toml new file mode 100644 index 0000000000..7fff7a2657 --- /dev/null +++ b/plugins/local-stt/permissions/autogenerated/commands/get_servers.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-servers" +description = "Enables the get_servers command without any pre-configured scope." +commands.allow = ["get_servers"] + +[[permission]] +identifier = "deny-get-servers" +description = "Denies the get_servers command without any pre-configured scope." +commands.deny = ["get_servers"] diff --git a/plugins/local-stt/permissions/autogenerated/commands/list_pro_models.toml b/plugins/local-stt/permissions/autogenerated/commands/list_pro_models.toml new file mode 100644 index 0000000000..b72027de54 --- /dev/null +++ b/plugins/local-stt/permissions/autogenerated/commands/list_pro_models.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-list-pro-models" +description = "Enables the list_pro_models command without any pre-configured scope." +commands.allow = ["list_pro_models"] + +[[permission]] +identifier = "deny-list-pro-models" +description = "Denies the list_pro_models command without any pre-configured scope." +commands.deny = ["list_pro_models"] diff --git a/plugins/local-stt/permissions/autogenerated/reference.md b/plugins/local-stt/permissions/autogenerated/reference.md index 53ad292ddb..5216876091 100644 --- a/plugins/local-stt/permissions/autogenerated/reference.md +++ b/plugins/local-stt/permissions/autogenerated/reference.md @@ -10,9 +10,11 @@ Default permissions for the plugin - `allow-download-model` - `allow-start-server` - `allow-stop-server` +- `allow-get-servers` - `allow-get-current-model` - `allow-set-current-model` - `allow-list-supported-models` +- `allow-list-pro-models` ## Permission Table @@ -78,6 +80,32 @@ Denies the get_current_model command without any pre-configured scope. +`local-stt:allow-get-servers` + + + + +Enables the get_servers command without any pre-configured scope. + + + + + + + +`local-stt:deny-get-servers` + + + + +Denies the get_servers command without any pre-configured scope. + + + + + + + `local-stt:allow-get-status` @@ -208,6 +236,32 @@ Denies the list_ggml_backends command without any pre-configured scope. +`local-stt:allow-list-pro-models` + + + + +Enables the list_pro_models command without any pre-configured scope. + + + + + + + +`local-stt:deny-list-pro-models` + + + + +Denies the list_pro_models command without any pre-configured scope. + + + + + + + `local-stt:allow-list-supported-models` diff --git a/plugins/local-stt/permissions/default.toml b/plugins/local-stt/permissions/default.toml index 5ff190f8cb..08f6118c73 100644 --- a/plugins/local-stt/permissions/default.toml +++ b/plugins/local-stt/permissions/default.toml @@ -7,7 +7,9 @@ permissions = [ "allow-download-model", "allow-start-server", "allow-stop-server", + "allow-get-servers", "allow-get-current-model", "allow-set-current-model", "allow-list-supported-models", + "allow-list-pro-models", ] diff --git a/plugins/local-stt/permissions/schemas/schema.json b/plugins/local-stt/permissions/schemas/schema.json index 8f5a702f2c..41f50bc31f 100644 --- a/plugins/local-stt/permissions/schemas/schema.json +++ b/plugins/local-stt/permissions/schemas/schema.json @@ -318,6 +318,18 @@ "const": "deny-get-current-model", "markdownDescription": "Denies the get_current_model command without any pre-configured scope." }, + { + "description": "Enables the get_servers command without any pre-configured scope.", + "type": "string", + "const": "allow-get-servers", + "markdownDescription": "Enables the get_servers command without any pre-configured scope." + }, + { + "description": "Denies the get_servers command without any pre-configured scope.", + "type": "string", + "const": "deny-get-servers", + "markdownDescription": "Denies the get_servers command without any pre-configured scope." + }, { "description": "Enables the get_status command without any pre-configured scope.", "type": "string", @@ -378,6 +390,18 @@ "const": "deny-list-ggml-backends", "markdownDescription": "Denies the list_ggml_backends command without any pre-configured scope." }, + { + "description": "Enables the list_pro_models command without any pre-configured scope.", + "type": "string", + "const": "allow-list-pro-models", + "markdownDescription": "Enables the list_pro_models command without any pre-configured scope." + }, + { + "description": "Denies the list_pro_models command without any pre-configured scope.", + "type": "string", + "const": "deny-list-pro-models", + "markdownDescription": "Denies the list_pro_models command without any pre-configured scope." + }, { "description": "Enables the list_supported_models command without any pre-configured scope.", "type": "string", @@ -451,10 +475,10 @@ "markdownDescription": "Denies the stop_server command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-models-dir`\n- `allow-is-model-downloaded`\n- `allow-is-model-downloading`\n- `allow-download-model`\n- `allow-start-server`\n- `allow-stop-server`\n- `allow-get-current-model`\n- `allow-set-current-model`\n- `allow-list-supported-models`", + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-models-dir`\n- `allow-is-model-downloaded`\n- `allow-is-model-downloading`\n- `allow-download-model`\n- `allow-start-server`\n- `allow-stop-server`\n- `allow-get-servers`\n- `allow-get-current-model`\n- `allow-set-current-model`\n- `allow-list-supported-models`\n- `allow-list-pro-models`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-models-dir`\n- `allow-is-model-downloaded`\n- `allow-is-model-downloading`\n- `allow-download-model`\n- `allow-start-server`\n- `allow-stop-server`\n- `allow-get-current-model`\n- `allow-set-current-model`\n- `allow-list-supported-models`" + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-models-dir`\n- `allow-is-model-downloaded`\n- `allow-is-model-downloading`\n- `allow-download-model`\n- `allow-start-server`\n- `allow-stop-server`\n- `allow-get-servers`\n- `allow-get-current-model`\n- `allow-set-current-model`\n- `allow-list-supported-models`\n- `allow-list-pro-models`" } ] } diff --git a/plugins/local-stt/src/commands.rs b/plugins/local-stt/src/commands.rs index 84acf9953e..6f85910527 100644 --- a/plugins/local-stt/src/commands.rs +++ b/plugins/local-stt/src/commands.rs @@ -1,7 +1,8 @@ -use crate::{server::ServerType, LocalSttPluginExt}; +use std::collections::HashMap; +use tauri::ipc::Channel; +use crate::{server::ServerType, LocalSttPluginExt}; use hypr_whisper_local_model::WhisperModel; -use tauri::ipc::Channel; #[tauri::command] #[specta::specta] @@ -33,6 +34,16 @@ pub async fn list_supported_models() -> Result, String> { Ok(models) } +#[tauri::command] +#[specta::specta] +pub async fn list_pro_models() -> Result, String> { + Ok(vec![ + hypr_am::Model::WhisperSmallEn.info(), + hypr_am::Model::WhisperLargeV3.info(), + hypr_am::Model::ParakeetV2.info(), + ]) +} + #[tauri::command] #[specta::specta] pub async fn is_model_downloaded( @@ -103,3 +114,11 @@ pub async fn stop_server( .await .map_err(|e| e.to_string()) } + +#[tauri::command] +#[specta::specta] +pub async fn get_servers( + app: tauri::AppHandle, +) -> Result>, String> { + app.get_servers().await.map_err(|e| e.to_string()) +} diff --git a/plugins/local-stt/src/error.rs b/plugins/local-stt/src/error.rs index dc61ed83ba..044ce18521 100644 --- a/plugins/local-stt/src/error.rs +++ b/plugins/local-stt/src/error.rs @@ -18,8 +18,12 @@ pub enum Error { StoreError(#[from] tauri_plugin_store2::Error), #[error("Model not downloaded")] ModelNotDownloaded, - #[error("Binary not found")] - BinaryNotFound, + #[error("Server already running")] + ServerAlreadyRunning, + #[error("AM binary not found")] + AmBinaryNotFound, + #[error("AM API key not set")] + AmApiKeyNotSet, } impl Serialize for Error { diff --git a/plugins/local-stt/src/ext.rs b/plugins/local-stt/src/ext.rs index da5c7c2204..9c135eb072 100644 --- a/plugins/local-stt/src/ext.rs +++ b/plugins/local-stt/src/ext.rs @@ -1,4 +1,4 @@ -use std::{future::Future, path::PathBuf}; +use std::{collections::HashMap, future::Future, path::PathBuf}; use tauri::{ipc::Channel, Manager, Runtime}; use tauri_plugin_shell::ShellExt; @@ -26,6 +26,9 @@ pub trait LocalSttPluginExt { &self, server_type: Option, ) -> impl Future>; + fn get_servers( + &self, + ) -> impl Future>, crate::Error>>; fn get_current_model(&self) -> Result; fn set_current_model(&self, model: WhisperModel) -> Result<(), crate::Error>; @@ -112,6 +115,16 @@ impl> LocalSttPluginExt for T { return Err(crate::Error::ModelNotDownloaded); } + if self + .state::() + .lock() + .await + .internal_server + .is_some() + { + return Err(crate::Error::ServerAlreadyRunning); + } + let server_state = internal::ServerState::builder() .model_cache_dir(cache_dir) .model_type(model) @@ -130,6 +143,22 @@ impl> LocalSttPluginExt for T { Ok(api_base) } ServerType::External => { + if self + .state::() + .lock() + .await + .external_server + .is_some() + { + return Err(crate::Error::ServerAlreadyRunning); + } + + let am_key = { + let state = self.state::(); + let key = state.lock().await.am_api_key.clone(); + key.clone().ok_or(crate::Error::AmApiKeyNotSet)? + }; + let cmd: tauri_plugin_shell::process::Command = { #[cfg(debug_assertions)] { @@ -139,7 +168,7 @@ impl> LocalSttPluginExt for T { .join("../../internal/stt-aarch64-apple-darwin"); if !passthrough_path.exists() || !stt_path.exists() { - return Err(crate::Error::BinaryNotFound); + return Err(crate::Error::AmBinaryNotFound); } self.shell().command(passthrough_path).arg(stt_path).args([ @@ -157,6 +186,19 @@ impl> LocalSttPluginExt for T { let api_base = server.api_base.clone(); tokio::time::sleep(std::time::Duration::from_millis(100)).await; + let client = hypr_am::Client::new(&api_base); + let status = client.status().await?; + println!("Status: {status:?}"); + + let init_result = client + .init( + hypr_am::InitRequest::new(am_key) + .with_model(hypr_am::Model::WhisperSmallEn.model_key()) + .with_model_repo(hypr_am::Model::WhisperSmallEn.repo_name()), + ) + .await?; + println!("Init result: {init_result:?}"); + { let state = self.state::(); let mut s = state.lock().await; @@ -177,23 +219,23 @@ impl> LocalSttPluginExt for T { match server_type { Some(ServerType::External) => { if let Some(server) = s.external_server.take() { - let _ = server.shutdown.send(()); + let _ = server.terminate(); stopped = true; } } Some(ServerType::Internal) => { if let Some(server) = s.internal_server.take() { - let _ = server.shutdown.send(()); + let _ = server.terminate(); stopped = true; } } None => { if let Some(server) = s.external_server.take() { - let _ = server.shutdown.send(()); + let _ = server.terminate(); stopped = true; } if let Some(server) = s.internal_server.take() { - let _ = server.shutdown.send(()); + let _ = server.terminate(); stopped = true; } } @@ -202,6 +244,25 @@ impl> LocalSttPluginExt for T { Ok(stopped) } + #[tracing::instrument(skip_all)] + async fn get_servers(&self) -> Result>, crate::Error> { + let state = self.state::(); + let guard = state.lock().await; + + Ok([ + ( + ServerType::Internal, + guard.internal_server.as_ref().map(|s| s.api_base.clone()), + ), + ( + ServerType::External, + guard.external_server.as_ref().map(|s| s.api_base.clone()), + ), + ] + .into_iter() + .collect()) + } + #[tracing::instrument(skip_all)] async fn download_model( &self, diff --git a/plugins/local-stt/src/lib.rs b/plugins/local-stt/src/lib.rs index 219e5714af..d05230af18 100644 --- a/plugins/local-stt/src/lib.rs +++ b/plugins/local-stt/src/lib.rs @@ -36,8 +36,10 @@ fn make_specta_builder() -> tauri_specta::Builder { commands::list_supported_models, commands::get_current_model::, commands::set_current_model::, + commands::get_servers::, commands::start_server::, commands::stop_server::, + commands::list_pro_models, ]) .typ::() .events(tauri_specta::collect_events![ diff --git a/plugins/local-stt/src/server/external.rs b/plugins/local-stt/src/server/external.rs index cbe1649532..4725d87992 100644 --- a/plugins/local-stt/src/server/external.rs +++ b/plugins/local-stt/src/server/external.rs @@ -1,21 +1,28 @@ -#[derive(Clone)] pub struct ServerHandle { pub api_base: String, pub shutdown: tokio::sync::watch::Sender<()>, - client: Option, + child: tauri_plugin_shell::process::CommandChild, +} + +impl ServerHandle { + pub fn terminate(self) -> Result<(), crate::Error> { + let _ = self.shutdown.send(()); + self.child.kill().map_err(|e| crate::Error::ShellError(e))?; + Ok(()) + } } pub async fn run_server( cmd: tauri_plugin_shell::process::Command, ) -> Result { - let (_rx, _child) = cmd.args(["--port", "6942"]).spawn()?; + let (_rx, child) = cmd.args(["--port", "6942"]).spawn()?; let api_base = "http://localhost:6942"; - let client = hypr_am::AmClient::new(api_base); + let (shutdown_tx, _shutdown_rx) = tokio::sync::watch::channel(()); Ok(ServerHandle { api_base: api_base.to_string(), - client: Some(client), - shutdown: tokio::sync::watch::channel(()).0, + shutdown: shutdown_tx, + child, }) } diff --git a/plugins/local-stt/src/server/internal.rs b/plugins/local-stt/src/server/internal.rs index 4bdedb544a..abf72219fd 100644 --- a/plugins/local-stt/src/server/internal.rs +++ b/plugins/local-stt/src/server/internal.rs @@ -48,7 +48,14 @@ impl ServerState { #[derive(Clone)] pub struct ServerHandle { pub api_base: String, - pub shutdown: tokio::sync::watch::Sender<()>, + shutdown: tokio::sync::watch::Sender<()>, +} + +impl ServerHandle { + pub fn terminate(self) -> Result<(), crate::Error> { + let _ = self.shutdown.send(()); + Ok(()) + } } pub async fn run_server(state: ServerState) -> Result { diff --git a/plugins/local-stt/src/server/mod.rs b/plugins/local-stt/src/server/mod.rs index e620d13001..8116c20226 100644 --- a/plugins/local-stt/src/server/mod.rs +++ b/plugins/local-stt/src/server/mod.rs @@ -1,7 +1,9 @@ pub mod external; pub mod internal; -#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type, +)] pub enum ServerType { #[serde(rename = "internal")] Internal, diff --git a/plugins/windows/src/ext.rs b/plugins/windows/src/ext.rs index 08da7b25a1..2ea1d5460e 100644 --- a/plugins/windows/src/ext.rs +++ b/plugins/windows/src/ext.rs @@ -312,8 +312,8 @@ impl HyprWindow { .build()?, Self::Settings => self .window_builder(app, "/app/settings") + .resizable(false) .inner_size(800.0, 600.0) - .min_inner_size(800.0, 600.0) .build()?, Self::Video(id) => self .window_builder(app, &format!("/video?id={}", id))