From 0c2504ff8377ec615e93356ac1dc9ffbf9ce6fc8 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sun, 8 Feb 2026 18:59:05 +0100 Subject: [PATCH] Fix caption cache timing, commit path tracking, and clean up start script - Move caption cache refresh before UI render so indicators show on load - Return actual dest paths from commit_batch/commit_global to fix caption-on-apply silently failing when files are renamed on collision - Simplify start.sh to only run NiceGUI (remove Streamlit) - Add requests to requirements.txt Co-Authored-By: Claude Opus 4.6 --- engine.py | 58 ++++++++++++++++++++++++++++++-------------------- gallery_app.py | 41 +++++++++++------------------------ start.sh | 33 +++++++++++++++++----------- 3 files changed, 68 insertions(+), 64 deletions(-) mode change 100644 => 100755 start.sh diff --git a/engine.py b/engine.py index 6340f55..d1c13b5 100644 --- a/engine.py +++ b/engine.py @@ -450,23 +450,25 @@ class SorterEngine: @staticmethod def commit_global(output_root, cleanup_mode, operation="Copy", source_root=None, profile=None): - """Commits ALL staged files and fixes permissions.""" + """Commits ALL staged files and fixes permissions. + Returns dict mapping original_path -> {dest, cat} for committed files.""" data = SorterEngine.get_staged_data() - + committed = {} + # Save folder tags BEFORE processing (so we can restore them later) if source_root: SorterEngine.save_folder_tags(source_root, profile) - + conn = sqlite3.connect(SorterEngine.DB_PATH) cursor = conn.cursor() - + if not os.path.exists(output_root): os.makedirs(output_root, exist_ok=True) - + # 1. Process all Staged Items for old_p, info in data.items(): if os.path.exists(old_p): final_dst = os.path.join(output_root, info['name']) - + if os.path.exists(final_dst): root, ext = os.path.splitext(info['name']) c = 1 @@ -478,12 +480,15 @@ class SorterEngine: shutil.copy2(old_p, final_dst) else: shutil.move(old_p, final_dst) - + # --- FIX PERMISSIONS --- SorterEngine.fix_permissions(final_dst) - + + # Track actual destination + committed[old_p] = {"dest": final_dst, "cat": info['cat']} + # Log History - cursor.execute("INSERT OR REPLACE INTO processed_log VALUES (?, ?, ?)", + cursor.execute("INSERT OR REPLACE INTO processed_log VALUES (?, ?, ?)", (old_p, info['cat'], operation)) # 2. Global Cleanup @@ -495,16 +500,17 @@ class SorterEngine: unused_dir = os.path.join(source_root, "unused") os.makedirs(unused_dir, exist_ok=True) dest_unused = os.path.join(unused_dir, os.path.basename(img_p)) - + shutil.move(img_p, dest_unused) SorterEngine.fix_permissions(dest_unused) - - elif cleanup_mode == "Delete": + + elif cleanup_mode == "Delete": os.remove(img_p) cursor.execute("DELETE FROM staging_area") conn.commit() conn.close() + return committed # --- 6. CORE UTILITIES (SYNC & UNDO) --- @staticmethod @@ -616,21 +622,23 @@ class SorterEngine: @staticmethod def commit_batch(file_list, output_root, cleanup_mode, operation="Copy"): - """Commits files and fixes permissions.""" + """Commits files and fixes permissions. + Returns dict mapping original_path -> actual_dest_path for committed files.""" data = SorterEngine.get_staged_data() conn = sqlite3.connect(SorterEngine.DB_PATH) cursor = conn.cursor() - + committed = {} + if not os.path.exists(output_root): os.makedirs(output_root, exist_ok=True) - + for file_path in file_list: if not os.path.exists(file_path): continue - + # --- CASE A: Tagged --- if file_path in data and data[file_path]['marked']: info = data[file_path] final_dst = os.path.join(output_root, info['name']) - + # Collision Check if os.path.exists(final_dst): root, ext = os.path.splitext(info['name']) @@ -638,7 +646,7 @@ class SorterEngine: while os.path.exists(final_dst): final_dst = os.path.join(output_root, f"{root}_{c}{ext}") c += 1 - + # Perform Action if operation == "Copy": shutil.copy2(file_path, final_dst) @@ -648,26 +656,30 @@ class SorterEngine: # --- FIX PERMISSIONS --- SorterEngine.fix_permissions(final_dst) + # Track actual destination + committed[file_path] = {"dest": final_dst, "cat": info['cat']} + # Update DB cursor.execute("DELETE FROM staging_area WHERE original_path = ?", (file_path,)) - cursor.execute("INSERT OR REPLACE INTO processed_log VALUES (?, ?, ?)", + cursor.execute("INSERT OR REPLACE INTO processed_log VALUES (?, ?, ?)", (file_path, info['cat'], operation)) - + # --- CASE B: Cleanup --- elif cleanup_mode != "Keep": if cleanup_mode == "Move to Unused": unused_dir = os.path.join(os.path.dirname(file_path), "unused") os.makedirs(unused_dir, exist_ok=True) dest_unused = os.path.join(unused_dir, os.path.basename(file_path)) - + shutil.move(file_path, dest_unused) SorterEngine.fix_permissions(dest_unused) # Fix here too - + elif cleanup_mode == "Delete": os.remove(file_path) - + conn.commit() conn.close() + return committed @staticmethod def rename_category(old_name, new_name): diff --git a/gallery_app.py b/gallery_app.py index 2cdf6df..46c9feb 100644 --- a/gallery_app.py +++ b/gallery_app.py @@ -273,10 +273,10 @@ def load_images(): state.page = 0 refresh_staged_info() - refresh_ui() - # Refresh caption cache in background (non-blocking) + # Load caption data before rendering so indicators appear on cards state.refresh_caption_cache() state.load_caption_settings() + refresh_ui() # ========================================== # PAIRING MODE FUNCTIONS @@ -715,24 +715,16 @@ async def action_apply_page(): ui.notify("No images on current page", type='warning') return - # Get tagged images and their categories before commit (they'll be moved/copied) - tagged_batch = [] - for img_path in batch: - if img_path in state.staged_data: - info = state.staged_data[img_path] - # Calculate destination path - dest_path = os.path.join(state.output_dir, info['name']) - tagged_batch.append((img_path, info['cat'], dest_path)) + committed = SorterEngine.commit_batch(batch, state.output_dir, state.cleanup_mode, state.batch_mode) - SorterEngine.commit_batch(batch, state.output_dir, state.cleanup_mode, state.batch_mode) - - # Caption on apply if enabled - if state.caption_on_apply and tagged_batch: + # Caption on apply if enabled - uses actual dest paths from commit + if state.caption_on_apply and committed: state.load_caption_settings() caption_count = 0 - for orig_path, category, dest_path in tagged_batch: + for orig_path, info in committed.items(): + dest_path = info['dest'] if os.path.exists(dest_path): - prompt = SorterEngine.get_category_prompt(state.profile_name, category) + prompt = SorterEngine.get_category_prompt(state.profile_name, info['cat']) caption, error = await run.io_bound( SorterEngine.caption_image_vllm, dest_path, prompt, state.caption_settings @@ -753,14 +745,7 @@ async def action_apply_global(): """Apply all staged changes globally.""" ui.notify("Starting global apply... This may take a while.", type='info') - # Capture staged data before commit for captioning - staged_before_commit = {} - if state.caption_on_apply: - for img_path, info in state.staged_data.items(): - dest_path = os.path.join(state.output_dir, info['name']) - staged_before_commit[img_path] = {'cat': info['cat'], 'dest': dest_path} - - await run.io_bound( + committed = await run.io_bound( SorterEngine.commit_global, state.output_dir, state.cleanup_mode, @@ -769,13 +754,13 @@ async def action_apply_global(): state.profile_name ) - # Caption on apply if enabled - if state.caption_on_apply and staged_before_commit: + # Caption on apply if enabled - uses actual dest paths from commit + if state.caption_on_apply and committed: state.load_caption_settings() - ui.notify(f"Captioning {len(staged_before_commit)} images...", type='info') + ui.notify(f"Captioning {len(committed)} images...", type='info') caption_count = 0 - for orig_path, info in staged_before_commit.items(): + for orig_path, info in committed.items(): dest_path = info['dest'] if os.path.exists(dest_path): prompt = SorterEngine.get_category_prompt(state.profile_name, info['cat']) diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index ee115af..51a05a3 --- a/start.sh +++ b/start.sh @@ -1,18 +1,25 @@ #!/bin/bash -# 1. Navigate to app directory -cd /app +# NiceSorter - Start Script +# Runs the NiceGUI gallery interface on port 8080 -# 2. Install dependencies (Including NiceGUI if missing) -# This checks your requirements.txt AND ensures nicegui is present -pip install --no-cache-dir -r requirements.txt +set -e -# 3. Start NiceGUI in the Background (&) -# This runs silently while the script continues -echo "🚀 Starting NiceGUI on Port 8080..." -python3 gallery_app.py & +# Navigate to app directory if running in container +if [ -d "/app" ]; then + cd /app +fi -# 4. Start Streamlit in the Foreground -# This keeps the container running -echo "🚀 Starting Streamlit on Port 8501..." -streamlit run app.py --server.port=8501 --server.address=0.0.0.0 \ No newline at end of file +# Install dependencies if requirements.txt exists +if [ -f "requirements.txt" ]; then + echo "📦 Installing dependencies..." + pip install --no-cache-dir -q -r requirements.txt +fi + +# Initialize database +echo "🗄️ Initializing database..." +python3 -c "from engine import SorterEngine; SorterEngine.init_db()" + +# Start NiceGUI +echo "🚀 Starting NiceSorter on http://0.0.0.0:8080" +exec python3 gallery_app.py