440 lines
16 KiB
Rust
440 lines
16 KiB
Rust
// --- 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<String, String> {
|
|
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<String, String> {
|
|
(0..encrypted.len())
|
|
.step_by(2)
|
|
.map(|i| u8::from_str_radix(&encrypted[i..i + 2], 16))
|
|
.collect::<Result<Vec<u8>, _>>()
|
|
.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<String, String> {
|
|
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<String>) -> Result<transcription::TranscriptionResult, String> {
|
|
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<String, String> {
|
|
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<String, String> {
|
|
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<String, String> {
|
|
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<video_editor::VideoInfo, String> {
|
|
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<String, String> {
|
|
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<bool, String> {
|
|
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<String>, num_speakers: Option<u32>, use_gpu: Option<bool>) -> Result<diarization::TranscriptionResult, String> {
|
|
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<String>, api_key: Option<String>, base_url: Option<String>, system_prompt: Option<String>, temperature: Option<f64>) -> Result<String, String> {
|
|
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<String>) -> Result<Vec<String>, 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<caption_generator::Word>, deleted_indices: Option<std::collections::HashSet<usize>>, words_per_line: Option<usize>) -> Result<String, String> {
|
|
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<caption_generator::Word>, deleted_indices: Option<std::collections::HashSet<usize>>, words_per_line: Option<usize>) -> Result<String, String> {
|
|
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<caption_generator::Word>, deleted_indices: Option<std::collections::HashSet<usize>>, words_per_line: Option<usize>, style: Option<caption_generator::CaptionStyle>) -> Result<String, String> {
|
|
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<String, String> {
|
|
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<Vec<models::ModelInfo>, 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<licensing::AppStatus, String> {
|
|
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::LicensePayload, String> {
|
|
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<licensing::LicensePayload, String> {
|
|
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<licensing::TrialState, String> {
|
|
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<bool, String> {
|
|
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<bool, String> {
|
|
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<String, String> {
|
|
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<Option<String>, 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");
|
|
}
|