Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c5d2fc4e0 | |||
| e6d260eb1a | |||
| 8dafee9f6d | |||
| b405427a6b | |||
| c771fa3451 | |||
| 4b40d0f50c | |||
| 97c755316b | |||
| d1e32e5fc4 |
+32
-2
@@ -13,7 +13,7 @@ from fastapi.responses import FileResponse
|
|||||||
from nicegui import app
|
from nicegui import app
|
||||||
|
|
||||||
from db import ProjectDB
|
from db import ProjectDB
|
||||||
from utils import load_json, load_config, KEY_BATCH_DATA, KEY_SEQUENCE_NUMBER
|
from utils import load_json, load_config, resolve_path_case_insensitive, KEY_BATCH_DATA, KEY_SEQUENCE_NUMBER
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -58,7 +58,17 @@ def _get_project(name: str) -> dict[str, Any]:
|
|||||||
proj = db.get_project(name)
|
proj = db.get_project(name)
|
||||||
if not proj:
|
if not proj:
|
||||||
raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
|
raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
|
||||||
return {"name": proj["name"], "folder_path": proj["folder_path"],
|
folder_path = proj["folder_path"]
|
||||||
|
resolved = resolve_path_case_insensitive(folder_path)
|
||||||
|
if resolved:
|
||||||
|
folder_path = str(resolved)
|
||||||
|
# Apply configured path replacements (e.g. Docker mount casing differences)
|
||||||
|
config = load_config()
|
||||||
|
for rep in config.get("path_replacements", []):
|
||||||
|
src, dst = rep.get("from", ""), rep.get("to", "")
|
||||||
|
if src:
|
||||||
|
folder_path = folder_path.replace(src, dst)
|
||||||
|
return {"name": proj["name"], "folder_path": folder_path,
|
||||||
"description": proj.get("description", "")}
|
"description": proj.get("description", "")}
|
||||||
|
|
||||||
|
|
||||||
@@ -94,6 +104,17 @@ def _get_data(name: str, file_name: str, seq: int = Query(default=1)) -> dict[st
|
|||||||
if match is None:
|
if match is None:
|
||||||
raise HTTPException(status_code=404, detail=f"Sequence {seq} not found")
|
raise HTTPException(status_code=404, detail=f"Sequence {seq} not found")
|
||||||
result = dict(match)
|
result = dict(match)
|
||||||
|
# Inject strength defaults if not yet saved to JSON
|
||||||
|
for key, default in (
|
||||||
|
("start frame high strength", 1.0),
|
||||||
|
("start frame low strength", 1.0),
|
||||||
|
("middle frame high strength", 1.0),
|
||||||
|
("middle frame low strength", 1.0),
|
||||||
|
("end frame high strength", 1.0),
|
||||||
|
("end frame low strength", 1.0),
|
||||||
|
):
|
||||||
|
result.setdefault(key, default)
|
||||||
|
# Computed stem names from frame paths
|
||||||
for out_key, src_key in (
|
for out_key, src_key in (
|
||||||
("start_name", "start frame path"),
|
("start_name", "start frame path"),
|
||||||
("middle_name", "middle frame path"),
|
("middle_name", "middle frame path"),
|
||||||
@@ -124,6 +145,15 @@ def _get_keys(name: str, file_name: str, seq: int = Query(default=1)) -> dict[st
|
|||||||
types.append("FLOAT")
|
types.append("FLOAT")
|
||||||
else:
|
else:
|
||||||
types.append("STRING")
|
types.append("STRING")
|
||||||
|
# Injected defaults — always present even if not yet saved to JSON
|
||||||
|
for key in (
|
||||||
|
"start frame high strength", "start frame low strength",
|
||||||
|
"middle frame high strength", "middle frame low strength",
|
||||||
|
"end frame high strength", "end frame low strength",
|
||||||
|
):
|
||||||
|
if key not in match:
|
||||||
|
keys.append(key)
|
||||||
|
types.append("FLOAT")
|
||||||
# Computed keys derived from frame paths
|
# Computed keys derived from frame paths
|
||||||
for out_key, src_key in (
|
for out_key, src_key in (
|
||||||
("start_name", "start frame path"),
|
("start_name", "start frame path"),
|
||||||
|
|||||||
+23
-21
@@ -314,12 +314,12 @@ def render_batch_processor(state: AppState):
|
|||||||
'lora 3 high', 'lora 3 high strength', 'lora 3 low', 'lora 3 low strength']
|
'lora 3 high', 'lora 3 high strength', 'lora 3 low', 'lora 3 low strength']
|
||||||
standard_keys = {
|
standard_keys = {
|
||||||
'name', 'mode', 'general_prompt', 'general_negative', 'current_prompt', 'negative', 'prompt',
|
'name', 'mode', 'general_prompt', 'general_negative', 'current_prompt', 'negative', 'prompt',
|
||||||
'seed', 'cfg', 'camera', 'flf', KEY_SEQUENCE_NUMBER,
|
'seed', 'camera', KEY_SEQUENCE_NUMBER,
|
||||||
'frame_to_skip', 'end_frame', 'logic index', 'transition', 'vace_length',
|
'frame_to_skip', 'logic index', 'transition', 'vace_length',
|
||||||
'input_a_frames', 'input_b_frames', 'reference switch', 'vace schedule',
|
'input_a_frames', 'input_b_frames', 'reference switch', 'vace schedule',
|
||||||
'start frame path', 'start frame strength',
|
'start frame path', 'start frame high strength', 'start frame low strength',
|
||||||
'middle frame path', 'middle frame strength',
|
'middle frame path', 'middle frame high strength', 'middle frame low strength',
|
||||||
'end frame path', 'end frame strength',
|
'end frame path', 'end frame high strength', 'end frame low strength',
|
||||||
'video file path',
|
'video file path',
|
||||||
}
|
}
|
||||||
standard_keys.update(lora_keys)
|
standard_keys.update(lora_keys)
|
||||||
@@ -560,15 +560,17 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state,
|
|||||||
|
|
||||||
# --- Frame paths (start / middle / end) ---
|
# --- Frame paths (start / middle / end) ---
|
||||||
logic_val = int(seq.get('logic index', 0))
|
logic_val = int(seq.get('logic index', 0))
|
||||||
for bit, img_label, img_key, str_key in [
|
for bit, img_label, img_key, hi_key, lo_key in [
|
||||||
(0, 'Start Frame', 'start frame path', 'start frame strength'),
|
(0, 'Start Frame', 'start frame path', 'start frame high strength', 'start frame low strength'),
|
||||||
(1, 'Middle Frame', 'middle frame path', 'middle frame strength'),
|
(1, 'Middle Frame', 'middle frame path', 'middle frame high strength', 'middle frame low strength'),
|
||||||
(2, 'End Frame', 'end frame path', 'end frame strength'),
|
(2, 'End Frame', 'end frame path', 'end frame high strength', 'end frame low strength'),
|
||||||
]:
|
]:
|
||||||
ui.label(img_label).classes('text-caption text-weight-bold q-mt-sm')
|
ui.label(img_label).classes('text-caption text-weight-bold q-mt-sm')
|
||||||
|
is_on = bool((logic_val >> bit) & 1)
|
||||||
with ui.row().classes('w-full items-center no-wrap q-mt-xs'):
|
with ui.row().classes('w-full items-center no-wrap q-mt-xs'):
|
||||||
inp = dict_input(ui.input, 'Path', seq, img_key).classes(
|
inp = dict_input(ui.input, 'Path', seq, img_key).classes(
|
||||||
'col').props('outlined dense input-style="text-align: right"')
|
'col').props('outlined dense input-style="text-align: right"')
|
||||||
|
thumb = None
|
||||||
img_path = Path(seq.get(img_key, '')) if seq.get(img_key) else None
|
img_path = Path(seq.get(img_key, '')) if seq.get(img_key) else None
|
||||||
if (img_path and img_path.exists() and
|
if (img_path and img_path.exists() and
|
||||||
img_path.suffix.lower() in IMAGE_EXTENSIONS):
|
img_path.suffix.lower() in IMAGE_EXTENSIONS):
|
||||||
@@ -576,16 +578,22 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state,
|
|||||||
with ui.dialog() as img_dlg, ui.card().style('max-width:90vw; padding:0'):
|
with ui.dialog() as img_dlg, ui.card().style('max-width:90vw; padding:0'):
|
||||||
ui.html(f'<img src="{img_url}" '
|
ui.html(f'<img src="{img_url}" '
|
||||||
f'style="max-width:80vw;max-height:80vh;display:block">')
|
f'style="max-width:80vw;max-height:80vh;display:block">')
|
||||||
ui.html(
|
thumb = ui.html(
|
||||||
f'<img src="{img_url}" '
|
f'<img src="{img_url}" '
|
||||||
f'style="width:36px;height:36px;object-fit:cover;'
|
f'style="width:36px;height:36px;object-fit:cover;'
|
||||||
f'border-radius:4px;cursor:pointer;flex-shrink:0">'
|
f'border-radius:4px;cursor:pointer;flex-shrink:0;'
|
||||||
|
f'opacity:{"1.0" if is_on else "0.25"}">'
|
||||||
).on('click', img_dlg.open)
|
).on('click', img_dlg.open)
|
||||||
str_inp = dict_number('Strength', seq, str_key, default=1.0,
|
sw = ui.switch(value=is_on)
|
||||||
step=0.05, format='%.2f').style(
|
|
||||||
'width:80px').props('outlined dense')
|
|
||||||
sw = ui.switch(value=bool((logic_val >> bit) & 1))
|
|
||||||
frame_switches.append(sw)
|
frame_switches.append(sw)
|
||||||
|
if thumb is not None:
|
||||||
|
sw.on('update:model-value',
|
||||||
|
lambda e, t=thumb, s=sw: t.style(f'opacity: {"1.0" if s.value else "0.25"}'))
|
||||||
|
with ui.row().classes('w-full no-wrap q-mt-xs q-gutter-xs'):
|
||||||
|
dict_number('High', seq, hi_key, default=1.0,
|
||||||
|
step=0.05, format='%.2f').classes('col').props('outlined dense')
|
||||||
|
dict_number('Low', seq, lo_key, default=1.0,
|
||||||
|
step=0.05, format='%.2f').classes('col').props('outlined dense')
|
||||||
|
|
||||||
with splitter.after:
|
with splitter.after:
|
||||||
# Mode
|
# Mode
|
||||||
@@ -610,13 +618,7 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state,
|
|||||||
|
|
||||||
ui.button(icon='casino', on_click=randomize_seed).props('flat')
|
ui.button(icon='casino', on_click=randomize_seed).props('flat')
|
||||||
|
|
||||||
# CFG
|
|
||||||
dict_number('CFG', seq, 'cfg', default=DEFAULTS['cfg'],
|
|
||||||
step=0.5, format='%.1f').props('outlined').classes('w-full')
|
|
||||||
|
|
||||||
dict_input(ui.input, 'Camera', seq, 'camera').props('outlined').classes('w-full')
|
dict_input(ui.input, 'Camera', seq, 'camera').props('outlined').classes('w-full')
|
||||||
dict_input(ui.input, 'FLF', seq, 'flf').props('outlined').classes('w-full')
|
|
||||||
ef_input = dict_number('End Frame', seq, 'end_frame').props('outlined').classes('w-full')
|
|
||||||
seq.setdefault('logic index', 0)
|
seq.setdefault('logic index', 0)
|
||||||
li_input = dict_number('Logic Index', seq, 'logic index').props('outlined readonly').classes('w-full')
|
li_input = dict_number('Logic Index', seq, 'logic index').props('outlined readonly').classes('w-full')
|
||||||
with li_input:
|
with li_input:
|
||||||
|
|||||||
@@ -59,6 +59,48 @@ def render_projects_tab(state: AppState):
|
|||||||
|
|
||||||
ui.button('Create Project', icon='add', on_click=create_project).classes('w-full')
|
ui.button('Create Project', icon='add', on_click=create_project).classes('w-full')
|
||||||
|
|
||||||
|
# --- Path replacements (for ComfyUI Docker path differences) ---
|
||||||
|
with ui.card().classes('w-full q-pa-md q-mb-md'):
|
||||||
|
ui.label('ComfyUI Path Replacements').classes('section-header')
|
||||||
|
ui.label('Applied to project_path output — use to fix Docker mount casing differences.'
|
||||||
|
).classes('text-caption q-mb-sm')
|
||||||
|
|
||||||
|
replacements: list[dict] = state.config.get('path_replacements', [])
|
||||||
|
|
||||||
|
@ui.refreshable
|
||||||
|
def render_replacements():
|
||||||
|
for idx, rep in enumerate(replacements):
|
||||||
|
with ui.row().classes('w-full items-center no-wrap q-gutter-xs'):
|
||||||
|
ui.input('From', value=rep.get('from', '')).classes('col').props(
|
||||||
|
'outlined dense').on('update:model-value',
|
||||||
|
lambda e, i=idx: _update_replacement(i, 'from', e.args))
|
||||||
|
ui.label('→').classes('text-caption')
|
||||||
|
ui.input('To', value=rep.get('to', '')).classes('col').props(
|
||||||
|
'outlined dense').on('update:model-value',
|
||||||
|
lambda e, i=idx: _update_replacement(i, 'to', e.args))
|
||||||
|
ui.button(icon='delete', on_click=lambda i=idx: _remove_replacement(i)
|
||||||
|
).props('flat dense color=negative')
|
||||||
|
|
||||||
|
def _update_replacement(idx, field, value):
|
||||||
|
replacements[idx][field] = value
|
||||||
|
state.config['path_replacements'] = replacements
|
||||||
|
save_config(state.current_dir, state.config.get('favorites', []), state.config)
|
||||||
|
|
||||||
|
def _remove_replacement(idx):
|
||||||
|
replacements.pop(idx)
|
||||||
|
state.config['path_replacements'] = replacements
|
||||||
|
save_config(state.current_dir, state.config.get('favorites', []), state.config)
|
||||||
|
render_replacements.refresh()
|
||||||
|
|
||||||
|
def _add_replacement():
|
||||||
|
replacements.append({'from': '', 'to': ''})
|
||||||
|
state.config['path_replacements'] = replacements
|
||||||
|
save_config(state.current_dir, state.config.get('favorites', []), state.config)
|
||||||
|
render_replacements.refresh()
|
||||||
|
|
||||||
|
render_replacements()
|
||||||
|
ui.button('Add Replacement', icon='add', on_click=_add_replacement).props('flat dense')
|
||||||
|
|
||||||
# --- Active project indicator ---
|
# --- Active project indicator ---
|
||||||
# Fetch once with file counts and reuse in render_project_list
|
# Fetch once with file counts and reuse in render_project_list
|
||||||
_cached_projects = state.db.list_projects_with_file_counts()
|
_cached_projects = state.db.list_projects_with_file_counts()
|
||||||
|
|||||||
+1
-2
@@ -577,7 +577,6 @@ def _render_preview_fields(item_data: dict):
|
|||||||
|
|
||||||
with ui.row().classes('w-full q-gutter-md'):
|
with ui.row().classes('w-full q-gutter-md'):
|
||||||
ui.input('Camera', value=str(item_data.get('camera', 'static'))).props('readonly outlined')
|
ui.input('Camera', value=str(item_data.get('camera', 'static'))).props('readonly outlined')
|
||||||
ui.input('FLF', value=str(item_data.get('flf', '0.0'))).props('readonly outlined')
|
|
||||||
ui.input('Seed', value=str(item_data.get('seed', '-1'))).props('readonly outlined')
|
ui.input('Seed', value=str(item_data.get('seed', '-1'))).props('readonly outlined')
|
||||||
|
|
||||||
with ui.expansion('LoRA Configuration'):
|
with ui.expansion('LoRA Configuration'):
|
||||||
@@ -617,7 +616,7 @@ def _render_preview_fields(item_data: dict):
|
|||||||
|
|
||||||
known_keys = {
|
known_keys = {
|
||||||
'sequence_number', 'general_prompt', 'general_negative', 'current_prompt', 'prompt',
|
'sequence_number', 'general_prompt', 'general_negative', 'current_prompt', 'prompt',
|
||||||
'negative', 'camera', 'flf', 'seed', 'resolutions',
|
'negative', 'camera', 'seed', 'resolutions',
|
||||||
'frame_to_skip', 'vace schedule', 'video file path', 'middle frame path', 'end frame path', 'start frame path',
|
'frame_to_skip', 'vace schedule', 'video file path', 'middle frame path', 'end frame path', 'start frame path',
|
||||||
'logic index',
|
'logic index',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,16 +28,13 @@ DEFAULTS = {
|
|||||||
"current_prompt": "",
|
"current_prompt": "",
|
||||||
"negative": "",
|
"negative": "",
|
||||||
"seed": -1,
|
"seed": -1,
|
||||||
"cfg": 1.5,
|
|
||||||
|
|
||||||
# --- Settings ---
|
# --- Settings ---
|
||||||
"mode": 0,
|
"mode": 0,
|
||||||
"camera": "static",
|
"camera": "static",
|
||||||
"flf": 0.0,
|
|
||||||
|
|
||||||
# --- I2V / VACE Specifics ---
|
# --- I2V / VACE Specifics ---
|
||||||
"frame_to_skip": 81,
|
"frame_to_skip": 81,
|
||||||
"end_frame": 0,
|
|
||||||
"logic index": 0,
|
"logic index": 0,
|
||||||
"transition": "1-2",
|
"transition": "1-2",
|
||||||
"vace_length": 49,
|
"vace_length": 49,
|
||||||
@@ -47,11 +44,14 @@ DEFAULTS = {
|
|||||||
"reference switch": 1,
|
"reference switch": 1,
|
||||||
"video file path": "",
|
"video file path": "",
|
||||||
"start frame path": "",
|
"start frame path": "",
|
||||||
"start frame strength": 1.0,
|
"start frame high strength": 1.0,
|
||||||
|
"start frame low strength": 1.0,
|
||||||
"middle frame path": "",
|
"middle frame path": "",
|
||||||
"middle frame strength": 1.0,
|
"middle frame high strength": 1.0,
|
||||||
|
"middle frame low strength": 1.0,
|
||||||
"end frame path": "",
|
"end frame path": "",
|
||||||
"end frame strength": 1.0,
|
"end frame high strength": 1.0,
|
||||||
|
"end frame low strength": 1.0,
|
||||||
|
|
||||||
# --- LoRAs (name as STRING, strength as FLOAT) ---
|
# --- LoRAs (name as STRING, strength as FLOAT) ---
|
||||||
"lora 1 high": "",
|
"lora 1 high": "",
|
||||||
@@ -154,6 +154,17 @@ def save_snippets(snippets):
|
|||||||
json.dump(snippets, f, indent=4)
|
json.dump(snippets, f, indent=4)
|
||||||
os.replace(tmp, SNIPPETS_FILE)
|
os.replace(tmp, SNIPPETS_FILE)
|
||||||
|
|
||||||
|
_REMOVED_KEYS = {"cfg", "flf", "end_frame"}
|
||||||
|
|
||||||
|
def _migrate_remove_keys(data: dict) -> None:
|
||||||
|
"""Drop keys that have been removed from the schema."""
|
||||||
|
for item in data.get(KEY_BATCH_DATA, []):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
for k in _REMOVED_KEYS:
|
||||||
|
item.pop(k, None)
|
||||||
|
|
||||||
|
|
||||||
def _migrate_key_renames(data: dict) -> None:
|
def _migrate_key_renames(data: dict) -> None:
|
||||||
"""Rename legacy keys to their current names."""
|
"""Rename legacy keys to their current names."""
|
||||||
for item in data.get(KEY_BATCH_DATA, []):
|
for item in data.get(KEY_BATCH_DATA, []):
|
||||||
@@ -165,6 +176,13 @@ def _migrate_key_renames(data: dict) -> None:
|
|||||||
item['end frame path'] = item.pop('flf image path')
|
item['end frame path'] = item.pop('flf image path')
|
||||||
if 'reference image path' in item and 'start frame path' not in item:
|
if 'reference image path' in item and 'start frame path' not in item:
|
||||||
item['start frame path'] = item.pop('reference image path')
|
item['start frame path'] = item.pop('reference image path')
|
||||||
|
# Split old single strength into high+low
|
||||||
|
for prefix in ('start frame', 'middle frame', 'end frame'):
|
||||||
|
old_key = f'{prefix} strength'
|
||||||
|
if old_key in item:
|
||||||
|
val = item.pop(old_key)
|
||||||
|
item.setdefault(f'{prefix} high strength', val)
|
||||||
|
item.setdefault(f'{prefix} low strength', val)
|
||||||
|
|
||||||
|
|
||||||
def _migrate_lora_keys(data: dict) -> None:
|
def _migrate_lora_keys(data: dict) -> None:
|
||||||
@@ -225,6 +243,7 @@ def load_json(path: str | Path) -> tuple[dict[str, Any], float]:
|
|||||||
with open(path, 'r') as f:
|
with open(path, 'r') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
t1 = time.time()
|
t1 = time.time()
|
||||||
|
_migrate_remove_keys(data)
|
||||||
_migrate_key_renames(data)
|
_migrate_key_renames(data)
|
||||||
_migrate_lora_keys(data)
|
_migrate_lora_keys(data)
|
||||||
t2 = time.time()
|
t2 = time.time()
|
||||||
|
|||||||
Reference in New Issue
Block a user