parakeet v2 support (#116)
* parakeet v2 support * Update model.rs * fix parakeet size * new onboarding UI for parakeet v2 introduction * tweak height
This commit is contained in:
parent
ae406aebe2
commit
65c1e2dbcb
8 changed files with 130 additions and 59 deletions
|
|
@ -31,6 +31,8 @@ pub struct ModelInfo {
|
|||
pub partial_size: u64,
|
||||
pub is_directory: bool,
|
||||
pub engine_type: EngineType,
|
||||
pub accuracy_score: f32, // 0.0 to 1.0, higher is more accurate
|
||||
pub speed_score: f32, // 0.0 to 1.0, higher is faster
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -62,6 +64,7 @@ impl ModelManager {
|
|||
|
||||
let mut available_models = HashMap::new();
|
||||
|
||||
// TODO this should be read from a JSON file or something..
|
||||
available_models.insert(
|
||||
"small".to_string(),
|
||||
ModelInfo {
|
||||
|
|
@ -76,6 +79,8 @@ impl ModelManager {
|
|||
partial_size: 0,
|
||||
is_directory: false,
|
||||
engine_type: EngineType::Whisper,
|
||||
accuracy_score: 0.60,
|
||||
speed_score: 0.85,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -94,6 +99,8 @@ impl ModelManager {
|
|||
partial_size: 0,
|
||||
is_directory: false,
|
||||
engine_type: EngineType::Whisper,
|
||||
accuracy_score: 0.75,
|
||||
speed_score: 0.60,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -111,6 +118,8 @@ impl ModelManager {
|
|||
partial_size: 0,
|
||||
is_directory: false,
|
||||
engine_type: EngineType::Whisper,
|
||||
accuracy_score: 0.80,
|
||||
speed_score: 0.40,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -128,10 +137,31 @@ impl ModelManager {
|
|||
partial_size: 0,
|
||||
is_directory: false,
|
||||
engine_type: EngineType::Whisper,
|
||||
accuracy_score: 0.85,
|
||||
speed_score: 0.30,
|
||||
},
|
||||
);
|
||||
|
||||
// Add NVIDIA Parakeet models (directory-based)
|
||||
available_models.insert(
|
||||
"parakeet-tdt-0.6b-v2".to_string(),
|
||||
ModelInfo {
|
||||
id: "parakeet-tdt-0.6b-v2".to_string(),
|
||||
name: "Parakeet V2".to_string(),
|
||||
description: "English only. The best model for English speakers.".to_string(),
|
||||
filename: "parakeet-tdt-0.6b-v2-int8".to_string(), // Directory name
|
||||
url: Some("https://blob.handy.computer/parakeet-v2-int8.tar.gz".to_string()),
|
||||
size_mb: 473, // Approximate size for int8 quantized model
|
||||
is_downloaded: false,
|
||||
is_downloading: false,
|
||||
partial_size: 0,
|
||||
is_directory: true,
|
||||
engine_type: EngineType::Parakeet,
|
||||
accuracy_score: 0.85,
|
||||
speed_score: 0.85,
|
||||
},
|
||||
);
|
||||
|
||||
// Add NVIDIA Parakeet model (directory-based)
|
||||
available_models.insert(
|
||||
"parakeet-tdt-0.6b-v3".to_string(),
|
||||
ModelInfo {
|
||||
|
|
@ -140,12 +170,14 @@ impl ModelManager {
|
|||
description: "Fast and accurate".to_string(),
|
||||
filename: "parakeet-tdt-0.6b-v3-int8".to_string(), // Directory name
|
||||
url: Some("https://blob.handy.computer/parakeet-v3-int8.tar.gz".to_string()),
|
||||
size_mb: 850, // Approximate size for int8 quantized model
|
||||
size_mb: 478, // Approximate size for int8 quantized model
|
||||
is_downloaded: false,
|
||||
is_downloading: false,
|
||||
partial_size: 0,
|
||||
is_directory: true,
|
||||
engine_type: EngineType::Parakeet,
|
||||
accuracy_score: 0.80,
|
||||
speed_score: 0.85,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
"label": "main",
|
||||
"title": "Handy",
|
||||
"width": 680,
|
||||
"height": 520,
|
||||
"height": 570,
|
||||
"minWidth": 680,
|
||||
"minHeight": 520,
|
||||
"minHeight": 570,
|
||||
"resizable": true,
|
||||
"maximizable": false,
|
||||
"visible": false
|
||||
|
|
|
|||
10
src/App.tsx
10
src/App.tsx
|
|
@ -4,7 +4,6 @@ import { Toaster } from "sonner";
|
|||
import "./App.css";
|
||||
import AccessibilityPermissions from "./components/AccessibilityPermissions";
|
||||
import Footer from "./components/footer";
|
||||
import HandyTextLogo from "./components/icons/HandyTextLogo";
|
||||
import Onboarding from "./components/onboarding";
|
||||
import { Sidebar, SidebarSection, SECTIONS_CONFIG } from "./components/Sidebar";
|
||||
import { useSettings } from "./hooks/useSettings";
|
||||
|
|
@ -67,14 +66,7 @@ function App() {
|
|||
};
|
||||
|
||||
if (showOnboarding) {
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-4 gap-2">
|
||||
<HandyTextLogo width={200} />
|
||||
<Onboarding onModelSelected={handleModelSelected} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <Onboarding onModelSelected={handleModelSelected} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import React from "react";
|
|||
import { Download } from "lucide-react";
|
||||
import { ModelInfo } from "../../lib/types";
|
||||
import { formatModelSize } from "../../lib/utils/format";
|
||||
import Badge from "../ui/Badge";
|
||||
|
||||
interface ModelCardProps {
|
||||
model: ModelInfo;
|
||||
variant?: "default" | "featured";
|
||||
disabled?: boolean;
|
||||
badgeText?: string;
|
||||
className?: string;
|
||||
onSelect: (modelId: string) => void;
|
||||
}
|
||||
|
|
@ -16,31 +16,18 @@ const ModelCard: React.FC<ModelCardProps> = ({
|
|||
model,
|
||||
variant = "default",
|
||||
disabled = false,
|
||||
badgeText,
|
||||
className = "",
|
||||
onSelect,
|
||||
}) => {
|
||||
const isFeatured = variant === "featured";
|
||||
|
||||
const baseButtonClasses =
|
||||
"relative rounded-xl p-3 p-4 text-left transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-logo-primary/25 active:scale-[0.98] cursor-pointer group";
|
||||
"flex justify-between items-center rounded-xl p-3 px-4 text-left transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-logo-primary/25 active:scale-[0.98] cursor-pointer group";
|
||||
|
||||
const variantClasses = isFeatured
|
||||
? "border-2 border-logo-primary/40 bg-logo-primary/5 hover:border-logo-primary/60 hover:bg-logo-primary/10 hover:shadow-lg hover:scale-[1.02] disabled:hover:border-logo-primary/40 disabled:hover:bg-logo-primary/5 disabled:hover:shadow-none disabled:hover:scale-100"
|
||||
? "border-2 border-logo-primary/25 bg-logo-primary/5 hover:border-logo-primary/40 hover:bg-logo-primary/8 hover:shadow-lg hover:scale-[1.02] disabled:hover:border-logo-primary/25 disabled:hover:bg-logo-primary/5 disabled:hover:shadow-none disabled:hover:scale-100"
|
||||
: "border-2 border-mid-gray/20 hover:border-logo-primary/50 hover:bg-logo-primary/5 hover:shadow-lg hover:scale-[1.02] disabled:hover:border-mid-gray/20 disabled:hover:bg-transparent disabled:hover:shadow-none disabled:hover:scale-100";
|
||||
|
||||
const titleClasses =
|
||||
"text-lg font-semibold text-text group-hover:text-logo-primary transition-colors";
|
||||
|
||||
const descriptionClasses = "text-text/60 text-sm leading-relaxed";
|
||||
|
||||
const sizeRowClasses =
|
||||
"mt-1 flex items-center gap-2 text-xs text-text/60 tabular-nums";
|
||||
|
||||
const containerSpacing = "space-y-3";
|
||||
|
||||
const sizeIconClasses = "h-3.5 w-3.5 text-text/45";
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onSelect(model.id)}
|
||||
|
|
@ -50,26 +37,36 @@ const ModelCard: React.FC<ModelCardProps> = ({
|
|||
.join(" ")}
|
||||
type="button"
|
||||
>
|
||||
{badgeText && (
|
||||
<div className="absolute -top-2 -right-2 bg-logo-primary text-white text-sm px-4 py-2 rounded-full font-medium shadow-md">
|
||||
{badgeText}
|
||||
<div className="flex flex-col items-ce">
|
||||
<div className="flex items-center gap-4">
|
||||
<h3 className="text-lg font-semibold text-text group-hover:text-logo-primary transition-colors">
|
||||
{model.name}
|
||||
</h3>
|
||||
<DownloadSize sizeMb={model.size_mb} />
|
||||
{isFeatured && <Badge variant="primary">Recommended</Badge>}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-text/60 text-sm leading-relaxed">
|
||||
{model.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={containerSpacing}>
|
||||
<div className="space-y-0">
|
||||
<h3 className={titleClasses}>{model.name}</h3>
|
||||
<p className={descriptionClasses}>{model.description}</p>
|
||||
<div className={sizeRowClasses}>
|
||||
<Download
|
||||
aria-hidden="true"
|
||||
className={sizeIconClasses}
|
||||
strokeWidth={1.75}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs text-text/70 w-16 text-right">accuracy</p>
|
||||
<div className="w-20 h-2 bg-mid-gray/20 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-logo-primary rounded-full transition-all duration-300"
|
||||
style={{ width: `${model.accuracy_score * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs text-text/70 w-16 text-right">speed</p>
|
||||
<div className="w-20 h-2 bg-mid-gray/20 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-logo-primary rounded-full transition-all duration-300"
|
||||
style={{ width: `${model.speed_score * 100}%` }}
|
||||
/>
|
||||
<span className="sr-only">Download size</span>
|
||||
<span className="font-medium text-text/70">
|
||||
{formatModelSize(model.size_mb)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -77,4 +74,20 @@ const ModelCard: React.FC<ModelCardProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const DownloadSize = ({ sizeMb }: { sizeMb: number }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-xs text-text/60 tabular-nums">
|
||||
<Download
|
||||
aria-hidden="true"
|
||||
className="h-3.5 w-3.5 text-text/45"
|
||||
strokeWidth={1.75}
|
||||
/>
|
||||
<span className="sr-only">Download size</span>
|
||||
<span className="font-medium text-text/70">
|
||||
{formatModelSize(sizeMb)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelCard;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { ModelInfo } from "../../lib/types";
|
||||
import ModelCard from "./ModelCard";
|
||||
import HandyTextLogo from "../icons/HandyTextLogo";
|
||||
|
||||
interface OnboardingProps {
|
||||
onModelSelected: () => void;
|
||||
|
|
@ -48,19 +49,23 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto text-center space-y-6">
|
||||
<p className="text-text/70 max-w-md font-medium mx-auto">
|
||||
To get started, choose a transcription model
|
||||
</p>
|
||||
<div className="h-screen w-screen flex flex-col p-6 gap-4 inset-0">
|
||||
<div className="flex flex-col items-center gap-2 shrink-0">
|
||||
<HandyTextLogo width={200} />
|
||||
<p className="text-text/70 max-w-md font-medium mx-auto">
|
||||
To get started, choose a transcription model
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="max-w-[600px] w-full mx-auto text-center flex-1 flex flex-col min-h-0">
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 mb-4 shrink-0">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-2xl mx-auto mt-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{/*<div className="flex flex-col gap-4 bg-background-dark p-4 py-5 w-full rounded-2xl flex-1 overflow-y-auto min-h-0">*/}
|
||||
<div className="flex flex-col gap-4 ">
|
||||
{availableModels
|
||||
.filter((model) => getRecommendedBadge(model.id))
|
||||
.map((model) => (
|
||||
|
|
@ -68,9 +73,7 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
|
|||
key={model.id}
|
||||
model={model}
|
||||
variant="featured"
|
||||
badgeText="Recommended"
|
||||
disabled={downloading}
|
||||
className="sm:col-span-2"
|
||||
onSelect={handleDownloadModel}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
27
src/components/ui/Badge.tsx
Normal file
27
src/components/ui/Badge.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from "react";
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode;
|
||||
variant?: "primary";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Badge: React.FC<BadgeProps> = ({
|
||||
children,
|
||||
variant = "primary",
|
||||
className = "",
|
||||
}) => {
|
||||
const variantClasses = {
|
||||
primary: "bg-logo-primary",
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${variantClasses[variant]} ${className}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Badge;
|
||||
|
|
@ -13,6 +13,8 @@ interface ModelInfo {
|
|||
is_downloading: boolean;
|
||||
partial_size: number;
|
||||
is_directory: boolean;
|
||||
accuracy_score: number;
|
||||
speed_score: number;
|
||||
}
|
||||
|
||||
interface DownloadProgress {
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ export const ModelInfoSchema = z.object({
|
|||
is_downloading: z.boolean(),
|
||||
partial_size: z.number(),
|
||||
is_directory: z.boolean(),
|
||||
accuracy_score: z.number(),
|
||||
speed_score: z.number(),
|
||||
});
|
||||
|
||||
export type ModelInfo = z.infer<typeof ModelInfoSchema>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue