diff --git a/README.md b/README.md
index 2e7bb07..ef21b79 100644
--- a/README.md
+++ b/README.md
@@ -38,19 +38,6 @@ A visual dashboard for managing, versioning, and batch-processing JSON configura
-
-
-Single File Editor
-
-
-- Visual editing of Prompts, Seeds, LoRAs, Camera, FLF, VACE params
-- Custom key-value parameters that persist and flow to ComfyUI
-- Conflict protection against external file modifications
-- Snippet library for reusable prompt fragments
-
- |
-
-
Batch Processor
@@ -60,6 +47,8 @@ Batch Processor
- Import settings from any file or history entry
- Per-shot custom keys (e.g. Shot 1: `fog: 0.5`, Shot 2: `fog: 0.0`)
- Clone, reorder, and manage sequences visually
+- Conflict protection against external file modifications
+- Snippet library for reusable prompt fragments
|
@@ -329,7 +318,6 @@ ComfyUI-JSON-Manager/
├── app.py # Streamlit main entry point & navigator
├── utils.py # I/O, config, defaults, case-insensitive path resolver
├── history_tree.py # Git-style branching engine
-├── tab_single.py # Single file editor UI
├── tab_batch.py # Batch processor UI
├── tab_timeline.py # Visual timeline UI
├── tab_comfy.py # ComfyUI server monitor
diff --git a/app.py b/app.py
index 79a3753..29685e0 100644
--- a/app.py
+++ b/app.py
@@ -9,7 +9,6 @@ from utils import (
KEY_BATCH_DATA, KEY_PROMPT_HISTORY, KEY_SEQUENCE_NUMBER,
resolve_path_case_insensitive,
)
-from tab_single import render_single_editor
from tab_batch import render_batch_processor
from tab_timeline import render_timeline_tab
from tab_comfy import render_comfy_monitor
@@ -28,9 +27,8 @@ _SESSION_DEFAULTS = {
"loaded_file": lambda: None,
"last_mtime": lambda: 0,
"edit_history_idx": lambda: None,
- "single_editor_cache": lambda: DEFAULTS.copy(),
"ui_reset_token": lambda: 0,
- "active_tab_name": lambda: "📝 Single Editor",
+ "active_tab_name": lambda: "🚀 Batch Processor",
}
if 'config' not in st.session_state:
@@ -187,11 +185,7 @@ if selected_file_name:
st.session_state.edit_history_idx = None
# --- AUTO-SWITCH TAB LOGIC ---
- is_batch = KEY_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"
+ st.session_state.active_tab_name = "🚀 Batch Processor"
else:
data = st.session_state.data_cache
@@ -201,7 +195,6 @@ if selected_file_name:
# --- CONTROLLED NAVIGATION ---
# Removed "🔌 Comfy Monitor" from this list
tabs_list = [
- "📝 Single Editor",
"🚀 Batch Processor",
"🕒 Timeline",
"💻 Raw Editor"
@@ -221,10 +214,7 @@ if selected_file_name:
st.markdown("---")
# --- RENDER EDITOR TABS ---
- if current_tab == "📝 Single Editor":
- render_single_editor(data, file_path)
-
- elif current_tab == "🚀 Batch Processor":
+ if current_tab == "🚀 Batch Processor":
render_batch_processor(data, file_path, json_files, st.session_state.current_dir, selected_file_name)
elif current_tab == "🕒 Timeline":
diff --git a/tab_single.py b/tab_single.py
deleted file mode 100644
index fd3afff..0000000
--- a/tab_single.py
+++ /dev/null
@@ -1,250 +0,0 @@
-import streamlit as st
-import random
-from utils import DEFAULTS, save_json, get_file_mtime, KEY_BATCH_DATA, KEY_PROMPT_HISTORY, KEY_SEQUENCE_NUMBER
-
-def render_single_editor(data, file_path):
- is_batch_file = KEY_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=250, 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", KEY_BATCH_DATA, KEY_PROMPT_HISTORY, KEY_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", 16)), key=f"{fk}_ia")
- spec_fields["input_b_frames"] = st.number_input("Input B Frames", value=int(data.get("input_b_frames", 16)), 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)
- 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 KEY_PROMPT_HISTORY not in data: data[KEY_PROMPT_HISTORY] = []
- data[KEY_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()
-
- # --- FULL HISTORY PANEL ---
- st.markdown("---")
- st.subheader("History")
- history = data.get(KEY_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()