// --- Commands --- use tauri::Manager; mod paths; mod transcription; mod video_editor; mod audio_cleaner; mod diarization; mod ai_provider; mod caption_generator; mod background_removal; mod licensing; mod models; #[tauri::command] fn get_projects_directory() -> Result { let dir = paths::project_root().join("Projects"); std::fs::create_dir_all(&dir) .map_err(|e| format!("Failed to create Projects directory: {e}"))?; Ok(dir.to_string_lossy().to_string()) } /// Returns the backend URL. #[tauri::command] fn get_backend_url() -> String { "http://127.0.0.1:8000".to_string() } /// Minimal encrypt: base64-encodes the string as a placeholder until a proper /// OS keychain implementation is added (e.g. tauri-plugin-stronghold). #[tauri::command] fn encrypt_string(data: String) -> String { data .as_bytes() .iter() .fold(String::new(), |mut acc, b| { use std::fmt::Write as FmtWrite; let _ = write!(acc, "{:02x}", b); acc }) } /// Companion decode for encrypt_string. #[tauri::command] fn decrypt_string(encrypted: String) -> Result { (0..encrypted.len()) .step_by(2) .map(|i| u8::from_str_radix(&encrypted[i..i + 2], 16)) .collect::, _>>() .map_err(|e| format!("decrypt error: {e}")) .and_then(|b| String::from_utf8(b).map_err(|e| format!("utf8 error: {e}"))) } /// Ensure a Whisper model is downloaded, downloading it if not present. #[tauri::command] async fn ensure_model(model_name: String) -> Result { tauri::async_runtime::spawn_blocking(move || { transcription::ensure_model_downloaded(&model_name) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Transcribe audio file using Whisper.cpp (runs on a background thread) #[tauri::command] async fn transcribe_audio(file_path: String, model_name: String, language: Option) -> Result { tauri::async_runtime::spawn_blocking(move || { transcription::transcribe_audio(&file_path, &model_name, language.as_deref()) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Export video using stream copy (fast, lossless) #[tauri::command] async fn export_stream_copy(input_path: String, output_path: String, keep_segments: serde_json::Value) -> Result { tauri::async_runtime::spawn_blocking(move || { video_editor::export_stream_copy(&input_path, &output_path, &keep_segments) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Export video with re-encoding #[tauri::command] async fn export_reencode(input_path: String, output_path: String, keep_segments: serde_json::Value, resolution: String, format_hint: String) -> Result { tauri::async_runtime::spawn_blocking(move || { video_editor::export_reencode(&input_path, &output_path, &keep_segments, &resolution, &format_hint) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Export video with re-encoding and subtitles #[tauri::command] async fn export_reencode_with_subs(input_path: String, output_path: String, keep_segments: serde_json::Value, subtitle_path: String, resolution: String, format_hint: String) -> Result { tauri::async_runtime::spawn_blocking(move || { video_editor::export_reencode_with_subs(&input_path, &output_path, &keep_segments, &subtitle_path, &resolution, &format_hint) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Get video information #[tauri::command] async fn get_video_info(input_path: String) -> Result { tauri::async_runtime::spawn_blocking(move || { video_editor::get_video_info(&input_path) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Clean audio using DeepFilterNet or FFmpeg fallback #[tauri::command] async fn clean_audio(input_path: String, output_path: String) -> Result { tauri::async_runtime::spawn_blocking(move || { audio_cleaner::clean_audio(&input_path, &output_path) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Check if DeepFilterNet is available #[tauri::command] async fn is_deepfilter_available() -> Result { tauri::async_runtime::spawn_blocking(move || { audio_cleaner::is_deepfilter_available() }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Apply speaker diarization to transcription result #[tauri::command] async fn diarize_and_label(transcription_result: diarization::TranscriptionResult, audio_path: String, hf_token: Option, num_speakers: Option, use_gpu: Option) -> Result { let use_gpu = use_gpu.unwrap_or(true); tauri::async_runtime::spawn_blocking(move || { diarization::diarize_and_label(&transcription_result, &audio_path, hf_token.as_deref(), num_speakers, use_gpu) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Complete text using AI provider #[tauri::command] async fn ai_complete(prompt: String, provider: String, model: Option, api_key: Option, base_url: Option, system_prompt: Option, temperature: Option) -> Result { let temperature = temperature.unwrap_or(0.3); tauri::async_runtime::spawn_blocking(move || { ai_provider::complete(&prompt, &provider, model.as_deref(), api_key.as_deref(), base_url.as_deref(), system_prompt.as_deref(), temperature) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// List available Ollama models #[tauri::command] async fn list_ollama_models(base_url: Option) -> Result, String> { let base_url = base_url.unwrap_or_else(|| "http://localhost:11434".to_string()); tauri::async_runtime::spawn_blocking(move || { ai_provider::list_ollama_models(&base_url) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Generate SRT caption content #[tauri::command] async fn generate_srt(words: Vec, deleted_indices: Option>, words_per_line: Option) -> Result { let words_per_line = words_per_line.unwrap_or(8); tauri::async_runtime::spawn_blocking(move || { caption_generator::generate_srt(&words, deleted_indices.as_ref(), words_per_line) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Generate VTT caption content #[tauri::command] async fn generate_vtt(words: Vec, deleted_indices: Option>, words_per_line: Option) -> Result { let words_per_line = words_per_line.unwrap_or(8); tauri::async_runtime::spawn_blocking(move || { caption_generator::generate_vtt(&words, deleted_indices.as_ref(), words_per_line) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Generate ASS subtitle content #[tauri::command] async fn generate_ass(words: Vec, deleted_indices: Option>, words_per_line: Option, style: Option) -> Result { let words_per_line = words_per_line.unwrap_or(8); tauri::async_runtime::spawn_blocking(move || { caption_generator::generate_ass(&words, deleted_indices.as_ref(), words_per_line, style.as_ref()) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Save caption content to file #[tauri::command] async fn save_captions(content: String, output_path: String) -> Result { tauri::async_runtime::spawn_blocking(move || { caption_generator::save_captions(&content, &output_path) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// List downloaded models (Whisper + LLM) with sizes. #[tauri::command] fn log_error(message: String, stack: String, component_stack: String) { log::error!( "[Frontend Error] {} — Stack: {} — Component: {}", message, stack, component_stack, ); } /// List downloaded models (Whisper + LLM) with sizes. #[tauri::command] fn list_models(app_handle: tauri::AppHandle) -> Result, String> { let data_dir = app_handle .path() .app_data_dir() .map_err(|e| format!("No app data directory: {e}"))?; Ok(models::list_models(&data_dir)) } /// Delete a downloaded model by path. #[tauri::command] fn delete_model(path: String) -> Result<(), String> { models::delete_model(&path) } /// Get the combined app status: licensed, trial, or expired. #[tauri::command] fn get_app_status(app_handle: tauri::AppHandle) -> Result { let data_dir = app_handle .path() .app_data_dir() .map_err(|e| format!("No app data directory: {e}"))?; Ok(licensing::get_app_status(&data_dir)) } /// Verify a license key signature without caching. Returns the payload. #[tauri::command] fn verify_license(license_key: String) -> Result { licensing::verify_license_key(&license_key) .map_err(|e| e.to_string()) } /// Verify and activate a license key. #[tauri::command] fn activate_license(app_handle: tauri::AppHandle, license_key: String) -> Result { let data_dir = app_handle .path() .app_data_dir() .map_err(|e| format!("No app data directory: {e}"))?; let payload = licensing::verify_license_key(&license_key) .map_err(|e| e.to_string())?; licensing::cache_license(&data_dir, &license_key) .map_err(|e| e.to_string())?; // Clear trial state since user has a valid license licensing::clear_trial(&data_dir); Ok(payload) } /// Remove the cached license (deactivate). Trial will resume if still valid. #[tauri::command] fn deactivate_license(app_handle: tauri::AppHandle) -> Result<(), String> { let data_dir = app_handle .path() .app_data_dir() .map_err(|e| format!("No app data directory: {e}"))?; licensing::remove_license(&data_dir); Ok(()) } /// Start the free trial if not already started. Returns trial info. #[tauri::command] fn start_trial(app_handle: tauri::AppHandle) -> Result { let data_dir = app_handle .path() .app_data_dir() .map_err(|e| format!("No app data directory: {e}"))?; let trial = licensing::get_or_start_trial(&data_dir); Ok(trial) } /// Check if a specific feature is enabled (requires valid license). #[tauri::command] fn has_license_feature(app_handle: tauri::AppHandle, feature: String) -> Result { let data_dir = app_handle .path() .app_data_dir() .map_err(|e| format!("No app data directory: {e}"))?; match licensing::load_cached_license(&data_dir) { Ok(payload) => Ok(licensing::has_feature(&payload, &feature)), Err(_) => Ok(false), } } /// Check if background removal is available #[tauri::command] async fn is_background_removal_available() -> Result { tauri::async_runtime::spawn_blocking(move || { background_removal::is_available() }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Remove background on export (placeholder for Phase 5) #[tauri::command] async fn remove_background_on_export(input_path: String, output_path: String, replacement: String, replacement_value: String) -> Result { tauri::async_runtime::spawn_blocking(move || { background_removal::remove_background_on_export(&input_path, &output_path, &replacement, &replacement_value) }) .await .map_err(|e| format!("Task error: {:?}", e))? } /// Write autosave data to the app data directory #[tauri::command] fn write_autosave(app_handle: tauri::AppHandle, data: String) -> Result<(), String> { let data_dir = app_handle.path().app_data_dir().map_err(|e| format!("No app data directory: {e}"))?; let path = data_dir.join("autosave.json"); std::fs::write(&path, data).map_err(|e| format!("Failed to write autosave: {e}"))?; Ok(()) } /// Read autosave data if it exists #[tauri::command] fn read_autosave(app_handle: tauri::AppHandle) -> Result, String> { let data_dir = app_handle.path().app_data_dir().map_err(|e| format!("No app data directory: {e}"))?; let path = data_dir.join("autosave.json"); match std::fs::read_to_string(&path) { Ok(data) => Ok(Some(data)), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(e) => Err(format!("Failed to read autosave: {e}")), } } /// Delete the autosave file #[tauri::command] fn delete_autosave(app_handle: tauri::AppHandle) -> Result<(), String> { let data_dir = app_handle.path().app_data_dir().map_err(|e| format!("No app data directory: {e}"))?; let path = data_dir.join("autosave.json"); match std::fs::remove_file(&path) { Ok(()) => Ok(()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(e) => Err(format!("Failed to delete autosave: {e}")), } } // --- App entry point --- #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .setup(|app| { if cfg!(debug_assertions) { app.handle().plugin( tauri_plugin_log::Builder::default() .level(log::LevelFilter::Info) .build(), )?; } // Check for cached license or trial at startup if let Ok(data_dir) = app.path().app_data_dir() { match licensing::get_app_status(&data_dir) { licensing::AppStatus::Licensed { license: payload } => { log::info!( "License: {} ({}), expires {}", payload.customer_email, payload.tier, payload.expires_at, ); } licensing::AppStatus::Trial { days_remaining, .. } => { log::info!("Trial active: {days_remaining} days remaining"); } licensing::AppStatus::Expired => { log::info!("Trial expired — license activation required"); } } } Ok(()) }) .invoke_handler(tauri::generate_handler![ get_projects_directory, get_backend_url, encrypt_string, decrypt_string, ensure_model, transcribe_audio, export_stream_copy, export_reencode, export_reencode_with_subs, get_video_info, clean_audio, is_deepfilter_available, diarize_and_label, ai_complete, list_ollama_models, generate_srt, generate_vtt, generate_ass, save_captions, is_background_removal_available, remove_background_on_export, get_app_status, activate_license, deactivate_license, verify_license, start_trial, has_license_feature, list_models, delete_model, log_error, write_autosave, read_autosave, delete_autosave, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }