Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 89 additions & 106 deletions apps/desktop/src/settings/ai/stt/configure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useCallback } from "react";
import {
commands as localSttCommands,
type LocalModel,
type SttModelInfo,
} from "@hypr/plugin-local-stt";
import { commands as openerCommands } from "@hypr/plugin-opener2";
import {
Expand Down Expand Up @@ -99,14 +100,7 @@ function HyprProviderCard({
icon: React.ReactNode;
badge?: string | null;
}) {
const supportedModels = useQuery({
queryKey: ["list-supported-models"],
queryFn: async () => {
const result = await localSttCommands.listSupportedModels();
return result.status === "ok" ? result.data : [];
},
staleTime: Infinity,
});
const supportedModels = useQuery(localSttQueries.supportedModels());

const argmaxModels =
supportedModels.data?.filter((m) => m.model_type === "argmax") ?? [];
Expand Down Expand Up @@ -167,45 +161,24 @@ function HyprProviderCard({
</div>

{argmaxModels.length > 0 && (
<>
<ModelGroupLabel label="Argmax" />
{argmaxModels.map((model) => (
<HyprProviderLocalRow
key={model.key as string}
model={model.key}
displayName={model.display_name}
description={model.description}
/>
))}
</>
<LocalModelSection label="Argmax" models={argmaxModels} />
)}

{whispercppModels.length > 0 && (
<>
<ModelGroupLabel label="WhisperCPP" />
{whispercppModels.map((model) => (
<HyprProviderLocalRow
key={model.key as string}
model={model.key}
displayName={model.display_name}
description={model.description}
/>
))}
</>
<LocalModelSection
label="WhisperCPP"
models={whispercppModels}
/>
)}

{cactusModels.length > 0 && (
<>
<ModelGroupLabel label="Cactus (Experimental)" />
<LocalModelSection
label="Cactus"
models={cactusModels}
modelsDir="cactus"
/>
{/* <CactusSettings models={cactusModels.map((m) => m.key)} /> */}

{cactusModels.map((model) => (
<CactusRow
key={model.key as string}
model={model.key}
displayName={model.display_name}
/>
))}
</>
)}
</>
Expand All @@ -216,52 +189,30 @@ function HyprProviderCard({
);
}

function CactusRow({
model,
displayName,
function LocalModelSection({
label,
models,
modelsDir = "default",
}: {
model: LocalModel;
displayName: string;
label: string;
models: SttModelInfo[];
modelsDir?: "default" | "cactus";
}) {
const handleSelectModel = useSafeSelectModel();
const { shouldHighlightDownload } = useSttSettings();

const {
progress,
hasError,
isDownloaded,
showProgress,
handleDownload,
handleCancel,
handleDelete,
} = useLocalModelDownload(model, handleSelectModel);

const handleOpen = () => {
void localSttCommands.cactusModelsDir().then((result) => {
if (result.status === "ok") {
void openerCommands.openPath(result.data, null);
}
});
};

return (
<HyprProviderRow>
<div className="flex-1">
<span className="text-sm font-medium">{displayName}</span>
<div className="flex flex-col gap-2">
<ModelGroupLabel label={label} />
<div className="flex flex-wrap gap-2">
{models.map((model) => (
<LocalModelTile
key={model.key as string}
model={model.key}
displayName={model.display_name}
description={model.description}
modelsDir={modelsDir}
/>
))}
</div>

<LocalModelAction
isDownloaded={isDownloaded}
showProgress={showProgress}
progress={progress}
hasError={hasError}
highlight={shouldHighlightDownload}
onOpen={handleOpen}
onDownload={handleDownload}
onCancel={handleCancel}
onDelete={handleDelete}
/>
</HyprProviderRow>
</div>
);
}

Expand Down Expand Up @@ -386,6 +337,7 @@ function LocalModelAction({
onDownload,
onCancel,
onDelete,
compact = false,
}: {
isDownloaded: boolean;
showProgress: boolean;
Expand All @@ -396,6 +348,7 @@ function LocalModelAction({
onDownload: () => void;
onCancel: () => void;
onDelete: () => void;
compact?: boolean;
}) {
const showShimmer = highlight && !isDownloaded && !showProgress && !hasError;

Expand All @@ -404,19 +357,24 @@ function LocalModelAction({
<div className="flex items-center gap-1.5">
<button
onClick={onOpen}
aria-label="Show in Finder"
title="Show in Finder"
className={cn([
"h-8.5 rounded-full px-4 text-center font-mono text-xs",
compact
? "size-8.5 rounded-full"
: "h-8.5 rounded-full px-4 text-center font-mono text-xs",
"bg-linear-to-t from-neutral-200 to-neutral-100 text-neutral-900",
"shadow-xs hover:shadow-md",
"transition-all duration-150",
"flex items-center justify-center gap-1.5",
])}
>
<FolderOpen className="size-4" />
<span>Show in Finder</span>
{!compact && <span>Show in Finder</span>}
</button>
<button
onClick={onDelete}
aria-label="Delete model"
title="Delete Model"
className={cn([
"size-8.5 rounded-full",
Expand All @@ -437,7 +395,9 @@ function LocalModelAction({
<button
onClick={onDownload}
className={cn([
"h-8.5 w-fit rounded-full px-4 text-center font-mono text-xs",
compact
? "h-8.5 rounded-full px-3 text-center font-mono text-xs"
: "h-8.5 w-fit rounded-full px-4 text-center font-mono text-xs",
"bg-linear-to-t from-red-600 to-red-500 text-white",
"shadow-md hover:scale-[102%] hover:shadow-lg active:scale-[98%]",
"transition-all duration-150",
Expand All @@ -456,7 +416,9 @@ function LocalModelAction({
onClick={onCancel}
className={cn([
"group relative overflow-hidden",
"h-8.5 w-27.5 rounded-full px-4 text-center font-mono text-xs",
compact
? "h-8.5 w-24 rounded-full px-3 text-center font-mono text-xs"
: "h-8.5 w-27.5 rounded-full px-4 text-center font-mono text-xs",
"bg-linear-to-t from-neutral-300 to-neutral-200 text-neutral-900",
"shadow-xs",
"transition-all duration-150",
Expand All @@ -482,8 +444,9 @@ function LocalModelAction({
<button
onClick={onDownload}
className={cn([
"relative h-8.5 w-fit overflow-hidden",
"rounded-full px-4 text-center font-mono text-xs",
compact
? "relative h-8.5 overflow-hidden rounded-full px-3 text-center font-mono text-xs"
: "relative h-8.5 w-fit overflow-hidden rounded-full px-4 text-center font-mono text-xs",
"bg-linear-to-t from-neutral-200 to-neutral-100 text-neutral-900",
"shadow-xs hover:scale-[102%] hover:shadow-md active:scale-[98%]",
"transition-all duration-150",
Expand All @@ -505,14 +468,16 @@ function LocalModelAction({
);
}

function HyprProviderLocalRow({
function LocalModelTile({
model,
displayName,
description,
modelsDir = "default",
}: {
model: LocalModel;
displayName: string;
description: string;
modelsDir?: "default" | "cactus";
}) {
const handleSelectModel = useSafeSelectModel();
const { shouldHighlightDownload } = useSttSettings();
Expand All @@ -528,32 +493,50 @@ function HyprProviderLocalRow({
} = useLocalModelDownload(model, handleSelectModel);

const handleOpen = () => {
void localSttCommands.modelsDir().then((result) => {
const request =
modelsDir === "cactus"
? localSttCommands.cactusModelsDir()
: localSttCommands.modelsDir();

void request.then((result) => {
if (result.status === "ok") {
void openerCommands.openPath(result.data, null);
}
});
};

return (
<HyprProviderRow>
<div className="flex-1">
<span className="text-sm font-medium">{displayName}</span>
<p className="text-xs text-neutral-500">{description}</p>
<div
className={cn([
"flex min-w-64 grow basis-[19rem] flex-col gap-3",
"rounded-md border bg-white px-3 py-2.5",
])}
>
<div className="min-w-0">
<p className="text-sm leading-5 font-medium text-neutral-900">
{displayName}
</p>
{!!description && (
<p className="mt-1 text-xs leading-4 text-neutral-500">
{description}
</p>
)}
</div>

<LocalModelAction
isDownloaded={isDownloaded}
showProgress={showProgress}
progress={progress}
hasError={hasError}
highlight={shouldHighlightDownload}
onOpen={handleOpen}
onDownload={handleDownload}
onCancel={handleCancel}
onDelete={handleDelete}
/>
</HyprProviderRow>
<div className="mt-auto flex justify-end">
<LocalModelAction
compact
isDownloaded={isDownloaded}
showProgress={showProgress}
progress={progress}
hasError={hasError}
highlight={shouldHighlightDownload}
onOpen={handleOpen}
onDownload={handleDownload}
onCancel={handleCancel}
onDelete={handleDelete}
/>
</div>
</div>
);
}

Expand Down
43 changes: 37 additions & 6 deletions crates/cactus-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,43 @@ impl CactusSttModel {

pub fn description(&self) -> &str {
match self {
CactusSttModel::WhisperSmallInt8Apple
| CactusSttModel::WhisperMediumInt4Apple
| CactusSttModel::WhisperMediumInt8Apple
| CactusSttModel::ParakeetCtc0_6bInt4Apple
| CactusSttModel::ParakeetCtc0_6bInt8Apple => "Apple Neural Engine",
_ => "",
CactusSttModel::WhisperSmallInt4 => {
"Smaller multilingual Whisper model. Lowest storage use."
}
CactusSttModel::WhisperSmallInt8 => {
"Smaller multilingual Whisper model. Good balance of speed and accuracy."
}
CactusSttModel::WhisperSmallInt8Apple => {
"Smaller multilingual Whisper model for Apple Silicon. Uses the Neural Engine."
}
CactusSttModel::WhisperMediumInt4 => {
"More accurate multilingual Whisper model with a smaller footprint."
}
CactusSttModel::WhisperMediumInt4Apple => {
"More accurate multilingual Whisper model for Apple Silicon. Uses the Neural Engine."
}
CactusSttModel::WhisperMediumInt8 => {
"Most accurate Whisper option here. Larger download."
}
CactusSttModel::WhisperMediumInt8Apple => {
"Most accurate Whisper option for Apple Silicon. Uses the Neural Engine."
}
CactusSttModel::ParakeetCtc0_6bInt4 => {
"English only. Smaller download and lower memory use."
}
CactusSttModel::ParakeetCtc0_6bInt4Apple => {
"English only for Apple Silicon. Smaller download and uses the Neural Engine."
}
CactusSttModel::ParakeetCtc0_6bInt8 => {
"English only. Better accuracy than INT4 with a larger download."
}
CactusSttModel::ParakeetCtc0_6bInt8Apple => {
"English only for Apple Silicon. Better accuracy than INT4 and uses the Neural Engine."
}
CactusSttModel::ParakeetTdt0_6bV3Int4 => "English only. Fastest download and startup.",
CactusSttModel::ParakeetTdt0_6bV3Int8 => {
"English only. Better accuracy than INT4 with a larger download."
}
}
}

Expand Down
Loading