diff --git a/db.py b/db.py index f4ac10e..c870e8f 100644 --- a/db.py +++ b/db.py @@ -81,7 +81,7 @@ class ProjectDB: self._migrate_all_lora_data() 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() updated = 0 self.conn.execute("BEGIN") @@ -242,9 +242,10 @@ class ProjectDB: ) self.conn.commit() + @staticmethod @staticmethod 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 tier in ('high', 'low'): name_key = f'lora {idx} {tier}' @@ -252,16 +253,31 @@ class ProjectDB: raw = str(data.get(name_key, '')) if raw.startswith('', '') - data[name_key] = inner - if ':' not in inner: - strength = data.pop(str_key, 1.0) - data[name_key] = f'{inner}:{float(strength)}' if inner else '' + if ':' in inner: + parts = inner.rsplit(':', 1) + data[name_key] = parts[0] + try: + data[str_key] = float(parts[1]) + except ValueError: + data[str_key] = 1.0 else: - data.pop(str_key, None) - elif str_key in data: - strength = float(data.pop(str_key)) - if raw and ':' not in raw: - data[name_key] = f'{raw}:{strength}' + data[name_key] = inner + if str_key not in data: + data[str_key] = 1.0 + elif ':' in raw and raw: + 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 def get_sequence(self, data_file_id: int, sequence_number: int) -> dict | None: diff --git a/tab_batch_ng.py b/tab_batch_ng.py index a8d9888..7bf4a59 100644 --- a/tab_batch_ng.py +++ b/tab_batch_ng.py @@ -306,9 +306,9 @@ def render_batch_processor(state: AppState): ui.button('From Source', icon='file_download', on_click=add_from_source) # --- Standard / LoRA / VACE key sets --- - lora_keys = ['lora 1 high', 'lora 1 low', - 'lora 2 high', 'lora 2 low', - 'lora 3 high', 'lora 3 low'] + lora_keys = ['lora 1 high', 'lora 1 high strength', 'lora 1 low', 'lora 1 low strength', + 'lora 2 high', 'lora 2 high strength', 'lora 2 low', 'lora 2 low strength', + 'lora 3 high', 'lora 3 high strength', 'lora 3 low', 'lora 3 low strength'] standard_keys = { 'name', 'mode', 'general_prompt', 'general_negative', 'current_prompt', 'negative', 'prompt', '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')]: lora_key = f'lora {lora_idx} {tier}' - # Parse combined name:strength value - raw = str(seq.get(lora_key, '')) - # Handle legacy wrapper - if raw.startswith('', '') - # 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: - lora_strength = float(parts[1]) - except ValueError: - lora_strength = 1.0 - else: - lora_name = raw + lora_name = str(seq.get(lora_key, '')) + strength_key = f'lora {lora_idx} {tier} strength' + lora_strength = seq.get(strength_key, 1.0) + try: + lora_strength = float(lora_strength) + except (ValueError, TypeError): lora_strength = 1.0 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', ).props('outlined dense').style('max-width: 80px') - def _lora_sync(k=lora_key, n_inp=name_input, s_inp=strength_input): - name = n_inp.value or '' - strength = s_inp.value if s_inp.value is not None else 1.0 - seq[k] = f'{name}:{strength}' if name else '' + def _lora_sync(k=lora_key, sk=strength_key, n_inp=name_input, s_inp=strength_input): + seq[k] = n_inp.value or '' + seq[sk] = float(s_inp.value) if s_inp.value is not None else 1.0 name_input.on('blur', lambda _, s=_lora_sync: s()) name_input.on('update:model-value', lambda _, s=_lora_sync: s()) diff --git a/tab_timeline_ng.py b/tab_timeline_ng.py index b0b82d4..7e89b40 100644 --- a/tab_timeline_ng.py +++ b/tab_timeline_ng.py @@ -667,9 +667,12 @@ def _render_preview_fields(item_data: dict): with ui.row().classes('w-full q-gutter-md'): for lora_idx in range(1, 4): 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}', - value=item_data.get(f'lora {lora_idx} {tier}', '')).props( - 'readonly outlined dense') + value=str(lora_name)).props('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'] if any(k in item_data for k in vace_keys): diff --git a/utils.py b/utils.py index 8b8b640..91f151a 100644 --- a/utils.py +++ b/utils.py @@ -49,13 +49,19 @@ DEFAULTS = { "reference path": "", "flf image path": "", - # --- LoRAs (format: "name:strength" or empty) --- + # --- LoRAs (name as STRING, strength as FLOAT) --- "lora 1 high": "", + "lora 1 high strength": 1.0, "lora 1 low": "", + "lora 1 low strength": 1.0, "lora 2 high": "", + "lora 2 high strength": 1.0, "lora 2 low": "", + "lora 2 low strength": 1.0, "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") @@ -145,12 +151,12 @@ def save_snippets(snippets): os.replace(tmp, SNIPPETS_FILE) 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: - 1. → Name:0.5 - 2. Separate name_key + str_key → name:strength (then delete str_key) - 3. Already merged name:strength → no change + Handles legacy formats: + 1. → name_key='Name', str_key=0.5 + 2. 'Name:0.5' (merged) → name_key='Name', str_key=0.5 + 3. Already split (name_key + str_key exist) → no change """ for item in data.get(KEY_BATCH_DATA, []): if not isinstance(item, dict): @@ -162,24 +168,35 @@ def _migrate_lora_keys(data: dict) -> None: raw = str(item.get(name_key, '')) if raw.startswith(' format → Name:0.5 + # Legacy format inner = raw.replace('', '') - item[name_key] = inner # already name:strength or just name - if ':' not in inner: - # No strength in the wrapper, check separate key - strength = item.pop(str_key, 1.0) - item[name_key] = f'{inner}:{float(strength)}' if inner else '' + if ':' in inner: + parts = inner.rsplit(':', 1) + item[name_key] = parts[0] + try: + item[str_key] = float(parts[1]) + except ValueError: + item[str_key] = 1.0 else: - item.pop(str_key, None) - elif str_key in item: - # Separate strength key exists → merge into name:strength - strength = float(item.pop(str_key)) - if raw: - # Avoid double-merging if already has name:strength format - if ':' not in raw: - item[name_key] = f'{raw}:{strength}' - # else: already merged, just remove the stale strength key - # No change needed if already in name:strength format or empty + item[name_key] = inner + if str_key not in item: + item[str_key] = 1.0 + elif ':' in raw and raw: + # Combined 'name:strength' format → split + parts = raw.rsplit(':', 1) + try: + strength = float(parts[1]) + item[name_key] = parts[0] + 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]: