Update tab_single.py
This commit is contained in:
344
tab_single.py
344
tab_single.py
@@ -1,243 +1,169 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
import random
|
import random
|
||||||
from utils import DEFAULTS, save_json, get_file_mtime
|
import json
|
||||||
|
from utils import DEFAULTS, save_json, get_file_mtime, render_smart_input
|
||||||
|
from history_tree import HistoryTree
|
||||||
|
|
||||||
def render_single_editor(data, file_path):
|
def render_single_editor(data, file_path):
|
||||||
is_batch_file = "batch_data" in data or isinstance(data, list)
|
is_batch_file = "batch_data" in data or isinstance(data, list)
|
||||||
|
|
||||||
if is_batch_file:
|
if is_batch_file:
|
||||||
st.info("This is a batch file. Switch to the 'Batch Processor' tab.")
|
st.warning("⚠️ This file looks like a Batch file. Please switch to the 'Batch Processor' tab.")
|
||||||
return
|
return
|
||||||
|
|
||||||
col1, col2 = st.columns([2, 1])
|
# 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.")
|
||||||
|
|
||||||
# Unique prefix for this file's widgets + Version Token (Fixes Restore bug)
|
# --- TOP ROW: MODELS (SMART INPUTS) ---
|
||||||
fk = f"{file_path.name}_v{st.session_state.ui_reset_token}"
|
st.subheader("🤖 Models")
|
||||||
|
c1, c2 = st.columns(2)
|
||||||
|
|
||||||
# --- FORM ---
|
# Access metadata from session state
|
||||||
with col1:
|
meta = st.session_state.get("comfy_meta", {})
|
||||||
with st.expander("🌍 General Prompts (Global Layer)", expanded=False):
|
ckpts = meta.get("checkpoints", [])
|
||||||
gen_prompt = st.text_area("General Prompt", value=data.get("general_prompt", ""), height=100, key=f"{fk}_gp")
|
vaes = meta.get("vaes", [])
|
||||||
gen_negative = st.text_area("General Negative", value=data.get("general_negative", DEFAULTS["general_negative"]), height=100, key=f"{fk}_gn")
|
|
||||||
|
with c1:
|
||||||
|
data["model_name"] = render_smart_input(
|
||||||
|
"Checkpoint", "s_model", data.get("model_name", ""), ckpts
|
||||||
|
)
|
||||||
|
with c2:
|
||||||
|
data["vae_name"] = render_smart_input(
|
||||||
|
"VAE", "s_vae", data.get("vae_name", ""), vaes
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- PROMPTS ---
|
||||||
|
st.markdown("---")
|
||||||
|
st.subheader("📝 Prompts")
|
||||||
|
|
||||||
st.write("📝 **Specific Prompts**")
|
|
||||||
current_prompt_val = data.get("current_prompt", "")
|
|
||||||
if 'append_prompt' in st.session_state:
|
if 'append_prompt' in st.session_state:
|
||||||
current_prompt_val = (current_prompt_val.strip() + ", " + st.session_state.append_prompt).strip(', ')
|
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
|
del st.session_state.append_prompt
|
||||||
|
|
||||||
new_prompt = st.text_area("Specific Prompt", value=current_prompt_val, height=150, key=f"{fk}_sp")
|
data["positive_prompt"] = st.text_area("Positive Prompt", value=data.get("positive_prompt", ""), height=150)
|
||||||
new_negative = st.text_area("Specific Negative", value=data.get("negative", ""), height=100, key=f"{fk}_sn")
|
data["negative_prompt"] = st.text_area("Negative Prompt", value=data.get("negative_prompt", ""), height=100)
|
||||||
|
|
||||||
# Seed
|
# --- MAIN SETTINGS ---
|
||||||
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.markdown("---")
|
||||||
st.subheader("🔧 Custom Parameters")
|
st.subheader("⚙️ Settings")
|
||||||
|
|
||||||
# Filter keys: Only those NOT in the standard set
|
col1, col2, col3 = st.columns(3)
|
||||||
|
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)
|
||||||
|
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 (SMART INPUTS) ---
|
||||||
|
st.subheader("💊 LoRAs")
|
||||||
|
lora_list = meta.get("loras", [])
|
||||||
|
|
||||||
|
l1, l2 = st.columns(2)
|
||||||
|
|
||||||
|
def lora_row(col, num):
|
||||||
|
with col:
|
||||||
|
st.caption(f"LoRA {num}")
|
||||||
|
k_high = f"lora {num} high"
|
||||||
|
k_low = f"lora {num} low"
|
||||||
|
|
||||||
|
# SMART INPUT for Name
|
||||||
|
data[k_high] = render_smart_input(
|
||||||
|
"Model", f"s_l{num}_h", data.get(k_high, ""), lora_list
|
||||||
|
)
|
||||||
|
# Slider for Strength
|
||||||
|
try:
|
||||||
|
val = float(data.get(k_low, 1.0))
|
||||||
|
except:
|
||||||
|
val = 1.0
|
||||||
|
data[k_low] = st.slider("Strength", 0.0, 2.0, val, 0.05, key=f"s_l{num}_l")
|
||||||
|
|
||||||
|
lora_row(l1, 1)
|
||||||
|
lora_row(l2, 2)
|
||||||
|
lora_row(l1, 3)
|
||||||
|
|
||||||
|
# --- 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]
|
custom_keys = [k for k in data.keys() if k not in standard_keys]
|
||||||
|
|
||||||
keys_to_remove = []
|
|
||||||
|
|
||||||
if custom_keys:
|
if custom_keys:
|
||||||
|
keys_to_remove = []
|
||||||
for k in custom_keys:
|
for k in custom_keys:
|
||||||
c1, c2, c3 = st.columns([1, 2, 0.5])
|
ck1, ck2, ck3 = st.columns([1, 2, 0.5])
|
||||||
c1.text_input("Key", value=k, disabled=True, key=f"{fk}_ck_lbl_{k}", label_visibility="collapsed")
|
ck1.text_input("Key", value=k, disabled=True, key=f"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] = ck2.text_input("Value", value=str(data[k]), key=f"cv_{k}", label_visibility="collapsed")
|
||||||
data[k] = val
|
if ck3.button("🗑️", key=f"cdel_{k}"):
|
||||||
|
|
||||||
if c3.button("🗑️", key=f"{fk}_cdel_{k}"):
|
|
||||||
keys_to_remove.append(k)
|
keys_to_remove.append(k)
|
||||||
else:
|
|
||||||
st.caption("No custom keys added.")
|
|
||||||
|
|
||||||
# Add New Key Interface
|
if keys_to_remove:
|
||||||
with st.expander("➕ Add New Parameter"):
|
for k in keys_to_remove: del data[k]
|
||||||
nk_col, nv_col = st.columns(2)
|
save_json(file_path, data)
|
||||||
new_k = nk_col.text_input("Key Name", key=f"{fk}_new_k")
|
st.rerun()
|
||||||
new_v = nv_col.text_input("Value", key=f"{fk}_new_v")
|
|
||||||
|
|
||||||
if st.button("Add Parameter", key=f"{fk}_add_cust"):
|
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:
|
if new_k and new_k not in data:
|
||||||
data[new_k] = new_v
|
data[new_k] = new_v
|
||||||
st.rerun()
|
save_json(file_path, data)
|
||||||
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()
|
st.rerun()
|
||||||
|
|
||||||
# --- ACTIONS & HISTORY ---
|
# --- SAVE ACTIONS ---
|
||||||
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("---")
|
st.markdown("---")
|
||||||
archive_note = st.text_input("Archive Note")
|
c_save, c_snap = st.columns([1, 2])
|
||||||
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 ---
|
with c_save:
|
||||||
st.markdown("---")
|
if st.button("💾 Save Changes", use_container_width=True):
|
||||||
st.subheader("History")
|
save_json(file_path, data)
|
||||||
history = data.get("prompt_history", [])
|
st.toast("Saved!", icon="✅")
|
||||||
|
|
||||||
if not history:
|
with c_snap:
|
||||||
st.caption("No history yet.")
|
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"):
|
||||||
|
# Commit to History Tree
|
||||||
|
tree_data = data.get("history_tree", {})
|
||||||
|
htree = HistoryTree(tree_data)
|
||||||
|
|
||||||
for idx, h in enumerate(history):
|
snapshot = data.copy()
|
||||||
note = h.get('note', 'No Note')
|
if "history_tree" in snapshot: del snapshot["history_tree"]
|
||||||
|
|
||||||
with st.container():
|
htree.commit(snapshot, note=note if note else "Manual Snapshot")
|
||||||
if st.session_state.edit_history_idx == idx:
|
data["history_tree"] = htree.to_dict()
|
||||||
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])
|
save_json(file_path, data)
|
||||||
if hc1.button("💾 Save", key=f"h_save_{idx}"):
|
st.toast("Snapshot Saved!", icon="📸")
|
||||||
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()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user