This commit is contained in:
2026-02-05 14:59:03 +01:00
parent e58dc27dce
commit 5defd664ed
2 changed files with 129 additions and 44 deletions

View File

@@ -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:

View File

@@ -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()))