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]
|
||||
pub async fn get_recommended_first_model() -> Result<String, String> {
|
||||
// Recommend small model for first-time users
|
||||
Ok("small".to_string())
|
||||
// Recommend Parakeet V3 model for first-time users - fastest and most accurate
|
||||
Ok("parakeet-tdt-0.6b-v3".to_string())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
use crate::settings::{get_settings, write_settings};
|
||||
use anyhow::Result;
|
||||
use flate2::read::GzDecoder;
|
||||
use futures_util::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{App, AppHandle, Emitter, Manager};
|
||||
use tar::Archive;
|
||||
use std::fs::File;
|
||||
use flate2::read::GzDecoder;
|
||||
use tauri::{App, AppHandle, Emitter, Manager};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum EngineType {
|
||||
|
|
@ -69,7 +69,7 @@ impl ModelManager {
|
|||
ModelInfo {
|
||||
id: "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(),
|
||||
url: Some("https://blob.handy.computer/ggml-small.bin".to_string()),
|
||||
size_mb: 244,
|
||||
|
|
@ -121,7 +121,7 @@ impl ModelManager {
|
|||
ModelInfo {
|
||||
id: "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(),
|
||||
url: Some("https://blob.handy.computer/ggml-large-v3-q5_0.bin".to_string()),
|
||||
size_mb: 1080, // Approximate size
|
||||
|
|
@ -139,7 +139,7 @@ impl ModelManager {
|
|||
ModelInfo {
|
||||
id: "parakeet-tdt-0.6b-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
|
||||
url: Some("https://blob.handy.computer/parakeet-v3-int8.tar.gz".to_string()),
|
||||
size_mb: 850, // Approximate size for int8 quantized model
|
||||
|
|
@ -214,7 +214,9 @@ impl ModelManager {
|
|||
// For directory-based models, check if the directory exists
|
||||
let model_path = self.models_dir.join(&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
|
||||
if extracting_path.exists() {
|
||||
|
|
@ -427,14 +429,16 @@ impl ModelManager {
|
|||
println!("Extracting archive for directory-based model: {}", model_id);
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
// Clean up any previous incomplete extraction
|
||||
if temp_extract_dir.exists() {
|
||||
let _ = fs::remove_dir_all(&temp_extract_dir);
|
||||
}
|
||||
|
||||
|
||||
// Create temporary extraction directory
|
||||
fs::create_dir_all(&temp_extract_dir)?;
|
||||
|
||||
|
|
@ -448,19 +452,22 @@ impl ModelManager {
|
|||
let error_msg = format!("Failed to extract archive: {}", e);
|
||||
// Clean up failed extraction
|
||||
let _ = fs::remove_dir_all(&temp_extract_dir);
|
||||
let _ = self.app_handle.emit("model-extraction-failed", &serde_json::json!({
|
||||
"model_id": model_id,
|
||||
"error": error_msg
|
||||
}));
|
||||
let _ = self.app_handle.emit(
|
||||
"model-extraction-failed",
|
||||
&serde_json::json!({
|
||||
"model_id": model_id,
|
||||
"error": error_msg
|
||||
}),
|
||||
);
|
||||
anyhow::anyhow!(error_msg)
|
||||
})?;
|
||||
|
||||
|
||||
// Find the actual extracted directory (archive might have a nested structure)
|
||||
let extracted_dirs: Vec<_> = fs::read_dir(&temp_extract_dir)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
|
||||
.collect();
|
||||
|
||||
|
||||
if extracted_dirs.len() == 1 {
|
||||
// Single directory extracted, move it to the final location
|
||||
let source_dir = extracted_dirs[0].path();
|
||||
|
|
@ -481,7 +488,7 @@ impl ModelManager {
|
|||
println!("Successfully extracted archive for model: {}", model_id);
|
||||
// Emit extraction completed event
|
||||
let _ = self.app_handle.emit("model-extraction-completed", model_id);
|
||||
|
||||
|
||||
// Remove the downloaded tar.gz file
|
||||
let _ = fs::remove_file(&partial_path);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -208,11 +208,6 @@ impl TranscriptionManager {
|
|||
|
||||
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
|
||||
let loaded_engine = match model_info.engine_type {
|
||||
EngineType::Whisper => {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ function App() {
|
|||
<HandyTextLogo width={200} />
|
||||
<Onboarding onModelSelected={handleModelSelected} />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import React from "react";
|
||||
|
||||
type ModelStatus = "ready" | "loading" | "downloading" | "error" | "none";
|
||||
type ModelStatus =
|
||||
| "ready"
|
||||
| "loading"
|
||||
| "downloading"
|
||||
| "extracting"
|
||||
| "error"
|
||||
| "none";
|
||||
|
||||
interface ModelStatusButtonProps {
|
||||
status: ModelStatus;
|
||||
|
|
@ -25,6 +31,8 @@ const ModelStatusButton: React.FC<ModelStatusButtonProps> = ({
|
|||
return "bg-yellow-400 animate-pulse";
|
||||
case "downloading":
|
||||
return "bg-logo-primary animate-pulse";
|
||||
case "extracting":
|
||||
return "bg-orange-400 animate-pulse";
|
||||
case "error":
|
||||
return "bg-red-400";
|
||||
case "none":
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
|
|||
};
|
||||
|
||||
const getRecommendedBadge = (modelId: string): boolean => {
|
||||
return modelId === "small";
|
||||
return modelId === "parakeet-tdt-0.6b-v3";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto text-center space-y-8">
|
||||
<p className="text-text/70 max-w-md mx-auto">
|
||||
To get started, choose a transcription model to download
|
||||
<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>
|
||||
|
||||
{error && (
|
||||
|
|
@ -58,34 +58,59 @@ const Onboarding: React.FC<OnboardingProps> = ({ onModelSelected }) => {
|
|||
</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
|
||||
.sort((a, b) => a.size_mb - b.size_mb)
|
||||
.filter((model) => getRecommendedBadge(model.id))
|
||||
.map((model) => (
|
||||
<button
|
||||
key={model.id}
|
||||
onClick={() => handleDownloadModel(model.id)}
|
||||
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-xs px-3 py-1 rounded-full font-medium shadow-md">
|
||||
Recommended
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute -top-2 -right-2 bg-logo-primary text-white text-sm px-4 py-2 rounded-full font-medium shadow-md">
|
||||
Recommended
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-0">
|
||||
<h3 className="text-xl sm:text-2xl font-bold text-text group-hover:text-logo-primary transition-colors">
|
||||
{model.name}
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue