From 55e300cc0712fc8015c3c378ed9ddd10c3c8ae60 Mon Sep 17 00:00:00 2001 From: adaster98 Date: Thu, 23 Jul 2026 15:36:46 +0000 Subject: [PATCH] Add folder thumbnail generation and AI re-tagging --- src-tauri/src/app_state.rs | 2 + src-tauri/src/file_management.rs | 197 +++++++++++++++++++++++++--- src-tauri/src/lib.rs | 9 ++ src-tauri/src/tagging.rs | 98 +++++++++----- src/components/ui/AppProperties.tsx | 1 + src/hooks/useAppContextMenus.ts | 31 +++++ src/i18n/locales/de.json | 6 +- src/i18n/locales/en.json | 6 +- src/i18n/locales/es.json | 6 +- src/i18n/locales/fr.json | 6 +- src/i18n/locales/it.json | 6 +- src/i18n/locales/ja.json | 6 +- src/i18n/locales/ko.json | 6 +- src/i18n/locales/pl.json | 6 +- src/i18n/locales/pt.json | 6 +- src/i18n/locales/ru.json | 6 +- src/i18n/locales/zh-CN.json | 6 +- src/i18n/locales/zh-TW.json | 6 +- 18 files changed, 348 insertions(+), 62 deletions(-) diff --git a/src-tauri/src/app_state.rs b/src-tauri/src/app_state.rs index 83ebfddf0d..79a84ebb2e 100644 --- a/src-tauri/src/app_state.rs +++ b/src-tauri/src/app_state.rs @@ -156,6 +156,8 @@ pub struct AppState { pub initial_file_path: Mutex>, pub pending_edit_session: Mutex>, pub thumbnail_cancellation_token: Arc, + pub bulk_thumbnail_active: AtomicBool, + pub bulk_thumbnail_generation: AtomicUsize, pub thumbnail_progress: Mutex, pub preview_worker_tx: Mutex>>, pub analytics_worker_tx: Mutex>>, diff --git a/src-tauri/src/file_management.rs b/src-tauri/src/file_management.rs index 56fc585659..4684a62459 100644 --- a/src-tauri/src/file_management.rs +++ b/src-tauri/src/file_management.rs @@ -8,7 +8,7 @@ use std::hash::{Hash, Hasher}; use std::io::Cursor; use std::path::{Path, PathBuf}; use std::process::Command; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::thread; use anyhow::Result; @@ -253,7 +253,7 @@ impl fmt::Display for ReadFileError { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageFile { - path: String, + pub path: String, modified: u64, is_edited: bool, rating: u8, @@ -1645,6 +1645,145 @@ pub fn start_thumbnail_workers(app_handle: tauri::AppHandle) { } } +#[tauri::command] +pub async fn generate_folder_thumbnails( + folder_path: String, + app_handle: tauri::AppHandle, +) -> Result { + let folder = PathBuf::from(&folder_path); + if !folder.is_dir() { + return Err(format!("Folder does not exist: {}", folder_path)); + } + + let image_paths: Vec = list_images_recursive(folder_path.clone(), app_handle.clone())? + .into_iter() + .map(|image| image.path) + .collect(); + + let total = image_paths.len(); + let settings = load_settings(app_handle.clone()).unwrap_or_default(); + let worker_count = settings.thumbnail_worker_threads.unwrap_or(4).clamp(1, 16) as usize; + let cache_dir = get_thumb_cache_dir(&app_handle).map_err(|e| e.to_string())?; + let worker_pool = rayon::ThreadPoolBuilder::new() + .num_threads(worker_count) + .build() + .map_err(|e| format!("Could not start thumbnail workers: {}", e))?; + + let state = app_handle.state::(); + let generation = state + .bulk_thumbnail_generation + .fetch_add(1, std::sync::atomic::Ordering::SeqCst) + + 1; + state + .bulk_thumbnail_active + .store(true, std::sync::atomic::Ordering::SeqCst); + + // The bulk job owns progress while active, so grid visibility cannot change its total. + state.thumbnail_manager.queue.lock().unwrap().clear(); + { + let mut tracker = state.thumbnail_progress.lock().unwrap(); + tracker.total = 0; + tracker.completed = 0; + } + + let _ = app_handle.emit( + "thumbnail-progress", + serde_json::json!({ "current": 0, "total": total }), + ); + + if total == 0 { + state + .bulk_thumbnail_active + .store(false, std::sync::atomic::Ordering::SeqCst); + let _ = app_handle.emit("thumbnail-generation-complete", true); + return Ok(0); + } + + let gpu_context = crate::gpu_processing::get_or_init_gpu_context(&state, &app_handle).ok(); + let completed = Arc::new(Mutex::new(0usize)); + let app_handle_clone = app_handle.clone(); + + tauri::async_runtime::spawn_blocking(move || { + worker_pool.install(|| { + image_paths.par_iter().for_each(|path| { + let current_state = app_handle_clone.state::(); + if current_state + .bulk_thumbnail_generation + .load(std::sync::atomic::Ordering::SeqCst) + != generation + { + return; + } + + let result = generate_single_thumbnail_and_cache( + path, + &cache_dir, + gpu_context.as_ref(), + None, + false, + &app_handle_clone, + &settings, + ); + + if let Some((thumbnail_path, rating, is_edited)) = result { + emit_thumbnail_generated( + &app_handle_clone, + path, + &thumbnail_path, + rating, + is_edited, + ); + } + + if current_state + .bulk_thumbnail_generation + .load(std::sync::atomic::Ordering::SeqCst) + != generation + { + return; + } + + // Serialize increments and emissions so progress cannot arrive out of order. + let mut count = completed.lock().unwrap(); + *count += 1; + let _ = app_handle_clone.emit( + "thumbnail-progress", + serde_json::json!({ "current": *count, "total": total }), + ); + }); + }); + + let current_state = app_handle_clone.state::(); + if current_state + .bulk_thumbnail_generation + .load(std::sync::atomic::Ordering::SeqCst) + == generation + { + current_state + .bulk_thumbnail_active + .store(false, std::sync::atomic::Ordering::SeqCst); + let _ = app_handle_clone.emit("thumbnail-generation-complete", true); + } + }) + .await + .map_err(|e| { + let current_state = app_handle.state::(); + if current_state + .bulk_thumbnail_generation + .load(std::sync::atomic::Ordering::SeqCst) + == generation + { + current_state + .bulk_thumbnail_active + .store(false, std::sync::atomic::Ordering::SeqCst); + let _ = app_handle.emit("thumbnail-generation-complete", true); + } + e.to_string() + })?; + + Ok(total) +} + #[tauri::command] pub fn update_thumbnail_queue( paths: Vec, @@ -1661,10 +1800,15 @@ pub fn update_thumbnail_queue( tracker.completed = 0; drop(tracker); - let _ = app_handle.emit( - "thumbnail-progress", - serde_json::json!({ "current": 0, "total": 0 }), - ); + if !state + .bulk_thumbnail_active + .load(std::sync::atomic::Ordering::SeqCst) + { + let _ = app_handle.emit( + "thumbnail-progress", + serde_json::json!({ "current": 0, "total": 0 }), + ); + } state.thumbnail_manager.cvar.notify_all(); return Ok(()); } @@ -1697,10 +1841,15 @@ pub fn update_thumbnail_queue( let total = tracker.total; drop(tracker); - let _ = app_handle.emit( - "thumbnail-progress", - serde_json::json!({ "current": current, "total": total }), - ); + if !state + .bulk_thumbnail_active + .load(std::sync::atomic::Ordering::SeqCst) + { + let _ = app_handle.emit( + "thumbnail-progress", + serde_json::json!({ "current": current, "total": total }), + ); + } state.thumbnail_manager.cvar.notify_all(); Ok(()) @@ -1730,17 +1879,27 @@ pub fn increment_thumbnail_progress(state: &AppState, app_handle: &AppHandle) { tracker.completed = 0; drop(tracker); - let _ = app_handle.emit( - "thumbnail-progress", - serde_json::json!({ "current": 0, "total": 0 }), - ); - let _ = app_handle.emit("thumbnail-generation-complete", true); + if !state + .bulk_thumbnail_active + .load(std::sync::atomic::Ordering::SeqCst) + { + let _ = app_handle.emit( + "thumbnail-progress", + serde_json::json!({ "current": 0, "total": 0 }), + ); + let _ = app_handle.emit("thumbnail-generation-complete", true); + } } else { drop(tracker); - let _ = app_handle.emit( - "thumbnail-progress", - serde_json::json!({ "current": current, "total": total }), - ); + if !state + .bulk_thumbnail_active + .load(std::sync::atomic::Ordering::SeqCst) + { + let _ = app_handle.emit( + "thumbnail-progress", + serde_json::json!({ "current": current, "total": total }), + ); + } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2a28f3517c..2b7e202e3b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -226,6 +226,12 @@ fn cancel_thumbnail_generation( state .thumbnail_cancellation_token .store(true, Ordering::SeqCst); + state + .bulk_thumbnail_generation + .fetch_add(1, Ordering::SeqCst); + state.bulk_thumbnail_active.store(false, Ordering::SeqCst); + + state.thumbnail_manager.queue.lock().unwrap().clear(); let mut tracker = state.thumbnail_progress.lock().unwrap(); tracker.total = 0; @@ -2280,6 +2286,8 @@ pub fn run() { initial_file_path: Mutex::new(None), pending_edit_session: Mutex::new(None), thumbnail_cancellation_token: Arc::new(AtomicBool::new(false)), + bulk_thumbnail_active: AtomicBool::new(false), + bulk_thumbnail_generation: AtomicUsize::new(0), thumbnail_progress: Mutex::new(ThumbnailProgressTracker { total: 0, completed: 0 }), preview_worker_tx: Mutex::new(None), analytics_worker_tx: Mutex::new(None), @@ -2354,6 +2362,7 @@ pub fn run() { file_management::get_folder_children, file_management::get_pinned_folder_trees, file_management::update_thumbnail_queue, + file_management::generate_folder_thumbnails, file_management::create_folder, file_management::delete_folder, file_management::copy_files, diff --git a/src-tauri/src/tagging.rs b/src-tauri/src/tagging.rs index 0eeab4d317..be45c6e154 100644 --- a/src-tauri/src/tagging.rs +++ b/src-tauri/src/tagging.rs @@ -7,7 +7,7 @@ use ort::value::Tensor; use rayon::prelude::*; use std::collections::{HashMap, HashSet}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Emitter, Manager, State}; use tokenizers::Tokenizer; @@ -15,7 +15,6 @@ use tokio::task::JoinHandle; use walkdir::WalkDir; use crate::file_management::{self, parse_virtual_path}; -use crate::formats::is_supported_image_file; use crate::hierarchy::TAG_HIERARCHY; use crate::image_processing::ImageMetadata; use crate::{AppState, candidates::TAG_CANDIDATES}; @@ -251,23 +250,30 @@ pub fn generate_tags_with_clip( #[tauri::command] pub async fn start_background_indexing( folder_path: String, + force_retag: Option, + recursive: Option, app_handle: AppHandle, state: State<'_, AppState>, ) -> Result<(), String> { - if let Some(handle) = state.indexing_task_handle.lock().unwrap().take() { - println!("Cancelling previous indexing task."); - handle.abort(); + if !Path::new(&folder_path).is_dir() { + return Err(format!("Folder does not exist: {}", folder_path)); } let settings = crate::load_settings(app_handle.clone())?; - if !settings.enable_ai_tagging.unwrap_or(false) { + let force_retag = force_retag.unwrap_or(false); + if !force_retag && !settings.enable_ai_tagging.unwrap_or(false) { return Ok(()); } + let recursive = recursive.unwrap_or(false); let max_concurrent_tasks = settings.tagging_thread_count.unwrap_or(3).max(1) as usize; let custom_ai_tags = settings.custom_ai_tags.clone(); let ai_tag_count = settings.ai_tag_count.unwrap_or(10) as usize; + if force_retag { + println!("Preparing recursive AI re-tagging for: {}", folder_path); + } + let clip_models = crate::ai_processing::get_or_init_clip_models( &app_handle, &state.ai_state, @@ -276,6 +282,11 @@ pub async fn start_background_indexing( .await .map_err(|e| e.to_string())?; + if let Some(handle) = state.indexing_task_handle.lock().unwrap().take() { + println!("Cancelling previous indexing task."); + handle.abort(); + } + let app_handle_clone = app_handle.clone(); let task: JoinHandle<()> = tokio::spawn(async move { @@ -290,24 +301,39 @@ pub async fn start_background_indexing( let gpu_context = crate::gpu_processing::get_or_init_gpu_context(&state_clone, &app_handle).ok(); - let image_paths: Vec = match fs::read_dir(&folder_path) { - Ok(entries) => entries - .filter_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| { - path.is_file() && is_supported_image_file(path.to_string_lossy().as_ref()) - }) - .collect(), - Err(e) => { - eprintln!("Failed to read directory '{}': {}", folder_path, e); - let _ = app_handle_clone - .emit("indexing-error", format!("Failed to read directory: {}", e)); - *app_handle_clone - .state::() - .indexing_task_handle - .lock() - .unwrap() = None; - return; + let image_paths: Vec = if recursive { + match file_management::list_images_recursive( + folder_path.clone(), + app_handle_clone.clone(), + ) { + Ok(images) => images.into_iter().map(|image| image.path).collect(), + Err(e) => { + eprintln!("Failed to read directory '{}': {}", folder_path, e); + let _ = app_handle_clone + .emit("indexing-error", format!("Failed to read directory: {}", e)); + *app_handle_clone + .state::() + .indexing_task_handle + .lock() + .unwrap() = None; + return; + } + } + } else { + match file_management::list_images_in_dir(folder_path.clone(), app_handle_clone.clone()) + { + Ok(images) => images.into_iter().map(|image| image.path).collect(), + Err(e) => { + eprintln!("Failed to read directory '{}': {}", folder_path, e); + let _ = app_handle_clone + .emit("indexing-error", format!("Failed to read directory: {}", e)); + *app_handle_clone + .state::() + .indexing_task_handle + .lock() + .unwrap() = None; + return; + } } }; @@ -329,17 +355,19 @@ pub async fn start_background_indexing( let tags_inner = Arc::clone(&custom_ai_tags_shared); async move { - let path_str = path.to_string_lossy().to_string(); + let path_str = path; let (_, sidecar_path) = parse_virtual_path(&path_str); let mut metadata = crate::exif_processing::load_sidecar(&sidecar_path); - let should_generate_tags = match &metadata.tags { - None => true, - Some(tags) => !tags.iter().any(|tag| { - !tag.starts_with(COLOR_TAG_PREFIX) && !tag.starts_with(USER_TAG_PREFIX) - }), - }; + let should_generate_tags = force_retag + || match &metadata.tags { + None => true, + Some(tags) => !tags.iter().any(|tag| { + !tag.starts_with(COLOR_TAG_PREFIX) + && !tag.starts_with(USER_TAG_PREFIX) + }), + }; if should_generate_tags { match file_management::get_cached_or_generate_thumbnail_image( @@ -360,6 +388,14 @@ pub async fn start_background_indexing( let mut existing_tags: HashSet = metadata.tags.unwrap_or_default().into_iter().collect(); + if force_retag { + // Preserve user and generated color tags while replacing semantic AI tags. + existing_tags.retain(|tag| { + tag.starts_with(COLOR_TAG_PREFIX) + || tag.starts_with(USER_TAG_PREFIX) + }); + } + for tag in ai_tags { existing_tags.insert(tag); } diff --git a/src/components/ui/AppProperties.tsx b/src/components/ui/AppProperties.tsx index 86139f4003..06f71341aa 100644 --- a/src/components/ui/AppProperties.tsx +++ b/src/components/ui/AppProperties.tsx @@ -59,6 +59,7 @@ export enum Invokes { GenerateMaskOverlay = 'generate_mask_overlay', GeneratePresetPreview = 'generate_preset_preview', GenerateThumbnailsProgressive = 'generate_thumbnails_progressive', + GenerateFolderThumbnails = 'generate_folder_thumbnails', GenerateUncroppedPreview = 'generate_uncropped_preview', GetFolderTree = 'get_folder_tree', GetFolderChildren = 'get_folder_children', diff --git a/src/hooks/useAppContextMenus.ts b/src/hooks/useAppContextMenus.ts index 4464fdc6b9..9bb6659fa6 100644 --- a/src/hooks/useAppContextMenus.ts +++ b/src/hooks/useAppContextMenus.ts @@ -957,6 +957,37 @@ export function useAppContextMenus(props: UseAppContextMenusProps) { label: t('contextMenus.folders.refresh'), onClick: () => props.refreshAllFolderTrees(), }, + { type: OPTION_SEPARATOR }, + { + icon: Images, + label: t('contextMenus.folders.generateThumbnails'), + onClick: () => { + invoke(Invokes.GenerateFolderThumbnails, { folderPath: targetPath }).catch((err) => { + console.error('Failed to generate folder thumbnails:', err); + toast.error(t('contextMenus.toasts.failedGenerateThumbnails', { err })); + }); + }, + }, + ...(appSettings?.enableAiTagging + ? [ + { + icon: Tag, + label: t('contextMenus.folders.retagWithAi'), + onClick: () => { + toast.info(t('library.header.search.indexingImages')); + invoke(Invokes.StartBackgroundIndexing, { + folderPath: targetPath, + forceRetag: true, + recursive: true, + }).catch((err) => { + console.error('Failed to start folder AI tagging:', err); + toast.error(t('contextMenus.toasts.failedStartAiTagging', { err })); + }); + }, + }, + ] + : []), + { type: OPTION_SEPARATOR }, { disabled: isRoot, icon: Trash2, diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index a890d28ecc..254c2a7a92 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -201,6 +201,7 @@ "copyHere_one": "Bild hierher kopieren", "copyHere_other": "{{count}} Bilder hierher kopieren", "deleteFolder": "Ordner löschen", + "generateThumbnails": "Alle Miniaturbilder erstellen", "importImages": "Bilder importieren", "moveHere_one": "Bild hierher verschieben", "moveHere_other": "{{count}} Bilder hierher verschieben", @@ -208,6 +209,7 @@ "paste": "Einfügen", "pin": "Ordner anheften", "refresh": "Ordner aktualisieren", + "retagWithAi": "Inhalt mit KI neu taggen", "removeRoot": "Stammordner entfernen", "renameFolder": "Ordner umbenennen", "showExplorer": "Im Explorer anzeigen", @@ -272,10 +274,12 @@ "failedDelete": "Löschen fehlgeschlagen: {{err}}", "failedDeleteFolder": "Ordner löschen fehlgeschlagen: {{err}}", "failedDuplicate": "Datei duplizieren fehlgeschlagen: {{err}}", + "failedGenerateThumbnails": "Miniaturbilder konnten nicht erstellt werden: {{err}}", "failedMove": "Dateien verschieben fehlgeschlagen: {{err}}", "failedMoveError": "Verschieben fehlgeschlagen: {{err}}", "failedMoveInvalid": "Verschieben fehlgeschlagen: Zielgruppe nicht gefunden oder ungültig.", - "failedRemoveImages": "Bilder entfernen fehlgeschlagen: {{err}}" + "failedRemoveImages": "Bilder entfernen fehlgeschlagen: {{err}}", + "failedStartAiTagging": "KI-Tagging konnte nicht gestartet werden: {{err}}" } }, "editor": { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f12483b613..5eac14db86 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -201,6 +201,7 @@ "copyHere_one": "Copy image here", "copyHere_other": "Copy {{count}} images here", "deleteFolder": "Delete Folder", + "generateThumbnails": "Generate All Thumbnails", "importImages": "Import Images", "moveHere_one": "Move image here", "moveHere_other": "Move {{count}} images here", @@ -208,6 +209,7 @@ "paste": "Paste", "pin": "Pin Folder", "refresh": "Refresh Folders", + "retagWithAi": "Re-tag Contents with AI", "removeRoot": "Remove Root Folder", "renameFolder": "Rename Folder", "showExplorer": "Show in File Explorer", @@ -272,10 +274,12 @@ "failedDelete": "Failed to delete: {{err}}", "failedDeleteFolder": "Failed to delete folder: {{err}}", "failedDuplicate": "Failed to duplicate file: {{err}}", + "failedGenerateThumbnails": "Failed to generate thumbnails: {{err}}", "failedMove": "Failed to move files: {{err}}", "failedMoveError": "Failed to move: {{err}}", "failedMoveInvalid": "Failed to move: Target group not found or invalid.", - "failedRemoveImages": "Failed to remove images: {{err}}" + "failedRemoveImages": "Failed to remove images: {{err}}", + "failedStartAiTagging": "Failed to start AI tagging: {{err}}" } }, "editor": { diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 1a980d84a5..7122544939 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -203,6 +203,7 @@ "copyHere_one": "Copiar imagen aquí", "copyHere_other": "Copiar {{count}} imágenes aquí", "deleteFolder": "Eliminar carpeta", + "generateThumbnails": "Generar todas las miniaturas", "importImages": "Importar imágenes", "moveHere_many": "Mover {{count}} imágenes aquí", "moveHere_one": "Mover imagen aquí", @@ -211,6 +212,7 @@ "paste": "Pegar", "pin": "Fijar carpeta", "refresh": "Actualizar carpetas", + "retagWithAi": "Volver a etiquetar con IA", "removeRoot": "Quitar carpeta raíz", "renameFolder": "Renombrar carpeta", "showExplorer": "Mostrar en el Explorador de archivos", @@ -288,10 +290,12 @@ "failedDelete": "Error al eliminar: {{err}}", "failedDeleteFolder": "Error al eliminar la carpeta: {{err}}", "failedDuplicate": "Error al duplicar el archivo: {{err}}", + "failedGenerateThumbnails": "Error al generar miniaturas: {{err}}", "failedMove": "Error al mover los archivos: {{err}}", "failedMoveError": "Error al mover: {{err}}", "failedMoveInvalid": "Error al mover: Grupo de destino no encontrado o inválido.", - "failedRemoveImages": "Error al quitar las imágenes: {{err}}" + "failedRemoveImages": "Error al quitar las imágenes: {{err}}", + "failedStartAiTagging": "Error al iniciar el etiquetado con IA: {{err}}" } }, "editor": { diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 3bc2ecaf2c..59c88bde6e 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -203,6 +203,7 @@ "copyHere_one": "Copier l'image ici", "copyHere_other": "Copier {{count}} images ici", "deleteFolder": "Supprimer le dossier", + "generateThumbnails": "Générer toutes les miniatures", "importImages": "Importer des images", "moveHere_many": "Déplacer {{count}} images ici", "moveHere_one": "Déplacer l'image ici", @@ -211,6 +212,7 @@ "paste": "Coller", "pin": "Épingler le dossier", "refresh": "Actualiser les dossiers", + "retagWithAi": "Réétiqueter le contenu avec l’IA", "removeRoot": "Supprimer le dossier racine", "renameFolder": "Renommer le dossier", "showExplorer": "Afficher dans l'explorateur", @@ -288,10 +290,12 @@ "failedDelete": "Échec de la suppression : {{err}}", "failedDeleteFolder": "Échec de la suppression du dossier : {{err}}", "failedDuplicate": "Échec de la duplication du fichier : {{err}}", + "failedGenerateThumbnails": "Échec de la génération des miniatures : {{err}}", "failedMove": "Échec du déplacement des fichiers : {{err}}", "failedMoveError": "Échec du déplacement : {{err}}", "failedMoveInvalid": "Échec du déplacement : Groupe cible introuvable ou invalide.", - "failedRemoveImages": "Échec de la suppression des images : {{err}}" + "failedRemoveImages": "Échec de la suppression des images : {{err}}", + "failedStartAiTagging": "Échec du démarrage de l’étiquetage IA : {{err}}" } }, "editor": { diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index f4a78a9e1b..ade2815acd 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -203,6 +203,7 @@ "copyHere_one": "Copia immagine qui", "copyHere_other": "Copia {{count}} immagini qui", "deleteFolder": "Elimina Cartella", + "generateThumbnails": "Genera tutte le miniature", "importImages": "Importa Immagini", "moveHere_many": "Sposta {{count}} immagini qui", "moveHere_one": "Sposta immagine qui", @@ -211,6 +212,7 @@ "paste": "Incolla", "pin": "Fissa Cartella", "refresh": "Aggiorna Cartelle", + "retagWithAi": "Rietichetta i contenuti con l’IA", "removeRoot": "Rimuovi Cartella Principale", "renameFolder": "Rinomina Cartella", "showExplorer": "Mostra in Esplora File", @@ -288,10 +290,12 @@ "failedDelete": "Impossibile eliminare: {{err}}", "failedDeleteFolder": "Impossibile eliminare la cartella: {{err}}", "failedDuplicate": "Impossibile duplicare il file: {{err}}", + "failedGenerateThumbnails": "Impossibile generare le miniature: {{err}}", "failedMove": "Impossibile spostare i file: {{err}}", "failedMoveError": "Impossibile spostare: {{err}}", "failedMoveInvalid": "Impossibile spostare: Gruppo di destinazione non trovato o non valido.", - "failedRemoveImages": "Impossibile rimuovere le immagini: {{err}}" + "failedRemoveImages": "Impossibile rimuovere le immagini: {{err}}", + "failedStartAiTagging": "Impossibile avviare il tagging IA: {{err}}" } }, "editor": { diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 29f771171f..ffbf55d0a7 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -201,6 +201,7 @@ "copyHere_one": "画像をここにコピー", "copyHere_other": "{{count}} 枚の画像をここにコピー", "deleteFolder": "フォルダを削除", + "generateThumbnails": "すべてのサムネイルを生成", "importImages": "画像をインポート", "moveHere_one": "画像をここに移動", "moveHere_other": "{{count}} 枚の画像をここに移動", @@ -208,6 +209,7 @@ "paste": "貼り付け", "pin": "フォルダをピン留め", "refresh": "フォルダを更新", + "retagWithAi": "AIタグを再生成", "removeRoot": "ルートフォルダを解除", "renameFolder": "フォルダ名を変更", "showExplorer": "エクスプローラーで表示", @@ -272,10 +274,12 @@ "failedDelete": "削除に失敗しました: {{err}}", "failedDeleteFolder": "フォルダの削除に失敗しました: {{err}}", "failedDuplicate": "ファイルの複製に失敗しました: {{err}}", + "failedGenerateThumbnails": "サムネイルの生成に失敗しました: {{err}}", "failedMove": "ファイルの移動に失敗しました: {{err}}", "failedMoveError": "移動に失敗しました: {{err}}", "failedMoveInvalid": "移動に失敗しました: 対象グループが見つからないか無効です。", - "failedRemoveImages": "画像の削除に失敗しました: {{err}}" + "failedRemoveImages": "画像の削除に失敗しました: {{err}}", + "failedStartAiTagging": "AIタグ付けの開始に失敗しました: {{err}}" } }, "editor": { diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index ad75266ea9..dc953efa4a 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -201,6 +201,7 @@ "copyHere_one": "여기에 이미지 복사", "copyHere_other": "여기에 이미지 {{count}}개 복사", "deleteFolder": "폴더 삭제", + "generateThumbnails": "모든 썸네일 생성", "importImages": "이미지 가져오기", "moveHere_one": "여기로 이미지 이동", "moveHere_other": "여기로 이미지 {{count}}개 이동", @@ -208,6 +209,7 @@ "paste": "붙여넣기", "pin": "폴더 고정", "refresh": "폴더 새로고침", + "retagWithAi": "AI로 콘텐츠 다시 태그", "removeRoot": "루트 폴더 제거", "renameFolder": "폴더 이름 변경", "showExplorer": "파일 탐색기에서 표시", @@ -272,10 +274,12 @@ "failedDelete": "삭제하지 못했습니다: {{err}}", "failedDeleteFolder": "폴더를 삭제하지 못했습니다: {{err}}", "failedDuplicate": "파일을 복제하지 못했습니다: {{err}}", + "failedGenerateThumbnails": "썸네일을 생성하지 못했습니다: {{err}}", "failedMove": "파일을 이동하지 못했습니다: {{err}}", "failedMoveError": "이동하지 못했습니다: {{err}}", "failedMoveInvalid": "이동하지 못했습니다: 대상 그룹을 찾을 수 없거나 유효하지 않습니다.", - "failedRemoveImages": "이미지를 제거하지 못했습니다: {{err}}" + "failedRemoveImages": "이미지를 제거하지 못했습니다: {{err}}", + "failedStartAiTagging": "AI 태그 지정을 시작하지 못했습니다: {{err}}" } }, "editor": { diff --git a/src/i18n/locales/pl.json b/src/i18n/locales/pl.json index 1575021315..68b8c765ca 100644 --- a/src/i18n/locales/pl.json +++ b/src/i18n/locales/pl.json @@ -205,6 +205,7 @@ "copyHere_one": "Skopiuj obraz tutaj", "copyHere_other": "Skopiuj {{count}} obrazów tutaj", "deleteFolder": "Usuń folder", + "generateThumbnails": "Wygeneruj wszystkie miniatury", "importImages": "Importuj obrazy", "moveHere_few": "Przenieś {{count}} obrazy tutaj", "moveHere_many": "Przenieś {{count}} obrazów tutaj", @@ -214,6 +215,7 @@ "paste": "Wklej", "pin": "Przypnij folder", "refresh": "Odśwież foldery", + "retagWithAi": "Oznacz zawartość ponownie przez AI", "removeRoot": "Usuń folder główny", "renameFolder": "Zmień nazwę folderu", "showExplorer": "Pokaż w eksploratorze plików", @@ -304,10 +306,12 @@ "failedDelete": "Nie udało się usunąć: {{err}}", "failedDeleteFolder": "Nie udało się usunąć folderu: {{err}}", "failedDuplicate": "Nie udało się zduplikować pliku: {{err}}", + "failedGenerateThumbnails": "Nie udało się wygenerować miniatur: {{err}}", "failedMove": "Nie udało się przenieść plików: {{err}}", "failedMoveError": "Nie udało się przenieść: {{err}}", "failedMoveInvalid": "Nie udało się przenieść: Grupa docelowa nie znaleziona lub nieprawidłowa.", - "failedRemoveImages": "Nie udało się usunąć obrazów: {{err}}" + "failedRemoveImages": "Nie udało się usunąć obrazów: {{err}}", + "failedStartAiTagging": "Nie udało się uruchomić tagowania AI: {{err}}" } }, "editor": { diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 7b5f69e6e7..0f604b9752 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -203,6 +203,7 @@ "copyHere_one": "Copiar imagem para cá", "copyHere_other": "Copiar {{count}} imagens para cá", "deleteFolder": "Excluir Pasta", + "generateThumbnails": "Gerar todas as miniaturas", "importImages": "Importar Imagens", "moveHere_many": "Mover {{count}} imagens para cá", "moveHere_one": "Mover imagem para cá", @@ -211,6 +212,7 @@ "paste": "Colar", "pin": "Fixar Pasta", "refresh": "Atualizar Pastas", + "retagWithAi": "Reetiquetar conteúdo com IA", "removeRoot": "Remover Pasta Raiz", "renameFolder": "Renomear Pasta", "showExplorer": "Mostrar no Explorador de Arquivos", @@ -288,10 +290,12 @@ "failedDelete": "Falha ao excluir: {{err}}", "failedDeleteFolder": "Falha ao excluir pasta: {{err}}", "failedDuplicate": "Falha ao duplicar arquivo: {{err}}", + "failedGenerateThumbnails": "Falha ao gerar miniaturas: {{err}}", "failedMove": "Falha ao mover arquivos: {{err}}", "failedMoveError": "Falha ao mover: {{err}}", "failedMoveInvalid": "Falha ao mover: Grupo de destino não encontrado ou inválido.", - "failedRemoveImages": "Falha ao remover imagens: {{err}}" + "failedRemoveImages": "Falha ao remover imagens: {{err}}", + "failedStartAiTagging": "Falha ao iniciar etiquetagem por IA: {{err}}" } }, "editor": { diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index cf2b103884..5bb6eb3e4e 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -205,6 +205,7 @@ "copyHere_one": "Копировать изображение сюда", "copyHere_other": "Копировать {{count}} изображений сюда", "deleteFolder": "Удалить папку", + "generateThumbnails": "Создать все миниатюры", "importImages": "Импортировать изображения", "moveHere_few": "Переместить {{count}} изображений сюда", "moveHere_many": "Переместить {{count}} изображений сюда", @@ -214,6 +215,7 @@ "paste": "Вставить", "pin": "Закрепить папку", "refresh": "Обновить папки", + "retagWithAi": "Повторно назначить ИИ-теги", "removeRoot": "Удалить корневую папку", "renameFolder": "Переименовать папку", "showExplorer": "Показать в проводнике", @@ -304,10 +306,12 @@ "failedDelete": "Не удалось удалить: {{err}}", "failedDeleteFolder": "Не удалось удалить папку: {{err}}", "failedDuplicate": "Не удалось дублировать файл: {{err}}", + "failedGenerateThumbnails": "Не удалось создать миниатюры: {{err}}", "failedMove": "Не удалось переместить файлы: {{err}}", "failedMoveError": "Не удалось переместить: {{err}}", "failedMoveInvalid": "Не удалось переместить: целевая группа не найдена или недействительна.", - "failedRemoveImages": "Не удалось удалить изображения: {{err}}" + "failedRemoveImages": "Не удалось удалить изображения: {{err}}", + "failedStartAiTagging": "Не удалось запустить ИИ-тегирование: {{err}}" } }, "editor": { diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 3d0f18bd9e..5abffb662e 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -201,6 +201,7 @@ "copyHere_one": "复制图像至此", "copyHere_other": "复制 {{count}} 张图像至此", "deleteFolder": "删除文件夹", + "generateThumbnails": "生成所有缩略图", "importImages": "导入图像", "moveHere_one": "移动图像至此", "moveHere_other": "移动 {{count}} 张图像至此", @@ -208,6 +209,7 @@ "paste": "粘贴", "pin": "固定文件夹", "refresh": "刷新文件夹", + "retagWithAi": "使用 AI 重新标记内容", "removeRoot": "移除根文件夹", "renameFolder": "重命名文件夹", "showExplorer": "在文件资源管理器中显示", @@ -272,10 +274,12 @@ "failedDelete": "删除失败:{{err}}", "failedDeleteFolder": "删除文件夹失败:{{err}}", "failedDuplicate": "复制文件失败:{{err}}", + "failedGenerateThumbnails": "生成缩略图失败:{{err}}", "failedMove": "移动文件失败:{{err}}", "failedMoveError": "移动失败:{{err}}", "failedMoveInvalid": "移动失败:未找到目标组或目标组无效。", - "failedRemoveImages": "移除图像失败:{{err}}" + "failedRemoveImages": "移除图像失败:{{err}}", + "failedStartAiTagging": "启动 AI 标记失败:{{err}}" } }, "editor": { diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index f13c88f22b..f866c9cfb8 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -201,6 +201,7 @@ "copyHere_one": "複製影像至此", "copyHere_other": "複製 {{count}} 張影像至此", "deleteFolder": "刪除資料夾", + "generateThumbnails": "產生所有縮圖", "importImages": "匯入影像", "moveHere_one": "移動影像至此", "moveHere_other": "移動 {{count}} 張影像至此", @@ -208,6 +209,7 @@ "paste": "貼上", "pin": "固定資料夾", "refresh": "重新整理資料夾", + "retagWithAi": "使用 AI 重新標記內容", "removeRoot": "移除根資料夾", "renameFolder": "重新命名資料夾", "showExplorer": "在檔案資源管理器中顯示", @@ -272,10 +274,12 @@ "failedDelete": "刪除失敗:{{err}}", "failedDeleteFolder": "刪除資料夾失敗:{{err}}", "failedDuplicate": "複製檔案失敗:{{err}}", + "failedGenerateThumbnails": "產生縮圖失敗:{{err}}", "failedMove": "移動檔案失敗:{{err}}", "failedMoveError": "移動失敗:{{err}}", "failedMoveInvalid": "移動失敗:未找到目標組或目標組無效。", - "failedRemoveImages": "移除影像失敗:{{err}}" + "failedRemoveImages": "移除影像失敗:{{err}}", + "failedStartAiTagging": "啟動 AI 標記失敗:{{err}}" } }, "editor": {