diff --git a/tab_single.py b/tab_single.py index 4d19e70..a05b7c0 100644 --- a/tab_single.py +++ b/tab_single.py @@ -1,145 +1,250 @@ import streamlit as st import random -import json from utils import DEFAULTS, save_json, get_file_mtime -from history_tree import HistoryTree def render_single_editor(data, file_path): is_batch_file = "batch_data" in data or isinstance(data, list) + if is_batch_file: - st.warning("⚠️ This file looks like a Batch file. Please switch to the 'Batch Processor' tab.") + st.info("This is a batch file. Switch to the 'Batch Processor' tab.") return - # Check external modification - current_mtime = get_file_mtime(file_path) - if st.session_state.last_mtime != 0 and current_mtime > st.session_state.last_mtime: - st.error("⚠️ File has been modified externally! Save will overwrite.") - - # --- TOP ROW: MODELS --- - st.subheader("🤖 Models") - c1, c2 = st.columns(2) - with c1: - data["model_name"] = st.text_input("Checkpoint", value=data.get("model_name", "")) - with c2: - data["vae_name"] = st.text_input("VAE", value=data.get("vae_name", "")) - - # --- PROMPTS --- - st.markdown("---") - st.subheader("📝 Prompts") - - if 'append_prompt' in st.session_state: - current_p = data.get("positive_prompt", "") - if current_p: current_p += "\n" - data["positive_prompt"] = current_p + st.session_state.append_prompt - del st.session_state.append_prompt - - data["positive_prompt"] = st.text_area("Positive Prompt", value=data.get("positive_prompt", ""), height=150) - data["negative_prompt"] = st.text_area("Negative Prompt", value=data.get("negative_prompt", ""), height=100) - - # --- MAIN SETTINGS --- - st.markdown("---") - st.subheader("⚙️ Settings") + col1, col2 = st.columns([2, 1]) - col1, col2, col3 = st.columns(3) + # Unique prefix for this file's widgets + Version Token (Fixes Restore bug) + fk = f"{file_path.name}_v{st.session_state.ui_reset_token}" + + # --- FORM --- with col1: - data["steps"] = st.number_input("Steps", value=int(data.get("steps", 20))) - data["cfg"] = st.number_input("CFG", value=float(data.get("cfg", 7.0))) - with col2: - data["denoise"] = st.number_input("Denoise", value=float(data.get("denoise", 1.0))) - data["sampler_name"] = st.text_input("Sampler", value=data.get("sampler_name", "euler")) - with col3: - data["scheduler"] = st.text_input("Scheduler", value=data.get("scheduler", "normal")) - - # Seed Logic - s_row1, s_row2 = st.columns([3, 1]) - with s_row2: - st.write("") - st.write("") - if st.button("🎲"): - st.session_state.rand_seed = random.randint(0, 999999999999) + with st.expander("🌍 General Prompts (Global Layer)", expanded=False): + gen_prompt = st.text_area("General Prompt", value=data.get("general_prompt", ""), height=100, key=f"{fk}_gp") + gen_negative = st.text_area("General Negative", value=data.get("general_negative", DEFAULTS["general_negative"]), height=100, key=f"{fk}_gn") + + st.write("📝 **Specific Prompts**") + current_prompt_val = data.get("current_prompt", "") + if 'append_prompt' in st.session_state: + current_prompt_val = (current_prompt_val.strip() + ", " + st.session_state.append_prompt).strip(', ') + del st.session_state.append_prompt + + new_prompt = st.text_area("Specific Prompt", value=current_prompt_val, height=150, key=f"{fk}_sp") + new_negative = st.text_area("Specific Negative", value=data.get("negative", ""), height=100, key=f"{fk}_sn") + + # Seed + col_seed_val, col_seed_btn = st.columns([4, 1]) + seed_key = f"{fk}_seed" + + with col_seed_btn: + st.write("") + st.write("") + if st.button("🎲 Randomize", key=f"{fk}_rand"): + st.session_state[seed_key] = random.randint(0, 999999999999) st.rerun() - with s_row1: - current_seed = st.session_state.get('rand_seed', int(data.get("seed", -1))) - val = st.number_input("Seed", value=current_seed) - data["seed"] = val - - # --- ADVANCED SECTIONS --- - with st.expander("🎥 Camera & FLF Settings"): - data["camera"] = st.text_input("Camera Motion", value=data.get("camera", "static")) - data["flf"] = st.number_input("FLF", value=float(data.get("flf", 0.0))) - data["frame_to_skip"] = st.number_input("Frames to Skip (VACE)", value=int(data.get("frame_to_skip", 81))) - data["vace schedule"] = st.number_input("VACE Schedule", value=int(data.get("vace schedule", 1))) - - with st.expander("📂 File Paths"): - data["video file path"] = st.text_input("Video Input Path", value=data.get("video file path", "")) - data["reference image path"] = st.text_input("Reference Image Path", value=data.get("reference image path", "")) - - # --- LORAS (Reverted to plain text to avoid float/string crashes) --- - st.subheader("💊 LoRAs") - l1, l2, l3 = st.columns(3) - - with l1: - data["lora 1 high"] = st.text_input("LoRA 1 Name", value=data.get("lora 1 high", "")) - data["lora 1 low"] = st.text_input("LoRA 1 Strength", value=str(data.get("lora 1 low", ""))) - with l2: - data["lora 2 high"] = st.text_input("LoRA 2 Name", value=data.get("lora 2 high", "")) - data["lora 2 low"] = st.text_input("LoRA 2 Strength", value=str(data.get("lora 2 low", ""))) - with l3: - data["lora 3 high"] = st.text_input("LoRA 3 Name", value=data.get("lora 3 high", "")) - data["lora 3 low"] = st.text_input("LoRA 3 Strength", value=str(data.get("lora 3 low", ""))) - - # --- CUSTOM PARAMETERS --- - st.markdown("---") - st.caption("🔧 Custom Parameters") - - standard_keys = list(DEFAULTS.keys()) + ["history_tree", "prompt_history"] - custom_keys = [k for k in data.keys() if k not in standard_keys] - - if custom_keys: - keys_to_remove = [] - for k in custom_keys: - ck1, ck2, ck3 = st.columns([1, 2, 0.5]) - ck1.text_input("Key", value=k, disabled=True, key=f"ck_lbl_{k}", label_visibility="collapsed") - data[k] = ck2.text_input("Value", value=str(data[k]), key=f"cv_{k}", label_visibility="collapsed") - if ck3.button("🗑️", key=f"cdel_{k}"): - keys_to_remove.append(k) + with col_seed_val: + seed_val = st.session_state.get('rand_seed', int(data.get("seed", 0))) + new_seed = st.number_input("Seed", value=seed_val, step=1, min_value=0, format="%d", key=seed_key) + data["seed"] = new_seed + + # LoRAs + st.subheader("LoRAs") + l_col1, l_col2 = st.columns(2) + loras = {} + lora_keys = ["lora 1 high", "lora 1 low", "lora 2 high", "lora 2 low", "lora 3 high", "lora 3 low"] + for i, k in enumerate(lora_keys): + with (l_col1 if i % 2 == 0 else l_col2): + loras[k] = st.text_input(k.title(), value=data.get(k, ""), key=f"{fk}_{k}") + + # Settings + st.subheader("Settings") + spec_fields = {} + spec_fields["camera"] = st.text_input("Camera", value=str(data.get("camera", DEFAULTS["camera"])), key=f"{fk}_cam") + spec_fields["flf"] = st.text_input("FLF", value=str(data.get("flf", DEFAULTS["flf"])), key=f"{fk}_flf") + + # Explicitly track standard setting keys to exclude them from custom list + standard_keys = { + "general_prompt", "general_negative", "current_prompt", "negative", "prompt", "seed", + "camera", "flf", "batch_data", "prompt_history", "sequence_number", "ui_reset_token", + "model_name", "vae_name", "steps", "cfg", "denoise", "sampler_name", "scheduler" + } + standard_keys.update(lora_keys) + + if "vace" in file_path.name: + vace_keys = ["frame_to_skip", "input_a_frames", "input_b_frames", "reference switch", "vace schedule", "reference path", "video file path", "reference image path"] + standard_keys.update(vace_keys) + + spec_fields["frame_to_skip"] = st.number_input("Frame to Skip", value=int(data.get("frame_to_skip", 81)), key=f"{fk}_fts") + spec_fields["input_a_frames"] = st.number_input("Input A Frames", value=int(data.get("input_a_frames", 0)), key=f"{fk}_ia") + spec_fields["input_b_frames"] = st.number_input("Input B Frames", value=int(data.get("input_b_frames", 0)), key=f"{fk}_ib") + spec_fields["reference switch"] = st.number_input("Reference Switch", value=int(data.get("reference switch", 1)), key=f"{fk}_rsw") + spec_fields["vace schedule"] = st.number_input("VACE Schedule", value=int(data.get("vace schedule", 1)), key=f"{fk}_vsc") + for f in ["reference path", "video file path", "reference image path"]: + spec_fields[f] = st.text_input(f.title(), value=str(data.get(f, "")), key=f"{fk}_{f}") + elif "i2v" in file_path.name: + i2v_keys = ["reference image path", "flf image path", "video file path"] + standard_keys.update(i2v_keys) + + for f in i2v_keys: + spec_fields[f] = st.text_input(f.title(), value=str(data.get(f, "")), key=f"{fk}_{f}") + + # --- CUSTOM PARAMETERS LOGIC --- + st.markdown("---") + st.subheader("🔧 Custom Parameters") + + # Filter keys: Only those NOT in the standard set + custom_keys = [k for k in data.keys() if k not in standard_keys] + + keys_to_remove = [] + + if custom_keys: + for k in custom_keys: + c1, c2, c3 = st.columns([1, 2, 0.5]) + c1.text_input("Key", value=k, disabled=True, key=f"{fk}_ck_lbl_{k}", label_visibility="collapsed") + val = c2.text_input("Value", value=str(data[k]), key=f"{fk}_cv_{k}", label_visibility="collapsed") + data[k] = val + + if c3.button("🗑️", key=f"{fk}_cdel_{k}"): + keys_to_remove.append(k) + else: + st.caption("No custom keys added.") + + # Add New Key Interface + with st.expander("➕ Add New Parameter"): + nk_col, nv_col = st.columns(2) + new_k = nk_col.text_input("Key Name", key=f"{fk}_new_k") + new_v = nv_col.text_input("Value", key=f"{fk}_new_v") + + if st.button("Add Parameter", key=f"{fk}_add_cust"): + if new_k and new_k not in data: + data[new_k] = new_v + st.rerun() + elif new_k in data: + st.error(f"Key '{new_k}' already exists!") + + # Apply Removals if keys_to_remove: - for k in keys_to_remove: del data[k] - save_json(file_path, data) + for k in keys_to_remove: + del data[k] st.rerun() - with st.expander("➕ Add Parameter"): - nk, nv = st.columns(2) - new_k = nk.text_input("New Key") - new_v = nv.text_input("New Value") - if st.button("Add Parameter"): - if new_k and new_k not in data: - data[new_k] = new_v + # --- ACTIONS & HISTORY --- + with col2: + current_state = { + "general_prompt": gen_prompt, "general_negative": gen_negative, + "current_prompt": new_prompt, "negative": new_negative, + "seed": new_seed, **loras, **spec_fields + } + + # MERGE CUSTOM KEYS + for k in custom_keys: + if k not in keys_to_remove: + current_state[k] = data[k] + + st.session_state.single_editor_cache = current_state + + st.subheader("Actions") + current_disk_mtime = get_file_mtime(file_path) + is_conflict = current_disk_mtime > st.session_state.last_mtime + + if is_conflict: + st.error("⚠️ CONFLICT: Disk changed!") + if st.button("Force Save"): + data.update(current_state) + save_json(file_path, data) # No return val in new utils + st.session_state.last_mtime = get_file_mtime(file_path) # Manual Update + st.session_state.data_cache = data + st.toast("Saved!", icon="⚠️") + st.rerun() + if st.button("Reload File"): + st.session_state.loaded_file = None + st.rerun() + else: + if st.button("💾 Update File", use_container_width=True): + data.update(current_state) save_json(file_path, data) + st.session_state.last_mtime = get_file_mtime(file_path) + st.session_state.data_cache = data + st.toast("Updated!", icon="✅") + + st.markdown("---") + archive_note = st.text_input("Archive Note") + if st.button("📦 Snapshot to History", use_container_width=True): + entry = {"note": archive_note if archive_note else "Snapshot", **current_state} + if "prompt_history" not in data: data["prompt_history"] = [] + data["prompt_history"].insert(0, entry) + data.update(entry) + save_json(file_path, data) + st.session_state.last_mtime = get_file_mtime(file_path) + st.session_state.data_cache = data + st.toast("Archived!", icon="📦") st.rerun() - # --- SAVE ACTIONS --- - st.markdown("---") - c_save, c_snap = st.columns([1, 2]) - - with c_save: - if st.button("💾 Save Changes", use_container_width=True): - save_json(file_path, data) - st.toast("Saved!", icon="✅") - - with c_snap: - with st.popover("📸 Save Snapshot (History)", use_container_width=True): - note = st.text_input("Snapshot Note", placeholder="e.g. Changed lighting") - if st.button("Confirm Snapshot"): - tree_data = data.get("history_tree", {}) - htree = HistoryTree(tree_data) - - snapshot = data.copy() - if "history_tree" in snapshot: del snapshot["history_tree"] - - htree.commit(snapshot, note=note if note else "Manual Snapshot") - data["history_tree"] = htree.to_dict() - - save_json(file_path, data) - st.toast("Snapshot Saved!", icon="📸") + # --- FULL HISTORY PANEL --- + st.markdown("---") + st.subheader("History") + history = data.get("prompt_history", []) + + if not history: + st.caption("No history yet.") + + for idx, h in enumerate(history): + note = h.get('note', 'No Note') + + with st.container(): + if st.session_state.edit_history_idx == idx: + with st.expander(f"📝 Editing: {note}", expanded=True): + edit_note = st.text_input("Note", value=note, key=f"h_en_{idx}") + edit_seed = st.number_input("Seed", value=int(h.get('seed', 0)), key=f"h_es_{idx}") + edit_gp = st.text_area("General P", value=h.get('general_prompt', ''), height=60, key=f"h_egp_{idx}") + edit_gn = st.text_area("General N", value=h.get('general_negative', ''), height=60, key=f"h_egn_{idx}") + edit_sp = st.text_area("Specific P", value=h.get('prompt', ''), height=100, key=f"h_esp_{idx}") + edit_sn = st.text_area("Specific N", value=h.get('negative', ''), height=60, key=f"h_esn_{idx}") + + hc1, hc2 = st.columns([1, 4]) + if hc1.button("💾 Save", key=f"h_save_{idx}"): + h.update({ + 'note': edit_note, 'seed': edit_seed, + 'general_prompt': edit_gp, 'general_negative': edit_gn, + 'prompt': edit_sp, 'negative': edit_sn + }) + save_json(file_path, data) + st.session_state.last_mtime = get_file_mtime(file_path) + st.session_state.data_cache = data + st.session_state.edit_history_idx = None + st.rerun() + if hc2.button("Cancel", key=f"h_can_{idx}"): + st.session_state.edit_history_idx = None + st.rerun() + + else: + with st.expander(f"#{idx+1}: {note}"): + st.caption(f"Seed: {h.get('seed', 0)}") + st.text(f"SPEC: {h.get('prompt', '')[:40]}...") + + view_data = {k:v for k,v in h.items() if k not in ['prompt', 'negative', 'general_prompt', 'general_negative', 'note']} + st.json(view_data, expanded=False) + + bh1, bh2, bh3 = st.columns([2, 1, 1]) + + if bh1.button("Restore", key=f"h_rest_{idx}", use_container_width=True): + data.update(h) + if 'prompt' in h: data['current_prompt'] = h['prompt'] + save_json(file_path, data) + st.session_state.last_mtime = get_file_mtime(file_path) + st.session_state.data_cache = data + + # Refresh UI + st.session_state.ui_reset_token += 1 + + st.toast("Restored!", icon="⏪") + st.rerun() + + if bh2.button("✏️", key=f"h_edit_{idx}"): + st.session_state.edit_history_idx = idx + st.rerun() + + if bh3.button("🗑️", key=f"h_del_{idx}"): + history.pop(idx) + save_json(file_path, data) + st.session_state.last_mtime = get_file_mtime(file_path) + st.session_state.data_cache = data + st.rerun()