From 29335bd4d5ed5becc3183b30624b8e6f7a676621 Mon Sep 17 00:00:00 2001 From: ethanfel Date: Sat, 3 Jan 2026 16:13:26 +0100 Subject: [PATCH] Update app.py --- app.py | 430 +++++++++++++++++++++++++-------------------------------- 1 file changed, 190 insertions(+), 240 deletions(-) diff --git a/app.py b/app.py index 7482c7f..8fd194d 100644 --- a/app.py +++ b/app.py @@ -1,264 +1,214 @@ -import gradio as gr -import sqlite3 -import json -import os -import time -from datetime import datetime +import streamlit as st +import random +from pathlib import Path + +# --- Import Custom Modules --- +from utils import ( + load_config, save_config, load_snippets, save_snippets, + load_json, save_json, generate_templates, DEFAULTS +) +from tab_single import render_single_editor +from tab_batch import render_batch_processor +from tab_timeline import render_timeline_tab +from tab_timeline_wip import render_timeline_wip +from tab_comfy import render_comfy_monitor # ========================================== -# 1. DATABASE & PERSISTENCE LAYER +# 1. PAGE CONFIGURATION # ========================================== - -DB_FILE = "app_data.db" -LEGACY_JSON_FILE = "settings.json" # File to import from if DB is empty - -def get_db(): - """Connect to SQLite and return connection.""" - conn = sqlite3.connect(DB_FILE) - conn.row_factory = sqlite3.Row # Access columns by name - return conn - -def init_db(): - """Initialize tables and migrate legacy JSON if needed.""" - conn = get_db() - c = conn.cursor() - - # Table 1: Single Tab Settings (Keyed by file path) - c.execute(''' - CREATE TABLE IF NOT EXISTS path_settings ( - path TEXT PRIMARY KEY, - settings_json TEXT, - updated_at TIMESTAMP - ) - ''') - - # Table 2: Batch History (Logs of batch runs) - c.execute(''' - CREATE TABLE IF NOT EXISTS batch_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp TIMESTAMP, - folder_path TEXT, - log_data TEXT - ) - ''') - - conn.commit() - - # --- MIGRATION LOGIC --- - # Check if DB is empty - c.execute("SELECT COUNT(*) FROM path_settings") - count = c.fetchone()[0] - - if count == 0 and os.path.exists(LEGACY_JSON_FILE): - print(f"[DB] Database empty. Found {LEGACY_JSON_FILE}, migrating...") - try: - with open(LEGACY_JSON_FILE, 'r') as f: - legacy_data = json.load(f) - - # Assuming JSON structure: {"/path/to/img.png": {"threshold": 0.5, ...}} - # If your JSON is flat, you might need to adjust this loop. - if isinstance(legacy_data, dict): - for path, data in legacy_data.items(): - c.execute( - "INSERT OR IGNORE INTO path_settings (path, settings_json, updated_at) VALUES (?, ?, ?)", - (path, json.dumps(data), datetime.now()) - ) - conn.commit() - print("[DB] Migration complete.") - except Exception as e: - print(f"[DB] Migration failed: {e}") - - conn.close() - -# --- DB Helper Functions --- - -def load_settings(path): - """Load settings for a specific path from DB.""" - if not path: - return None - - conn = get_db() - c = conn.cursor() - c.execute("SELECT settings_json FROM path_settings WHERE path = ?", (path,)) - row = c.fetchone() - conn.close() - - if row: - return json.loads(row['settings_json']) - return None - -def save_settings(path, settings_dict): - """Save/Update settings for a specific path.""" - if not path: - return - - conn = get_db() - c = conn.cursor() - json_str = json.dumps(settings_dict) - - # Upsert: Insert, or Update if path exists - c.execute(''' - INSERT INTO path_settings (path, settings_json, updated_at) - VALUES (?, ?, ?) - ON CONFLICT(path) DO UPDATE SET - settings_json=excluded.settings_json, - updated_at=excluded.updated_at - ''', (path, json_str, datetime.now())) - - conn.commit() - conn.close() - print(f"[DB] Saved settings for {path}") - -def save_batch_log(folder_path, logs): - """Save batch run details to DB.""" - conn = get_db() - c = conn.cursor() - c.execute(''' - INSERT INTO batch_history (timestamp, folder_path, log_data) - VALUES (?, ?, ?) - ''', (datetime.now(), folder_path, json.dumps(logs))) - conn.commit() - conn.close() - -# Initialize DB on startup -init_db() - +st.set_page_config(layout="wide", page_title="AI Settings Manager") # ========================================== -# 2. APP LOGIC +# 2. SESSION STATE INITIALIZATION # ========================================== +if 'config' not in st.session_state: + st.session_state.config = load_config() + st.session_state.current_dir = Path(st.session_state.config.get("last_dir", Path.cwd())) -# Default settings to fall back on -DEFAULTS = { - "threshold": 0.5, - "invert": False, - "scale": 1.0 -} +if 'snippets' not in st.session_state: + st.session_state.snippets = load_snippets() -def on_path_load(path): - """ - Triggered when user enters a path or clicks Load. - Fetches specific settings for this path from DB. - """ - data = load_settings(path) - if data: - msg = f"Loaded saved settings for: {os.path.basename(path)}" - # Return values to update UI components - return msg, data.get("threshold", DEFAULTS["threshold"]), data.get("invert", DEFAULTS["invert"]), data.get("scale", DEFAULTS["scale"]) - else: - msg = "No saved settings found (using defaults)" - return msg, DEFAULTS["threshold"], DEFAULTS["invert"], DEFAULTS["scale"] +if 'loaded_file' not in st.session_state: + st.session_state.loaded_file = None -def process_single_image(path, threshold, invert, scale): - """ - Simulates processing. - Critically: SAVES the settings used to the DB. - """ - if not path: - return "Error: No path provided" +if 'last_mtime' not in st.session_state: + st.session_state.last_mtime = 0 - # 1. Save these settings to DB so they are remembered next time - current_settings = { - "threshold": threshold, - "invert": invert, - "scale": scale - } - save_settings(path, current_settings) +if 'edit_history_idx' not in st.session_state: + st.session_state.edit_history_idx = None - # 2. Run actual processing (Place your real code here) - time.sleep(0.5) # Simulate work - return f"Success! Processed {os.path.basename(path)} with Threshold={threshold}. Settings Saved." +if 'single_editor_cache' not in st.session_state: + st.session_state.single_editor_cache = DEFAULTS.copy() -def process_batch(folder_path, threshold): - """ - Simulates batch processing. - Logs the result to the 'batch_history' table. - """ - if not os.path.isdir(folder_path): - return "Error: Invalid folder path" - - files = [f for f in os.listdir(folder_path) if f.endswith(('.png', '.jpg'))] - log_messages = [] - - for f in files: - # Simulate processing each file - # You could also load individual file settings here if needed - log_messages.append(f"Processed {f} (Threshold: {threshold})") - - result_text = "\n".join(log_messages) - - # Save to Batch DB - save_batch_log(folder_path, log_messages) - - return f"Batch Complete. {len(files)} files processed.\n\nLOG:\n{result_text}" +if 'ui_reset_token' not in st.session_state: + st.session_state.ui_reset_token = 0 +# Track the active tab state for programmatic switching +if 'active_tab_name' not in st.session_state: + st.session_state.active_tab_name = "๐Ÿ“ Single Editor" # ========================================== -# 3. GRADIO INTERFACE +# 3. SIDEBAR (NAVIGATOR & TOOLS) # ========================================== - -with gr.Blocks(title="App with DB Persistence") as app: - gr.Markdown("## Image Processor (SQLite Powered)") +with st.sidebar: + st.header("๐Ÿ“‚ Navigator") - with gr.Tabs(): + # --- Path Navigator --- + new_path = st.text_input("Current Path", value=str(st.session_state.current_dir)) + if new_path != str(st.session_state.current_dir): + p = Path(new_path) + if p.exists() and p.is_dir(): + st.session_state.current_dir = p + st.session_state.config['last_dir'] = str(p) + save_config(st.session_state.current_dir, st.session_state.config['favorites']) + st.rerun() + + # --- Favorites System --- + if st.button("๐Ÿ“Œ Pin Current Folder"): + if str(st.session_state.current_dir) not in st.session_state.config['favorites']: + st.session_state.config['favorites'].append(str(st.session_state.current_dir)) + save_config(st.session_state.current_dir, st.session_state.config['favorites']) + st.rerun() + + fav_selection = st.radio( + "Jump to:", + ["Select..."] + st.session_state.config['favorites'], + index=0, + label_visibility="collapsed" + ) + if fav_selection != "Select..." and fav_selection != str(st.session_state.current_dir): + st.session_state.current_dir = Path(fav_selection) + st.rerun() + + st.markdown("---") + + # --- Snippet Library --- + st.subheader("๐Ÿงฉ Snippet Library") + with st.expander("Add New Snippet"): + snip_name = st.text_input("Name", placeholder="e.g. Cinematic") + snip_content = st.text_area("Content", placeholder="4k, high quality...") + if st.button("Save Snippet"): + if snip_name and snip_content: + st.session_state.snippets[snip_name] = snip_content + save_snippets(st.session_state.snippets) + st.success(f"Saved '{snip_name}'") + st.rerun() + + if st.session_state.snippets: + st.caption("Click to Append to Prompt:") + for name, content in st.session_state.snippets.items(): + col_s1, col_s2 = st.columns([4, 1]) + if col_s1.button(f"โž• {name}", use_container_width=True): + st.session_state.append_prompt = content + st.rerun() + if col_s2.button("๐Ÿ—‘๏ธ", key=f"del_snip_{name}"): + del st.session_state.snippets[name] + save_snippets(st.session_state.snippets) + st.rerun() + + st.markdown("---") + + # --- File List & Creation --- + json_files = sorted(list(st.session_state.current_dir.glob("*.json"))) + json_files = [f for f in json_files if f.name != ".editor_config.json" and f.name != ".editor_snippets.json"] + + if not json_files: + if st.button("Generate Templates"): + generate_templates(st.session_state.current_dir) + st.rerun() + + with st.expander("Create New JSON"): + new_filename = st.text_input("Filename", placeholder="my_prompt_vace") + is_batch = st.checkbox("Is Batch File?") + if st.button("Create"): + if not new_filename.endswith(".json"): new_filename += ".json" + path = st.session_state.current_dir / new_filename + if is_batch: + data = {"batch_data": []} + else: + data = DEFAULTS.copy() + if "vace" in new_filename: data.update({"frame_to_skip": 81, "vace schedule": 1, "video file path": ""}) + elif "i2v" in new_filename: data.update({"reference image path": "", "flf image path": ""}) + save_json(path, data) + st.rerun() + + # --- File Selector --- + if 'file_selector' not in st.session_state: + st.session_state.file_selector = json_files[0].name if json_files else None + if st.session_state.file_selector not in [f.name for f in json_files] and json_files: + st.session_state.file_selector = json_files[0].name + + selected_file_name = st.radio("Select File", [f.name for f in json_files], key="file_selector") + +# ========================================== +# 4. MAIN APP LOGIC +# ========================================== +if selected_file_name: + file_path = st.session_state.current_dir / selected_file_name + + # --- FILE LOADING & AUTO-SWITCH LOGIC --- + if st.session_state.loaded_file != str(file_path): + data, mtime = load_json(file_path) + st.session_state.data_cache = data + st.session_state.last_mtime = mtime + st.session_state.loaded_file = str(file_path) - # --- TAB 1: SINGLE IMAGE --- - with gr.Tab("Single Image"): - gr.Markdown("Settings are saved automatically per file path.") + # Clear transient states + if 'append_prompt' in st.session_state: del st.session_state.append_prompt + if 'rand_seed' in st.session_state: del st.session_state.rand_seed + if 'restored_indicator' in st.session_state: del st.session_state.restored_indicator + st.session_state.edit_history_idx = None + + # --- AUTO-SWITCH TAB LOGIC --- + # If the file has 'batch_data' or is a list, force Batch tab. + # Otherwise, force Single tab. + is_batch = "batch_data" in data or isinstance(data, list) + if is_batch: + st.session_state.active_tab_name = "๐Ÿš€ Batch Processor" + else: + st.session_state.active_tab_name = "๐Ÿ“ Single Editor" - with gr.Row(): - # The 'Key' for our database - path_input = gr.Textbox(label="Image File Path", placeholder="C:/Images/photo1.png", scale=3) - load_btn = gr.Button("Load Settings", scale=1) - - with gr.Row(): - # Settings Inputs - thresh_slider = gr.Slider(0.0, 1.0, value=DEFAULTS["threshold"], label="Threshold") - scale_num = gr.Number(value=DEFAULTS["scale"], label="Scale Factor") - invert_chk = gr.Checkbox(value=DEFAULTS["invert"], label="Invert Colors") + else: + data = st.session_state.data_cache - status_output = gr.Textbox(label="Status / Output", lines=2) - run_btn = gr.Button("Process & Save", variant="primary") + st.title(f"Editing: {selected_file_name}") - # Interactions - # 1. Loading settings when button clicked - load_btn.click( - fn=on_path_load, - inputs=[path_input], - outputs=[status_output, thresh_slider, invert_chk, scale_num] - ) - - # 2. (Optional) Auto-load when path input loses focus (blur) - path_input.blur( - fn=on_path_load, - inputs=[path_input], - outputs=[status_output, thresh_slider, invert_chk, scale_num] - ) + # --- CONTROLLED NAVIGATION (REPLACES ST.TABS) --- + # Using radio buttons allows us to change 'active_tab_name' programmatically above. + tabs_list = [ + "๐Ÿ“ Single Editor", + "๐Ÿš€ Batch Processor", + "๐Ÿ•’ Timeline", + "๐Ÿงช Interactive Timeline", + "๐Ÿ”Œ Comfy Monitor" + ] + + # Ensure active tab is valid (safety check) + if st.session_state.active_tab_name not in tabs_list: + st.session_state.active_tab_name = tabs_list[0] - # 3. Processing - run_btn.click( - fn=process_single_image, - inputs=[path_input, thresh_slider, invert_chk, scale_num], - outputs=[status_output] - ) + current_tab = st.radio( + "Navigation", + tabs_list, + key="active_tab_name", # Binds to session state + horizontal=True, + label_visibility="collapsed" + ) + + st.markdown("---") - # --- TAB 2: BATCH PROCESS --- - with gr.Tab("Batch Processing"): - gr.Markdown("Batch runs are logged to history.") - - batch_input = gr.Textbox(label="Input Folder", placeholder="C:/Images/Batch_Folder") - - # Example: Global override for batch, or you could load per file - batch_thresh = gr.Slider(0.0, 1.0, value=0.5, label="Global Threshold Override") - - batch_run_btn = gr.Button("Run Batch", variant="primary") - batch_output = gr.TextArea(label="Batch Log", lines=10) + # --- RENDER SELECTED TAB --- + if current_tab == "๐Ÿ“ Single Editor": + render_single_editor(data, file_path) + + elif current_tab == "๐Ÿš€ Batch Processor": + render_batch_processor(data, file_path, json_files, st.session_state.current_dir, selected_file_name) + + elif current_tab == "๐Ÿ•’ Timeline": + render_timeline_tab(data, file_path) + + elif current_tab == "๐Ÿงช Interactive Timeline": + render_timeline_wip(data, file_path) - batch_run_btn.click( - fn=process_batch, - inputs=[batch_input, batch_thresh], - outputs=[batch_output] - ) - -if __name__ == "__main__": - app.launch() + elif current_tab == "๐Ÿ”Œ Comfy Monitor": + render_comfy_monitor()