diff --git a/core/blender.py b/core/blender.py index ef6998e..9450a1b 100644 --- a/core/blender.py +++ b/core/blender.py @@ -1462,7 +1462,7 @@ class TransitionGenerator: spec: TransitionSpec, dest: Path, folder_idx_main: int, - base_file_idx: int + base_seq_num: int ) -> list[BlendResult]: """Generate blended frames for an asymmetric transition. @@ -1473,8 +1473,8 @@ class TransitionGenerator: Args: spec: TransitionSpec describing the transition. dest: Destination directory for blended frames. - folder_idx_main: Folder index for sequence naming. - base_file_idx: Starting file index for sequence naming. + folder_idx_main: Folder index (unused, kept for compatibility). + base_seq_num: Starting sequence number for continuous naming. Returns: List of BlendResult objects. @@ -1529,8 +1529,8 @@ class TransitionGenerator: # Generate output filename ext = f".{self.settings.output_format.lower()}" - file_idx = base_file_idx + i - output_name = f"seq{folder_idx_main + 1:02d}_{file_idx:04d}{ext}" + seq_num = base_seq_num + i + output_name = f"seq_{seq_num:05d}{ext}" output_path = dest / output_name result = self.blender.blend_images_pil( @@ -1569,7 +1569,7 @@ class TransitionGenerator: spec: TransitionSpec, dest: Path, folder_idx_main: int, - base_file_idx: int + base_seq_num: int ) -> list[BlendResult]: """Generate blended frames for a transition. @@ -1578,15 +1578,15 @@ class TransitionGenerator: Args: spec: TransitionSpec describing the transition. dest: Destination directory for blended frames. - folder_idx_main: Folder index for sequence naming. - base_file_idx: Starting file index for sequence naming. + folder_idx_main: Folder index (unused, kept for compatibility). + base_seq_num: Starting sequence number for continuous naming. Returns: List of BlendResult objects. """ # Use asymmetric blend for all cases (handles symmetric too) return self.generate_asymmetric_blend_frames( - spec, dest, folder_idx_main, base_file_idx + spec, dest, folder_idx_main, base_seq_num ) def generate_direct_interpolation_frames( @@ -1597,7 +1597,7 @@ class TransitionGenerator: method: DirectInterpolationMethod, dest: Path, folder_idx: int, - base_file_idx: int, + base_seq_num: int, practical_rife_model: str = 'v4.25', practical_rife_ensemble: bool = False ) -> list[BlendResult]: @@ -1615,8 +1615,8 @@ class TransitionGenerator: frame_count: Number of interpolated frames to generate. method: Interpolation method (RIFE or FILM). dest: Destination directory for generated frames. - folder_idx: Folder index for sequence naming. - base_file_idx: Starting file index for sequence naming. + folder_idx: Folder index (unused, kept for compatibility). + base_seq_num: Starting sequence number for continuous naming. practical_rife_model: Practical-RIFE model version. practical_rife_ensemble: Enable Practical-RIFE ensemble mode. @@ -1629,7 +1629,7 @@ class TransitionGenerator: # For FILM, use batch mode to generate all frames at once if method == DirectInterpolationMethod.FILM and FilmEnv.is_setup(): return self._generate_film_frames_batch( - img_a_path, img_b_path, frame_count, dest, folder_idx, base_file_idx + img_a_path, img_b_path, frame_count, dest, base_seq_num ) # For RIFE (or FILM fallback), generate frames one at a time @@ -1662,8 +1662,8 @@ class TransitionGenerator: # Generate output filename ext = f".{self.settings.output_format.lower()}" - file_idx = base_file_idx + i - output_name = f"seq{folder_idx + 1:02d}_trans_{file_idx:04d}{ext}" + seq_num = base_seq_num + i + output_name = f"seq_{seq_num:05d}{ext}" output_path = dest / output_name # Save the blended frame @@ -1713,8 +1713,7 @@ class TransitionGenerator: img_b_path: Path, frame_count: int, dest: Path, - folder_idx: int, - base_file_idx: int + base_seq_num: int ) -> list[BlendResult]: """Generate FILM frames using batch mode for better quality. @@ -1726,8 +1725,7 @@ class TransitionGenerator: img_b_path: Path to first frame of second sequence. frame_count: Number of interpolated frames to generate. dest: Destination directory for generated frames. - folder_idx: Folder index for sequence naming. - base_file_idx: Starting file index for sequence naming. + base_seq_num: Starting sequence number for continuous naming. Returns: List of BlendResult objects. @@ -1751,8 +1749,8 @@ class TransitionGenerator: for i in range(frame_count): t = (i + 1) / (frame_count + 1) ext = f".{self.settings.output_format.lower()}" - file_idx = base_file_idx + i - output_name = f"seq{folder_idx + 1:02d}_trans_{file_idx:04d}{ext}" + seq_num = base_seq_num + i + output_name = f"seq_{seq_num:05d}{ext}" output_path = dest / output_name results.append(BlendResult( @@ -1769,8 +1767,8 @@ class TransitionGenerator: for i, temp_path in enumerate(temp_paths): t = (i + 1) / (frame_count + 1) ext = f".{self.settings.output_format.lower()}" - file_idx = base_file_idx + i - output_name = f"seq{folder_idx + 1:02d}_trans_{file_idx:04d}{ext}" + seq_num = base_seq_num + i + output_name = f"seq_{seq_num:05d}{ext}" output_path = dest / output_name try: diff --git a/ui/main_window.py b/ui/main_window.py index a633b9b..9074aa0 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -1505,6 +1505,7 @@ class SequenceLinkerUI(QWidget): consecutive_main_pairs.append((i, i + 1)) # Process each folder + output_seq = 0 # Track continuous sequence number for preview for folder_idx, folder in enumerate(self.source_folders): folder_files = files_by_folder.get(folder, []) if not folder_files: @@ -1536,7 +1537,7 @@ class SequenceLinkerUI(QWidget): # These frames are consumed by the blend - skip them continue - seq_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}" + seq_name = f"seq_{output_seq:05d}" if should_blend and blend_trans: # Calculate which trans frame this blends with @@ -1551,19 +1552,21 @@ class SequenceLinkerUI(QWidget): trans_text = f"→ {trans_file}" item = QTreeWidgetItem([main_text, trans_text]) - item.setData(0, Qt.ItemDataRole.UserRole, (folder, filename, folder_idx, file_idx, 'blend')) + item.setData(0, Qt.ItemDataRole.UserRole, (folder, filename, folder_idx, file_idx, 'blend', output_seq)) item.setData(1, Qt.ItemDataRole.UserRole, (blend_trans.trans_folder, trans_file)) # Blue color for blend frames item.setForeground(0, QColor(100, 150, 255)) item.setForeground(1, QColor(100, 150, 255)) + output_seq += 1 elif folder_type == FolderType.TRANSITION: - # Transition folder files go in Transition column only - item = QTreeWidgetItem(["", f"{seq_name} ({filename})"]) - item.setData(1, Qt.ItemDataRole.UserRole, (folder, filename, folder_idx, file_idx, 'symlink')) + # Transition folder files go in Transition column only (no output file) + item = QTreeWidgetItem(["", f"({filename})"]) + item.setData(1, Qt.ItemDataRole.UserRole, (folder, filename, folder_idx, file_idx, 'symlink', -1)) else: # Main folder files go in Main column only item = QTreeWidgetItem([f"{seq_name} ({filename})", ""]) - item.setData(0, Qt.ItemDataRole.UserRole, (folder, filename, folder_idx, file_idx, 'symlink')) + item.setData(0, Qt.ItemDataRole.UserRole, (folder, filename, folder_idx, file_idx, 'symlink', output_seq)) + output_seq += 1 self.sequence_table.addTopLevelItem(item) @@ -1680,7 +1683,11 @@ class SequenceLinkerUI(QWidget): total = self.sequence_table.topLevelItemCount() self.image_index_label.setText(f"{row_idx + 1} / {total}") - seq_name = f"seq{data[2] + 1:02d}_{data[3]:04d}" + # Use continuous format for transitions, folder-based for regular export + if self.transition_group.isChecked() and len(data) > 5 and data[5] >= 0: + seq_name = f"seq_{data[5]:05d}" + else: + seq_name = f"seq{data[2] + 1:02d}_{data[3]:04d}" self.image_name_label.setText(f"{seq_name} ({filename})") def _on_sequence_table_clicked(self, item, column: int) -> None: @@ -1825,7 +1832,9 @@ class SequenceLinkerUI(QWidget): # Update labels self.image_index_label.setText(f"{row_idx + 1} / {total}") - seq_name = f"seq{data0[2] + 1:02d}_{data0[3]:04d}" + # Use continuous format for blend frames (always with transitions) + output_seq_num = data0[5] if len(data0) > 5 else row_idx + seq_name = f"seq_{output_seq_num:05d}" self.image_name_label.setText(f"[B] {seq_name} ({main_file} + {trans_file}) @ {factor:.0%}") except Exception as e: @@ -2528,10 +2537,13 @@ class SequenceLinkerUI(QWidget): db_transition_settings = self.db.get_transition_settings(latest_session.id) db_per_trans_settings = self.db.get_all_per_transition_settings(latest_session.id) - new_pattern = re.compile(r'seq(\d+)_(\d+)') - old_pattern = re.compile(r'seq_(\d+)') + # Pattern for new continuous format: seq_00000 + continuous_pattern = re.compile(r'seq_(\d+)') + # Pattern for old folder-based format: seq01_0000 + folder_pattern = re.compile(r'seq(\d+)_(\d+)') folder_data: dict[str, tuple[int, list[tuple[int, str]]]] = {} + folder_first_seq: dict[str, int] = {} # Track first sequence number per folder missing_count = 0 for link in symlinks: @@ -2543,23 +2555,36 @@ class SequenceLinkerUI(QWidget): folder = str(source_path.parent) link_name = Path(link.link_path).stem - match = new_pattern.match(link_name) + # Try continuous format first (new format) + match = continuous_pattern.match(link_name) if match: - folder_idx = int(match.group(1)) - 1 - file_idx = int(match.group(2)) + seq_num = int(match.group(1)) + # Use sequence number for ordering, folder from source_path + if folder not in folder_first_seq: + folder_first_seq[folder] = seq_num + file_idx = seq_num else: - match = old_pattern.match(link_name) + # Try old folder-based format + match = folder_pattern.match(link_name) if match: - folder_idx = 0 - file_idx = int(match.group(1)) + folder_idx_from_name = int(match.group(1)) - 1 + file_idx = int(match.group(2)) + if folder not in folder_first_seq: + folder_first_seq[folder] = folder_idx_from_name * 10000 + file_idx else: - folder_idx = 0 + # Fallback to database sequence number file_idx = link.sequence_number + if folder not in folder_first_seq: + folder_first_seq[folder] = file_idx if folder not in folder_data: - folder_data[folder] = (folder_idx, []) + folder_data[folder] = (0, []) # folder_idx will be set later folder_data[folder][1].append((file_idx, link.original_filename)) + # Sort folders by their first sequence number to maintain order + for folder in folder_data: + folder_data[folder] = (folder_first_seq.get(folder, 0), folder_data[folder][1]) + if not folder_data: return False @@ -2622,7 +2647,8 @@ class SequenceLinkerUI(QWidget): self._current_session_id = latest_session.id self._sync_dual_lists() - self._refresh_files() + # Restore exact files from session instead of refreshing from disk + self._restore_files_from_session(folder_data) self._update_flow_arrows() total_files = self.file_list.topLevelItemCount() @@ -2952,6 +2978,67 @@ class SequenceLinkerUI(QWidget): self._update_trim_slider_for_selected_folder() self._update_sequence_table() + def _restore_files_from_session( + self, + folder_data: dict[str, tuple[int, list[tuple[int, str]]]] + ) -> None: + """Restore file list from session data, preserving exact sequence. + + Args: + folder_data: Dict mapping folder paths to (folder_idx, [(file_idx, filename), ...]) + """ + self.file_list.clear() + if not folder_data: + self._folder_file_counts.clear() + return + + # Sort folders by their index + sorted_folders = sorted(folder_data.items(), key=lambda x: x[1][0]) + + self._folder_file_counts = {} + is_first_folder = True + + for folder_str, (folder_idx, file_list) in sorted_folders: + folder_path = Path(folder_str) + if not folder_path.exists(): + continue + + # Sort files by their sequence index + sorted_files = sorted(file_list, key=lambda x: x[0]) + + # Filter to only files that still exist + existing_files = [ + (idx, fname) for idx, fname in sorted_files + if (folder_path / fname).exists() + ] + + if not existing_files: + continue + + self._folder_file_counts[folder_path] = len(existing_files) + + # Add separator between folders (not before first) + if not is_first_folder: + separator = self._create_folder_separator(folder_idx) + self.file_list.addTopLevelItem(separator) + is_first_folder = False + + for file_idx, filename in existing_files: + ext = Path(filename).suffix + seq_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}" + + item = QTreeWidgetItem([seq_name, filename, str(folder_path)]) + item.setData(0, Qt.ItemDataRole.UserRole, (folder_path, filename, folder_idx, file_idx)) + self.file_list.addTopLevelItem(item) + + total = self.file_list.topLevelItemCount() + self.image_slider.setRange(0, max(0, total - 1)) + if total > 0: + self.file_list.setCurrentItem(self.file_list.topLevelItem(0)) + + self._update_trim_slider_for_selected_folder() + self._update_sequence_table() + def _create_folder_separator(self, next_folder_idx: int) -> QTreeWidgetItem: """Create a visual separator item between folders.""" separator = QTreeWidgetItem(["", f"── Sequence {next_folder_idx + 1} ──", ""]) @@ -3576,7 +3663,7 @@ class SequenceLinkerUI(QWidget): ) ext = f".{settings.output_format.lower()}" - output_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}" + output_name = f"seq_{output_seq:05d}{ext}" output_path = trans_dest / output_name result = generator.blender.blend_images( @@ -3600,7 +3687,7 @@ class SequenceLinkerUI(QWidget): output_seq += 1 else: ext = source_path.suffix - link_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}" + link_name = f"seq_{output_seq:05d}{ext}" link_path = symlink_dest / link_name rel_source = Path(os.path.relpath(source_path.resolve(), symlink_dest.resolve()))