tweak onboarding (#112)
This commit is contained in:
parent
442438b142
commit
338d15d2c4
6 changed files with 76 additions and 42 deletions
|
|
@ -120,6 +120,6 @@ pub async fn cancel_download(
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_recommended_first_model() -> Result<String, String> {
|
pub async fn get_recommended_first_model() -> Result<String, String> {
|
||||||
// Recommend small model for first-time users
|
// Recommend Parakeet V3 model for first-time users - fastest and most accurate
|
||||||
Ok("small".to_string())
|
Ok("parakeet-tdt-0.6b-v3".to_string())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
use crate::settings::{get_settings, write_settings};
|
use crate::settings::{get_settings, write_settings};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tauri::{App, AppHandle, Emitter, Manager};
|
|
||||||
use tar::Archive;
|
use tar::Archive;
|
||||||
use std::fs::File;
|
use tauri::{App, AppHandle, Emitter, Manager};
|
||||||
use flate2::read::GzDecoder;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum EngineType {
|
pub enum EngineType {
|
||||||
|
|
@ -69,7 +69,7 @@ impl ModelManager {
|
||||||
ModelInfo {
|
ModelInfo {
|
||||||
id: "small".to_string(),
|
id: "small".to_string(),
|
||||||
name: "Whisper Small".to_string(),
|
name: "Whisper Small".to_string(),
|
||||||
description: "Fast and efficient, great for most use cases".to_string(),
|
description: "Fast and fairly accurate.".to_string(),
|
||||||
filename: "ggml-small.bin".to_string(),
|
filename: "ggml-small.bin".to_string(),
|
||||||
url: Some("https://blob.handy.computer/ggml-small.bin".to_string()),
|
url: Some("https://blob.handy.computer/ggml-small.bin".to_string()),
|
||||||
size_mb: 244,
|
size_mb: 244,
|
||||||
|
|
@ -121,7 +121,7 @@ impl ModelManager {
|
||||||
ModelInfo {
|
ModelInfo {
|
||||||
id: "large".to_string(),
|
id: "large".to_string(),
|
||||||
name: "Whisper Large".to_string(),
|
name: "Whisper Large".to_string(),
|
||||||
description: "Highest accuracy, but slow.".to_string(),
|
description: "Good accuracy, but slow.".to_string(),
|
||||||
filename: "ggml-large-v3-q5_0.bin".to_string(),
|
filename: "ggml-large-v3-q5_0.bin".to_string(),
|
||||||
url: Some("https://blob.handy.computer/ggml-large-v3-q5_0.bin".to_string()),
|
url: Some("https://blob.handy.computer/ggml-large-v3-q5_0.bin".to_string()),
|
||||||
size_mb: 1080, // Approximate size
|
size_mb: 1080, // Approximate size
|
||||||
|
|
@ -139,7 +139,7 @@ impl ModelManager {
|
||||||
ModelInfo {
|
ModelInfo {
|
||||||
id: "parakeet-tdt-0.6b-v3".to_string(),
|
id: "parakeet-tdt-0.6b-v3".to_string(),
|
||||||
name: "Parakeet V3".to_string(),
|
name: "Parakeet V3".to_string(),
|
||||||
description: "The fastest and most accurate model".to_string(),
|
description: "Fast and accurate".to_string(),
|
||||||
filename: "parakeet-tdt-0.6b-v3-int8".to_string(), // Directory name
|
filename: "parakeet-tdt-0.6b-v3-int8".to_string(), // Directory name
|
||||||
url: Some("https://blob.handy.computer/parakeet-v3-int8.tar.gz".to_string()),
|
url: Some("https://blob.handy.computer/parakeet-v3-int8.tar.gz".to_string()),
|
||||||
size_mb: 850, // Approximate size for int8 quantized model
|
size_mb: 850, // Approximate size for int8 quantized model
|
||||||
|
|
@ -214,7 +214,9 @@ impl ModelManager {
|
||||||
// For directory-based models, check if the directory exists
|
// For directory-based models, check if the directory exists
|
||||||
let model_path = self.models_dir.join(&model.filename);
|
let model_path = self.models_dir.join(&model.filename);
|
||||||
let partial_path = self.models_dir.join(format!("{}.partial", &model.filename));
|
let partial_path = self.models_dir.join(format!("{}.partial", &model.filename));
|
||||||
let extracting_path = self.models_dir.join(format!("{}.extracting", &model.filename));
|
let extracting_path = self
|
||||||
|
.models_dir
|
||||||
|
.join(format!("{}.extracting", &model.filename));
|
||||||
|
|
||||||
// Clean up any leftover .extracting directories from interrupted extractions
|
// Clean up any leftover .extracting directories from interrupted extractions
|
||||||
if extracting_path.exists() {
|
if extracting_path.exists() {
|
||||||
|
|
@ -427,7 +429,9 @@ impl ModelManager {
|
||||||
println!("Extracting archive for directory-based model: {}", model_id);
|
println!("Extracting archive for directory-based model: {}", model_id);
|
||||||
|
|
||||||
// Use a temporary extraction directory to ensure atomic operations
|
// Use a temporary extraction directory to ensure atomic operations
|
||||||
let temp_extract_dir = self.models_dir.join(format!("{}.extracting", &model_info.filename));
|
let temp_extract_dir = self
|
||||||
|
.models_dir
|
||||||
|
.join(format!("{}.extracting", &model_info.filename));
|
||||||
let final_model_dir = self.models_dir.join(&model_info.filename);
|
let final_model_dir = self.models_dir.join(&model_info.filename);
|
||||||
|
|
||||||
// Clean up any previous incomplete extraction
|
// Clean up any previous incomplete extraction
|
||||||
|
|
@ -448,10 +452,13 @@ impl ModelManager {
|
||||||
let error_msg = format!("Failed to extract archive: {}", e);
|
let error_msg = format!("Failed to extract archive: {}", e);
|
||||||
// Clean up failed extraction
|
// Clean up failed extraction
|
||||||
let _ = fs::remove_dir_all(&temp_extract_dir);
|
let _ = fs::remove_dir_all(&temp_extract_dir);
|
||||||
let _ = self.app_handle.emit("model-extraction-failed", &serde_json::json!({
|
let _ = self.app_handle.emit(
|
||||||
"model_id": model_id,
|
"model-extraction-failed",
|
||||||
"error": error_msg
|
&serde_json::json!({
|
||||||
}));
|
"model_id": model_id,
|
||||||
|
"error": error_msg
|
||||||
|
}),
|
||||||
|
);
|
||||||
anyhow::anyhow!(error_msg)
|
anyhow::anyhow!(error_msg)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -208,11 +208,6 @@ impl TranscriptionManager {
|
||||||
|
|
||||||
let model_path = self.model_manager.get_model_path(model_id)?;
|
let model_path = self.model_manager.get_model_path(model_id)?;
|
||||||
|
|
||||||
println!(
|
|
||||||
"Loading transcription model {} from: {:?}",
|
|
||||||
model_id, model_path
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create appropriate engine based on model type
|
// Create appropriate engine based on model type
|
||||||
let loaded_engine = match model_info.engine_type {
|
let loaded_engine = match model_info.engine_type {
|
||||||
EngineType::Whisper => {
|
EngineType::Whisper => {
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,6 @@ function App() {
|
||||||
<HandyTextLogo width={200} />
|
<HandyTextLogo width={200} />
|
||||||
<Onboarding onModelSelected={handleModelSelected} />
|
<Onboarding onModelSelected={handleModelSelected} />
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
type ModelStatus = "ready" | "loading" | "downloading" | "error" | "none";
|
type ModelStatus =
|
||||||
|
| "ready"
|
||||||
|
| "loading"
|
||||||
|
| "downloading"
|
||||||
|
| "extracting"
|
||||||
|
| "error"
|
||||||
|
| "none";
|
||||||
|
|
||||||
interface ModelStatusButtonProps {
|
interface ModelStatusButtonProps {
|
||||||
status: ModelStatus;
|
status: ModelStatus;
|
||||||
|
|
@ -25,6 +31,8 @@ const ModelStatusButton: React.FC<ModelStatusButtonProps> = ({
|
||||||
return "bg-yellow-400 animate-pulse";
|
return "bg-yellow-400 animate-pulse";
|
||||||
case "downloading":
|
case "downloading":
|
||||||
return "bg-logo-primary animate-pulse";
|
return "bg-logo-primary animate-pulse";
|
||||||
|
case "extracting":
|
||||||
|
return "bg-orange-400 animate-pulse";
|
||||||
case "error":
|
case "error":
|
||||||
return "bg-red-400";
|
return "bg-red-400";
|
||||||
case "none":
|
case "none":
|
||||||
|
|
|
||||||
|
|
@ -43,13 +43,13 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRecommendedBadge = (modelId: string): boolean => {
|
const getRecommendedBadge = (modelId: string): boolean => {
|
||||||
return modelId === "small";
|
return modelId === "parakeet-tdt-0.6b-v3";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto text-center space-y-8">
|
<div className="max-w-4xl mx-auto text-center space-y-6">
|
||||||
<p className="text-text/70 max-w-md mx-auto">
|
<p className="text-text/70 max-w-md font-medium mx-auto">
|
||||||
To get started, choose a transcription model to download
|
To get started, choose a transcription model
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -58,34 +58,59 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 sm:gap-6 max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto space-y-4">
|
||||||
|
{/* Recommended model - full width */}
|
||||||
{availableModels
|
{availableModels
|
||||||
.sort((a, b) => a.size_mb - b.size_mb)
|
.filter((model) => getRecommendedBadge(model.id))
|
||||||
.map((model) => (
|
.map((model) => (
|
||||||
<button
|
<button
|
||||||
key={model.id}
|
key={model.id}
|
||||||
onClick={() => handleDownloadModel(model.id)}
|
onClick={() => handleDownloadModel(model.id)}
|
||||||
disabled={downloading}
|
disabled={downloading}
|
||||||
className="relative border-2 border-mid-gray/20 rounded-xl p-4 sm:p-6 text-left hover:border-logo-primary/50 hover:bg-logo-primary/5 hover:shadow-lg hover:scale-[1.02] focus:border-logo-primary focus:outline-none focus:ring-2 focus:ring-logo-primary/25 active:scale-[0.98] transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-mid-gray/20 disabled:hover:bg-transparent disabled:hover:shadow-none disabled:hover:scale-100 cursor-pointer group"
|
className="relative w-full border-2 border-logo-primary/40 bg-logo-primary/5 rounded-xl p-4 text-left hover:border-logo-primary/60 hover:bg-logo-primary/10 hover:shadow-lg hover:scale-[1.02] focus:border-logo-primary focus:outline-none focus:ring-2 focus:ring-logo-primary/25 active:scale-[0.98] transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-logo-primary/40 disabled:hover:bg-logo-primary/5 disabled:hover:shadow-none disabled:hover:scale-100 cursor-pointer group"
|
||||||
>
|
>
|
||||||
{getRecommendedBadge(model.id) && (
|
<div className="absolute -top-2 -right-2 bg-logo-primary text-white text-sm px-4 py-2 rounded-full font-medium shadow-md">
|
||||||
<div className="absolute -top-2 -right-2 bg-logo-primary text-white text-xs px-3 py-1 rounded-full font-medium shadow-md">
|
Recommended
|
||||||
Recommended
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-0">
|
||||||
<h3 className="text-lg sm:text-xl font-semibold text-text group-hover:text-logo-primary transition-colors">
|
<h3 className="text-xl sm:text-2xl font-bold text-text group-hover:text-logo-primary transition-colors">
|
||||||
{model.name}
|
{model.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-text/60 text-xs sm:text-sm leading-relaxed">
|
<p className="text-text/70 text-sm sm:text-base leading-relaxed">
|
||||||
{model.description}
|
{model.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Other models - 2 column grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{availableModels
|
||||||
|
.filter((model) => !getRecommendedBadge(model.id))
|
||||||
|
.sort((a, b) => a.size_mb - b.size_mb)
|
||||||
|
.map((model) => (
|
||||||
|
<button
|
||||||
|
key={model.id}
|
||||||
|
onClick={() => handleDownloadModel(model.id)}
|
||||||
|
disabled={downloading}
|
||||||
|
className="relative border-2 border-mid-gray/20 rounded-xl sm:p-4 text-left hover:border-logo-primary/50 hover:bg-logo-primary/5 hover:shadow-lg hover:scale-[1.02] focus:border-logo-primary focus:outline-none focus:ring-2 focus:ring-logo-primary/25 active:scale-[0.98] transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-mid-gray/20 disabled:hover:bg-transparent disabled:hover:shadow-none disabled:hover:scale-100 cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg sm:text-xl font-semibold text-text group-hover:text-logo-primary transition-colors">
|
||||||
|
{model.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-text/60 text-xs sm:text-sm leading-relaxed">
|
||||||
|
{model.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue