feat: split lora name and strength into separate keys

Reverses the previous merge migration. Lora data is now stored as
separate keys: 'lora 1 high' (STRING name) and 'lora 1 high strength'
(FLOAT). This allows ProjectKey relay nodes to output name and strength
as properly typed separate values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 16:18:33 +01:00
parent 3dc91319a2
commit 672b28e27f
4 changed files with 84 additions and 60 deletions
+27 -11
View File
@@ -81,7 +81,7 @@ class ProjectDB:
self._migrate_all_lora_data() self._migrate_all_lora_data()
def _migrate_all_lora_data(self) -> None: def _migrate_all_lora_data(self) -> None:
"""One-time bulk migration: merge separate lora strength keys in all stored sequences.""" """Bulk migration: split combined lora 'name:strength' into separate keys."""
rows = self.conn.execute("SELECT id, data FROM sequences").fetchall() rows = self.conn.execute("SELECT id, data FROM sequences").fetchall()
updated = 0 updated = 0
self.conn.execute("BEGIN") self.conn.execute("BEGIN")
@@ -242,9 +242,10 @@ class ProjectDB:
) )
self.conn.commit() self.conn.commit()
@staticmethod
@staticmethod @staticmethod
def _migrate_lora_keys(data: dict) -> dict: def _migrate_lora_keys(data: dict) -> dict:
"""Merge lora name + strength into single 'name:strength' key, remove separate strength keys.""" """Split combined lora 'name:strength' into separate name and strength keys."""
for idx in range(1, 4): for idx in range(1, 4):
for tier in ('high', 'low'): for tier in ('high', 'low'):
name_key = f'lora {idx} {tier}' name_key = f'lora {idx} {tier}'
@@ -252,16 +253,31 @@ class ProjectDB:
raw = str(data.get(name_key, '')) raw = str(data.get(name_key, ''))
if raw.startswith('<lora:'): if raw.startswith('<lora:'):
inner = raw.replace('<lora:', '').replace('>', '') inner = raw.replace('<lora:', '').replace('>', '')
data[name_key] = inner if ':' in inner:
if ':' not in inner: parts = inner.rsplit(':', 1)
strength = data.pop(str_key, 1.0) data[name_key] = parts[0]
data[name_key] = f'{inner}:{float(strength)}' if inner else '' try:
data[str_key] = float(parts[1])
except ValueError:
data[str_key] = 1.0
else: else:
data.pop(str_key, None) data[name_key] = inner
elif str_key in data: if str_key not in data:
strength = float(data.pop(str_key)) data[str_key] = 1.0
if raw and ':' not in raw: elif ':' in raw and raw:
data[name_key] = f'{raw}:{strength}' parts = raw.rsplit(':', 1)
try:
strength = float(parts[1])
data[name_key] = parts[0]
data[str_key] = strength
except ValueError:
if str_key not in data:
data[str_key] = 1.0
elif raw:
# Name exists without colon, ensure strength key exists
if str_key not in data:
data[str_key] = 1.0
# If name is empty, don't add a strength key
return data return data
def get_sequence(self, data_file_id: int, sequence_number: int) -> dict | None: def get_sequence(self, data_file_id: int, sequence_number: int) -> dict | None:
+11 -23
View File
@@ -306,9 +306,9 @@ def render_batch_processor(state: AppState):
ui.button('From Source', icon='file_download', on_click=add_from_source) ui.button('From Source', icon='file_download', on_click=add_from_source)
# --- Standard / LoRA / VACE key sets --- # --- Standard / LoRA / VACE key sets ---
lora_keys = ['lora 1 high', 'lora 1 low', lora_keys = ['lora 1 high', 'lora 1 high strength', 'lora 1 low', 'lora 1 low strength',
'lora 2 high', 'lora 2 low', 'lora 2 high', 'lora 2 high strength', 'lora 2 low', 'lora 2 low strength',
'lora 3 high', 'lora 3 low'] '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', 'cfg', 'camera', 'flf', KEY_SEQUENCE_NUMBER,
@@ -592,23 +592,12 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state,
for tier, tier_label in [('high', 'High'), ('low', 'Low')]: for tier, tier_label in [('high', 'High'), ('low', 'Low')]:
lora_key = f'lora {lora_idx} {tier}' lora_key = f'lora {lora_idx} {tier}'
# Parse combined name:strength value lora_name = str(seq.get(lora_key, ''))
raw = str(seq.get(lora_key, '')) strength_key = f'lora {lora_idx} {tier} strength'
# Handle legacy <lora:> wrapper lora_strength = seq.get(strength_key, 1.0)
if raw.startswith('<lora:'):
raw = raw.replace('<lora:', '').replace('>', '')
# Also remove stale separate strength key if present
seq.pop(f'lora {lora_idx} {tier} strength', None)
if ':' in raw and raw:
parts = raw.rsplit(':', 1)
lora_name = parts[0]
try: try:
lora_strength = float(parts[1]) lora_strength = float(lora_strength)
except ValueError: except (ValueError, TypeError):
lora_strength = 1.0
else:
lora_name = raw
lora_strength = 1.0 lora_strength = 1.0
with ui.row().classes('w-full items-center q-gutter-sm'): with ui.row().classes('w-full items-center q-gutter-sm'):
@@ -625,10 +614,9 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state,
format='%.1f', format='%.1f',
).props('outlined dense').style('max-width: 80px') ).props('outlined dense').style('max-width: 80px')
def _lora_sync(k=lora_key, n_inp=name_input, s_inp=strength_input): def _lora_sync(k=lora_key, sk=strength_key, n_inp=name_input, s_inp=strength_input):
name = n_inp.value or '' seq[k] = n_inp.value or ''
strength = s_inp.value if s_inp.value is not None else 1.0 seq[sk] = float(s_inp.value) if s_inp.value is not None else 1.0
seq[k] = f'{name}:{strength}' if name else ''
name_input.on('blur', lambda _, s=_lora_sync: s()) name_input.on('blur', lambda _, s=_lora_sync: s())
name_input.on('update:model-value', lambda _, s=_lora_sync: s()) name_input.on('update:model-value', lambda _, s=_lora_sync: s())
+5 -2
View File
@@ -667,9 +667,12 @@ 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'):
for lora_idx in range(1, 4): for lora_idx in range(1, 4):
for tier, tier_label in [('high', 'High'), ('low', 'Low')]: for tier, tier_label in [('high', 'High'), ('low', 'Low')]:
lora_name = item_data.get(f'lora {lora_idx} {tier}', '')
lora_str = item_data.get(f'lora {lora_idx} {tier} strength', 1.0)
ui.input(f'L{lora_idx} {tier_label}', ui.input(f'L{lora_idx} {tier_label}',
value=item_data.get(f'lora {lora_idx} {tier}', '')).props( value=str(lora_name)).props('readonly outlined dense')
'readonly outlined dense') ui.number(f'L{lora_idx} {tier_label} Str',
value=float(lora_str)).props('readonly outlined dense').style('max-width: 80px')
vace_keys = ['frame_to_skip', 'vace schedule', 'video file path'] vace_keys = ['frame_to_skip', 'vace schedule', 'video file path']
if any(k in item_data for k in vace_keys): if any(k in item_data for k in vace_keys):
+40 -23
View File
@@ -49,13 +49,19 @@ DEFAULTS = {
"reference path": "", "reference path": "",
"flf image path": "", "flf image path": "",
# --- LoRAs (format: "name:strength" or empty) --- # --- LoRAs (name as STRING, strength as FLOAT) ---
"lora 1 high": "", "lora 1 high": "",
"lora 1 high strength": 1.0,
"lora 1 low": "", "lora 1 low": "",
"lora 1 low strength": 1.0,
"lora 2 high": "", "lora 2 high": "",
"lora 2 high strength": 1.0,
"lora 2 low": "", "lora 2 low": "",
"lora 2 low strength": 1.0,
"lora 3 high": "", "lora 3 high": "",
"lora 3 low": "" "lora 3 high strength": 1.0,
"lora 3 low": "",
"lora 3 low strength": 1.0
} }
CONFIG_FILE = Path(".editor_config.json") CONFIG_FILE = Path(".editor_config.json")
@@ -145,12 +151,12 @@ def save_snippets(snippets):
os.replace(tmp, SNIPPETS_FILE) os.replace(tmp, SNIPPETS_FILE)
def _migrate_lora_keys(data: dict) -> None: def _migrate_lora_keys(data: dict) -> None:
"""Merge lora name + strength into single 'name:strength' key, remove separate strength keys. """Split combined lora 'name:strength' into separate name and strength keys.
Handles three legacy formats: Handles legacy formats:
1. <lora:Name:0.5> → Name:0.5 1. <lora:Name:0.5> → name_key='Name', str_key=0.5
2. Separate name_key + str_key → name:strength (then delete str_key) 2. 'Name:0.5' (merged) → name_key='Name', str_key=0.5
3. Already merged name:strength → no change 3. Already split (name_key + str_key exist) → no change
""" """
for item in data.get(KEY_BATCH_DATA, []): for item in data.get(KEY_BATCH_DATA, []):
if not isinstance(item, dict): if not isinstance(item, dict):
@@ -162,24 +168,35 @@ def _migrate_lora_keys(data: dict) -> None:
raw = str(item.get(name_key, '')) raw = str(item.get(name_key, ''))
if raw.startswith('<lora:'): if raw.startswith('<lora:'):
# Legacy <lora:Name:0.5> format → Name:0.5 # Legacy <lora:Name:0.5> format
inner = raw.replace('<lora:', '').replace('>', '') inner = raw.replace('<lora:', '').replace('>', '')
item[name_key] = inner # already name:strength or just name if ':' in inner:
if ':' not in inner: parts = inner.rsplit(':', 1)
# No strength in the wrapper, check separate key item[name_key] = parts[0]
strength = item.pop(str_key, 1.0) try:
item[name_key] = f'{inner}:{float(strength)}' if inner else '' item[str_key] = float(parts[1])
except ValueError:
item[str_key] = 1.0
else: else:
item.pop(str_key, None) item[name_key] = inner
elif str_key in item: if str_key not in item:
# Separate strength key exists → merge into name:strength item[str_key] = 1.0
strength = float(item.pop(str_key)) elif ':' in raw and raw:
if raw: # Combined 'name:strength' format → split
# Avoid double-merging if already has name:strength format parts = raw.rsplit(':', 1)
if ':' not in raw: try:
item[name_key] = f'{raw}:{strength}' strength = float(parts[1])
# else: already merged, just remove the stale strength key item[name_key] = parts[0]
# No change needed if already in name:strength format or empty item[str_key] = strength
except ValueError:
# Not a valid strength, leave as-is
if str_key not in item:
item[str_key] = 1.0
elif raw:
# Name exists without colon, ensure strength key exists
if str_key not in item:
item[str_key] = 1.0
# If name is empty, don't add a strength key
def load_json(path: str | Path) -> tuple[dict[str, Any], float]: def load_json(path: str | Path) -> tuple[dict[str, Any], float]: