Refactor single editor UI and logic for better clarity and functionality. Update button labels, streamline custom parameters handling, and remove unused imports.
244 lines
12 KiB
Python
244 lines
12 KiB
Python
import streamlit as st
|
||
import random
|
||
from utils import DEFAULTS, save_json, get_file_mtime
|
||
|
||
def render_single_editor(data, file_path):
|
||
is_batch_file = "batch_data" in data or isinstance(data, list)
|
||
|
||
if is_batch_file:
|
||
st.info("This is a batch file. Switch to the 'Batch Processor' tab.")
|
||
return
|
||
|
||
col1, col2 = st.columns([2, 1])
|
||
|
||
# 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:
|
||
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 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"
|
||
}
|
||
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]
|
||
st.rerun()
|
||
|
||
# --- 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)
|
||
st.session_state.last_mtime = save_json(file_path, data)
|
||
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)
|
||
st.session_state.last_mtime = save_json(file_path, data)
|
||
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)
|
||
st.session_state.last_mtime = save_json(file_path, data)
|
||
st.session_state.data_cache = data
|
||
st.toast("Archived!", icon="📦")
|
||
st.rerun()
|
||
|
||
# --- 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
|
||
})
|
||
st.session_state.last_mtime = save_json(file_path, data)
|
||
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']
|
||
st.session_state.last_mtime = save_json(file_path, data)
|
||
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)
|
||
st.session_state.last_mtime = save_json(file_path, data)
|
||
st.session_state.data_cache = data
|
||
st.rerun()
|