diff --git a/db.py b/db.py index 8af5ff5..3adc115 100644 --- a/db.py +++ b/db.py @@ -209,7 +209,7 @@ class ProjectDB: @staticmethod def _migrate_lora_keys(data: dict) -> dict: - """Split legacy values into separate name/strength keys.""" + """Merge lora name + strength into single 'name:strength' key, remove separate strength keys.""" for idx in range(1, 4): for tier in ('high', 'low'): name_key = f'lora {idx} {tier}' @@ -217,21 +217,16 @@ class ProjectDB: raw = str(data.get(name_key, '')) if raw.startswith('', '') - if ':' in inner: - parts = inner.rsplit(':', 1) - data[name_key] = parts[0] - try: - data[str_key] = float(parts[1]) - except ValueError: - data.setdefault(str_key, 1.0) + 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 '' else: - data[name_key] = inner - data.setdefault(str_key, 1.0) - elif name_key in data and str_key not in data: - data[str_key] = 1.0 - # Ensure strength is always a float (JSON may deserialize 1 as int) - if str_key in data: - data[str_key] = float(data[str_key]) + 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}' return data def get_sequence(self, data_file_id: int, sequence_number: int) -> dict | None: @@ -266,11 +261,6 @@ class ProjectDB: return 0 return self.count_sequences(df["id"]) - _FLOAT_KEYS = frozenset( - f'lora {idx} {tier} strength' - for idx in range(1, 4) for tier in ('high', 'low') - ) - def get_sequence_keys(self, data_file_id: int, sequence_number: int) -> tuple[list[str], list[str]]: """Returns (keys, types) for a sequence's data dict.""" data = self.get_sequence(data_file_id, sequence_number) @@ -280,9 +270,7 @@ class ProjectDB: types = [] for k, v in data.items(): keys.append(k) - if k in self._FLOAT_KEYS: - types.append("FLOAT") - elif isinstance(v, bool): + if isinstance(v, bool): types.append("STRING") elif isinstance(v, int): types.append("INT") diff --git a/tab_batch_ng.py b/tab_batch_ng.py index 0652926..71ff9d0 100644 --- a/tab_batch_ng.py +++ b/tab_batch_ng.py @@ -296,9 +296,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 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'] + lora_keys = ['lora 1 high', 'lora 1 low', + 'lora 2 high', 'lora 2 low', + 'lora 3 high', 'lora 3 low'] standard_keys = { 'name', 'mode', 'general_prompt', 'general_negative', 'current_prompt', 'negative', 'prompt', 'seed', 'cfg', 'camera', 'flf', KEY_SEQUENCE_NUMBER, @@ -561,28 +561,26 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state, with ui.expansion('LoRA Settings', icon='style').classes('w-full'): for lora_idx in range(1, 4): for tier, tier_label in [('high', 'High'), ('low', 'Low')]: - name_key = f'lora {lora_idx} {tier}' - str_key = f'lora {lora_idx} {tier} strength' + lora_key = f'lora {lora_idx} {tier}' - # Migrate legacy format - raw = str(seq.get(name_key, '')) + # Parse combined name:strength value + raw = str(seq.get(lora_key, '')) + # Handle legacy wrapper if raw.startswith('', '') - if ':' in inner: - parts = inner.rsplit(':', 1) - seq[name_key] = parts[0] - try: - seq[str_key] = float(parts[1]) - except ValueError: - seq[str_key] = 1.0 - else: - seq[name_key] = inner - seq.setdefault(str_key, 1.0) - else: - seq.setdefault(str_key, 1.0) + raw = raw.replace('', '') + # Also remove stale separate strength key if present + seq.pop(f'lora {lora_idx} {tier} strength', None) - lora_name = seq.get(name_key, '') - lora_strength = seq.get(str_key, 1.0) + 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_strength = 1.0 with ui.row().classes('w-full items-center q-gutter-sm'): ui.label(f'L{lora_idx} {tier_label}').classes( @@ -598,9 +596,10 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state, format='%.1f', ).props('outlined dense').style('max-width: 80px') - def _lora_sync(nk=name_key, sk=str_key, n_inp=name_input, s_inp=strength_input): - seq[nk] = n_inp.value or '' - seq[sk] = s_inp.value if s_inp.value is not None else 1.0 + 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 '' 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 cf3b88a..aebdf2b 100644 --- a/tab_timeline_ng.py +++ b/tab_timeline_ng.py @@ -626,13 +626,9 @@ 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')]: - with ui.column(): - ui.input(f'L{lora_idx} {tier_label} Name', - value=item_data.get(f'lora {lora_idx} {tier}', '')).props( - 'readonly outlined dense') - ui.input(f'L{lora_idx} {tier_label} Str', - value=str(item_data.get(f'lora {lora_idx} {tier} strength', 1.0))).props( - 'readonly outlined dense') + ui.input(f'L{lora_idx} {tier_label}', + value=item_data.get(f'lora {lora_idx} {tier}', '')).props( + 'readonly outlined dense') 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 f6185b4..c73e693 100644 --- a/utils.py +++ b/utils.py @@ -49,13 +49,13 @@ DEFAULTS = { "reference path": "", "flf image path": "", - # --- LoRAs --- - "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 high strength": 1.0, - "lora 3 low": "", "lora 3 low strength": 1.0 + # --- LoRAs (format: "name:strength" or empty) --- + "lora 1 high": "", + "lora 1 low": "", + "lora 2 high": "", + "lora 2 low": "", + "lora 3 high": "", + "lora 3 low": "" } CONFIG_FILE = Path(".editor_config.json") @@ -145,7 +145,13 @@ def save_snippets(snippets): os.replace(tmp, SNIPPETS_FILE) def _migrate_lora_keys(data: dict) -> None: - """Split legacy values into separate name/strength keys in-place.""" + """Merge lora name + strength into single 'name:strength' key, remove separate 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 + """ for item in data.get(KEY_BATCH_DATA, []): if not isinstance(item, dict): continue @@ -154,23 +160,26 @@ def _migrate_lora_keys(data: dict) -> None: name_key = f'lora {idx} {tier}' str_key = f'lora {idx} {tier} strength' raw = str(item.get(name_key, '')) + if raw.startswith(' format → Name:0.5 inner = raw.replace('', '') - if ':' in inner: - parts = inner.rsplit(':', 1) - item[name_key] = parts[0] - try: - item[str_key] = float(parts[1]) - except ValueError: - item.setdefault(str_key, 1.0) + 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 '' else: - item[name_key] = inner - item.setdefault(str_key, 1.0) - elif name_key in item and str_key not in item: - item[str_key] = 1.0 - # Ensure strength is always a float (JSON may deserialize 1 as int) - if str_key in item: - item[str_key] = float(item[str_key]) + 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 def load_json(path: str | Path) -> tuple[dict[str, Any], float]: