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 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 18:59:05 +01:00
parent 43772aba68
commit 0c2504ff83
3 changed files with 68 additions and 64 deletions

View File

@@ -450,23 +450,25 @@ class SorterEngine:
@staticmethod @staticmethod
def commit_global(output_root, cleanup_mode, operation="Copy", source_root=None, profile=None): 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() data = SorterEngine.get_staged_data()
committed = {}
# Save folder tags BEFORE processing (so we can restore them later) # Save folder tags BEFORE processing (so we can restore them later)
if source_root: if source_root:
SorterEngine.save_folder_tags(source_root, profile) SorterEngine.save_folder_tags(source_root, profile)
conn = sqlite3.connect(SorterEngine.DB_PATH) conn = sqlite3.connect(SorterEngine.DB_PATH)
cursor = conn.cursor() cursor = conn.cursor()
if not os.path.exists(output_root): os.makedirs(output_root, exist_ok=True) if not os.path.exists(output_root): os.makedirs(output_root, exist_ok=True)
# 1. Process all Staged Items # 1. Process all Staged Items
for old_p, info in data.items(): for old_p, info in data.items():
if os.path.exists(old_p): if os.path.exists(old_p):
final_dst = os.path.join(output_root, info['name']) final_dst = os.path.join(output_root, info['name'])
if os.path.exists(final_dst): if os.path.exists(final_dst):
root, ext = os.path.splitext(info['name']) root, ext = os.path.splitext(info['name'])
c = 1 c = 1
@@ -478,12 +480,15 @@ class SorterEngine:
shutil.copy2(old_p, final_dst) shutil.copy2(old_p, final_dst)
else: else:
shutil.move(old_p, final_dst) shutil.move(old_p, final_dst)
# --- FIX PERMISSIONS --- # --- FIX PERMISSIONS ---
SorterEngine.fix_permissions(final_dst) SorterEngine.fix_permissions(final_dst)
# Track actual destination
committed[old_p] = {"dest": final_dst, "cat": info['cat']}
# Log History # 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)) (old_p, info['cat'], operation))
# 2. Global Cleanup # 2. Global Cleanup
@@ -495,16 +500,17 @@ class SorterEngine:
unused_dir = os.path.join(source_root, "unused") unused_dir = os.path.join(source_root, "unused")
os.makedirs(unused_dir, exist_ok=True) os.makedirs(unused_dir, exist_ok=True)
dest_unused = os.path.join(unused_dir, os.path.basename(img_p)) dest_unused = os.path.join(unused_dir, os.path.basename(img_p))
shutil.move(img_p, dest_unused) shutil.move(img_p, dest_unused)
SorterEngine.fix_permissions(dest_unused) SorterEngine.fix_permissions(dest_unused)
elif cleanup_mode == "Delete": elif cleanup_mode == "Delete":
os.remove(img_p) os.remove(img_p)
cursor.execute("DELETE FROM staging_area") cursor.execute("DELETE FROM staging_area")
conn.commit() conn.commit()
conn.close() conn.close()
return committed
# --- 6. CORE UTILITIES (SYNC & UNDO) --- # --- 6. CORE UTILITIES (SYNC & UNDO) ---
@staticmethod @staticmethod
@@ -616,21 +622,23 @@ class SorterEngine:
@staticmethod @staticmethod
def commit_batch(file_list, output_root, cleanup_mode, operation="Copy"): 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() data = SorterEngine.get_staged_data()
conn = sqlite3.connect(SorterEngine.DB_PATH) conn = sqlite3.connect(SorterEngine.DB_PATH)
cursor = conn.cursor() cursor = conn.cursor()
committed = {}
if not os.path.exists(output_root): os.makedirs(output_root, exist_ok=True) if not os.path.exists(output_root): os.makedirs(output_root, exist_ok=True)
for file_path in file_list: for file_path in file_list:
if not os.path.exists(file_path): continue if not os.path.exists(file_path): continue
# --- CASE A: Tagged --- # --- CASE A: Tagged ---
if file_path in data and data[file_path]['marked']: if file_path in data and data[file_path]['marked']:
info = data[file_path] info = data[file_path]
final_dst = os.path.join(output_root, info['name']) final_dst = os.path.join(output_root, info['name'])
# Collision Check # Collision Check
if os.path.exists(final_dst): if os.path.exists(final_dst):
root, ext = os.path.splitext(info['name']) root, ext = os.path.splitext(info['name'])
@@ -638,7 +646,7 @@ class SorterEngine:
while os.path.exists(final_dst): while os.path.exists(final_dst):
final_dst = os.path.join(output_root, f"{root}_{c}{ext}") final_dst = os.path.join(output_root, f"{root}_{c}{ext}")
c += 1 c += 1
# Perform Action # Perform Action
if operation == "Copy": if operation == "Copy":
shutil.copy2(file_path, final_dst) shutil.copy2(file_path, final_dst)
@@ -648,26 +656,30 @@ class SorterEngine:
# --- FIX PERMISSIONS --- # --- FIX PERMISSIONS ---
SorterEngine.fix_permissions(final_dst) SorterEngine.fix_permissions(final_dst)
# Track actual destination
committed[file_path] = {"dest": final_dst, "cat": info['cat']}
# Update DB # Update DB
cursor.execute("DELETE FROM staging_area WHERE original_path = ?", (file_path,)) 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)) (file_path, info['cat'], operation))
# --- CASE B: Cleanup --- # --- CASE B: Cleanup ---
elif cleanup_mode != "Keep": elif cleanup_mode != "Keep":
if cleanup_mode == "Move to Unused": if cleanup_mode == "Move to Unused":
unused_dir = os.path.join(os.path.dirname(file_path), "unused") unused_dir = os.path.join(os.path.dirname(file_path), "unused")
os.makedirs(unused_dir, exist_ok=True) os.makedirs(unused_dir, exist_ok=True)
dest_unused = os.path.join(unused_dir, os.path.basename(file_path)) dest_unused = os.path.join(unused_dir, os.path.basename(file_path))
shutil.move(file_path, dest_unused) shutil.move(file_path, dest_unused)
SorterEngine.fix_permissions(dest_unused) # Fix here too SorterEngine.fix_permissions(dest_unused) # Fix here too
elif cleanup_mode == "Delete": elif cleanup_mode == "Delete":
os.remove(file_path) os.remove(file_path)
conn.commit() conn.commit()
conn.close() conn.close()
return committed
@staticmethod @staticmethod
def rename_category(old_name, new_name): def rename_category(old_name, new_name):

View File

@@ -273,10 +273,10 @@ def load_images():
state.page = 0 state.page = 0
refresh_staged_info() refresh_staged_info()
refresh_ui() # Load caption data before rendering so indicators appear on cards
# Refresh caption cache in background (non-blocking)
state.refresh_caption_cache() state.refresh_caption_cache()
state.load_caption_settings() state.load_caption_settings()
refresh_ui()
# ========================================== # ==========================================
# PAIRING MODE FUNCTIONS # PAIRING MODE FUNCTIONS
@@ -715,24 +715,16 @@ async def action_apply_page():
ui.notify("No images on current page", type='warning') ui.notify("No images on current page", type='warning')
return return
# Get tagged images and their categories before commit (they'll be moved/copied) committed = SorterEngine.commit_batch(batch, state.output_dir, state.cleanup_mode, state.batch_mode)
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))
SorterEngine.commit_batch(batch, state.output_dir, state.cleanup_mode, state.batch_mode) # Caption on apply if enabled - uses actual dest paths from commit
if state.caption_on_apply and committed:
# Caption on apply if enabled
if state.caption_on_apply and tagged_batch:
state.load_caption_settings() state.load_caption_settings()
caption_count = 0 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): 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( caption, error = await run.io_bound(
SorterEngine.caption_image_vllm, SorterEngine.caption_image_vllm,
dest_path, prompt, state.caption_settings dest_path, prompt, state.caption_settings
@@ -753,14 +745,7 @@ async def action_apply_global():
"""Apply all staged changes globally.""" """Apply all staged changes globally."""
ui.notify("Starting global apply... This may take a while.", type='info') ui.notify("Starting global apply... This may take a while.", type='info')
# Capture staged data before commit for captioning committed = await run.io_bound(
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(
SorterEngine.commit_global, SorterEngine.commit_global,
state.output_dir, state.output_dir,
state.cleanup_mode, state.cleanup_mode,
@@ -769,13 +754,13 @@ async def action_apply_global():
state.profile_name state.profile_name
) )
# Caption on apply if enabled # Caption on apply if enabled - uses actual dest paths from commit
if state.caption_on_apply and staged_before_commit: if state.caption_on_apply and committed:
state.load_caption_settings() 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 caption_count = 0
for orig_path, info in staged_before_commit.items(): for orig_path, info in committed.items():
dest_path = info['dest'] dest_path = info['dest']
if os.path.exists(dest_path): if os.path.exists(dest_path):
prompt = SorterEngine.get_category_prompt(state.profile_name, info['cat']) prompt = SorterEngine.get_category_prompt(state.profile_name, info['cat'])

33
start.sh Normal file → Executable file
View File

@@ -1,18 +1,25 @@
#!/bin/bash #!/bin/bash
# 1. Navigate to app directory # NiceSorter - Start Script
cd /app # Runs the NiceGUI gallery interface on port 8080
# 2. Install dependencies (Including NiceGUI if missing) set -e
# This checks your requirements.txt AND ensures nicegui is present
pip install --no-cache-dir -r requirements.txt
# 3. Start NiceGUI in the Background (&) # Navigate to app directory if running in container
# This runs silently while the script continues if [ -d "/app" ]; then
echo "🚀 Starting NiceGUI on Port 8080..." cd /app
python3 gallery_app.py & fi
# 4. Start Streamlit in the Foreground # Install dependencies if requirements.txt exists
# This keeps the container running if [ -f "requirements.txt" ]; then
echo "🚀 Starting Streamlit on Port 8501..." echo "📦 Installing dependencies..."
streamlit run app.py --server.port=8501 --server.address=0.0.0.0 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