film
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()))
|
||||
|
||||
Reference in New Issue
Block a user