Fix multiple bugs from code analysis rounds
- Fix RIFE warp() grid cache: move backwarp_tenGrid to module level so it persists across calls - Fix FILM partial download: download to .tmp file then atomically rename, validate file size - Fix fid_to_pos orphan fallthrough: use .get(fid) with None check instead of defaulting to 0 - Fix commonpath ValueError: guard against mixed-drive paths on Windows - Fix ON CONFLICT clause in direct_transition_settings to match UNIQUE constraint - Remove dead save_folder_type_override method from database - Fix subprocess resource leaks in video.py: add try/except/kill around proc loops - Fix concat file handle leak: wrap writes in try/finally - Fix format conversion in transition export: normalize extension comparison - Fix session restore loop: blockSignals during destination path changes - Fix missing folder handling: keep as placeholders instead of skipping - Fix fid=0 skip guard in _restore_files_from_session Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+2
-38
@@ -772,41 +772,6 @@ class DatabaseManager:
|
||||
)
|
||||
return None
|
||||
|
||||
def save_folder_type_override(
|
||||
self,
|
||||
session_id: int,
|
||||
folder: str,
|
||||
folder_type: FolderType,
|
||||
trim_start: int = 0,
|
||||
trim_end: int = 0
|
||||
) -> None:
|
||||
"""Save folder type override for a folder in a session.
|
||||
|
||||
Args:
|
||||
session_id: The session ID.
|
||||
folder: Path to the source folder.
|
||||
folder_type: The folder type override.
|
||||
trim_start: Number of images to trim from start.
|
||||
trim_end: Number of images to trim from end.
|
||||
|
||||
Raises:
|
||||
DatabaseError: If saving fails.
|
||||
"""
|
||||
try:
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO sequence_trim_settings
|
||||
(session_id, source_folder, trim_start, trim_end, folder_type)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(session_id, source_folder)
|
||||
DO UPDATE SET trim_start=excluded.trim_start,
|
||||
trim_end=excluded.trim_end,
|
||||
folder_type=excluded.folder_type""",
|
||||
(session_id, folder, trim_start, trim_end, folder_type.value)
|
||||
)
|
||||
except sqlite3.Error as e:
|
||||
raise DatabaseError(f"Failed to save folder type override: {e}") from e
|
||||
|
||||
def get_folder_type_overrides(self, session_id: int) -> dict[str, FolderType]:
|
||||
"""Get all folder type overrides for a session.
|
||||
|
||||
@@ -978,9 +943,8 @@ class DatabaseManager:
|
||||
"""INSERT INTO direct_transition_settings
|
||||
(session_id, after_folder, frame_count, method, enabled, folder_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(session_id, folder_order)
|
||||
DO UPDATE SET after_folder=excluded.after_folder,
|
||||
frame_count=excluded.frame_count,
|
||||
ON CONFLICT(session_id, after_folder, folder_order)
|
||||
DO UPDATE SET frame_count=excluded.frame_count,
|
||||
method=excluded.method,
|
||||
enabled=excluded.enabled""",
|
||||
(session_id, after_folder, frame_count, method, 1 if enabled else 0, folder_order)
|
||||
|
||||
+15
-2
@@ -72,10 +72,23 @@ def download_model(model_dir: Path) -> Path:
|
||||
model_dir.mkdir(parents=True, exist_ok=True)
|
||||
model_path = model_dir / FILM_MODEL_FILENAME
|
||||
|
||||
if model_path.exists():
|
||||
# Validate: a valid SavedModel has a saved_model.pb or is a valid file
|
||||
# Quick check: file should be at least 1MB for a real model
|
||||
if model_path.stat().st_size < 1_000_000:
|
||||
print(f"Removing incomplete download at {model_path}", file=sys.stderr)
|
||||
model_path.unlink()
|
||||
|
||||
if not model_path.exists():
|
||||
print(f"Downloading FILM model to {model_path}...", file=sys.stderr)
|
||||
urllib.request.urlretrieve(FILM_MODEL_URL, model_path)
|
||||
print("Download complete.", file=sys.stderr)
|
||||
tmp_path = model_path.with_suffix('.tmp')
|
||||
try:
|
||||
urllib.request.urlretrieve(FILM_MODEL_URL, tmp_path)
|
||||
tmp_path.rename(model_path)
|
||||
print("Download complete.", file=sys.stderr)
|
||||
except Exception:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
return model_path
|
||||
|
||||
|
||||
+6
-4
@@ -73,20 +73,22 @@ class IFBlock(nn.Module):
|
||||
return flow, mask
|
||||
|
||||
|
||||
_backwarp_tenGrid = {}
|
||||
|
||||
|
||||
def warp(tenInput, tenFlow):
|
||||
k = (str(tenFlow.device), str(tenFlow.size()))
|
||||
backwarp_tenGrid = {}
|
||||
if k not in backwarp_tenGrid:
|
||||
if k not in _backwarp_tenGrid:
|
||||
tenHorizontal = torch.linspace(-1.0, 1.0, tenFlow.shape[3], device=tenFlow.device).view(
|
||||
1, 1, 1, tenFlow.shape[3]).expand(tenFlow.shape[0], -1, tenFlow.shape[2], -1)
|
||||
tenVertical = torch.linspace(-1.0, 1.0, tenFlow.shape[2], device=tenFlow.device).view(
|
||||
1, 1, tenFlow.shape[2], 1).expand(tenFlow.shape[0], -1, -1, tenFlow.shape[3])
|
||||
backwarp_tenGrid[k] = torch.cat([tenHorizontal, tenVertical], 1)
|
||||
_backwarp_tenGrid[k] = torch.cat([tenHorizontal, tenVertical], 1)
|
||||
|
||||
tenFlow = torch.cat([tenFlow[:, 0:1, :, :] / ((tenInput.shape[3] - 1.0) / 2.0),
|
||||
tenFlow[:, 1:2, :, :] / ((tenInput.shape[2] - 1.0) / 2.0)], 1)
|
||||
|
||||
g = (backwarp_tenGrid[k] + tenFlow).permute(0, 2, 3, 1)
|
||||
g = (_backwarp_tenGrid[k] + tenFlow).permute(0, 2, 3, 1)
|
||||
return F.grid_sample(input=tenInput, grid=g, mode='bilinear', padding_mode='border', align_corners=True)
|
||||
|
||||
|
||||
|
||||
+44
-34
@@ -94,20 +94,24 @@ def encode_image_sequence(
|
||||
text=True,
|
||||
)
|
||||
|
||||
cancelled = False
|
||||
if proc.stdout:
|
||||
for line in proc.stdout:
|
||||
line = line.strip()
|
||||
m = re.match(r'^frame=(\d+)', line)
|
||||
if m and progress_callback is not None:
|
||||
current = int(m.group(1))
|
||||
if not progress_callback(current, total_frames):
|
||||
cancelled = True
|
||||
proc.terminate()
|
||||
proc.wait()
|
||||
break
|
||||
try:
|
||||
cancelled = False
|
||||
if proc.stdout:
|
||||
for line in proc.stdout:
|
||||
line = line.strip()
|
||||
m = re.match(r'^frame=(\d+)', line)
|
||||
if m and progress_callback is not None:
|
||||
current = int(m.group(1))
|
||||
if not progress_callback(current, total_frames):
|
||||
cancelled = True
|
||||
proc.terminate()
|
||||
break
|
||||
|
||||
proc.wait()
|
||||
proc.wait()
|
||||
except Exception:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
raise
|
||||
|
||||
if cancelled:
|
||||
# Clean up partial file
|
||||
@@ -181,15 +185,17 @@ def encode_from_file_list(
|
||||
mode='w', suffix='.txt', delete=False, prefix='vml_concat_'
|
||||
)
|
||||
concat_path = Path(concat_file.name)
|
||||
for p in file_paths:
|
||||
# Escape single quotes for ffmpeg concat format
|
||||
escaped = str(p.resolve()).replace("'", "'\\''")
|
||||
try:
|
||||
for p in file_paths:
|
||||
# Escape single quotes for ffmpeg concat format
|
||||
escaped = str(p.resolve()).replace("'", "'\\''")
|
||||
concat_file.write(f"file '{escaped}'\n")
|
||||
concat_file.write(f"duration {frame_duration}\n")
|
||||
# Repeat last file so the last frame displays for its full duration
|
||||
escaped = str(file_paths[-1].resolve()).replace("'", "'\\''")
|
||||
concat_file.write(f"file '{escaped}'\n")
|
||||
concat_file.write(f"duration {frame_duration}\n")
|
||||
# Repeat last file so the last frame displays for its full duration
|
||||
escaped = str(file_paths[-1].resolve()).replace("'", "'\\''")
|
||||
concat_file.write(f"file '{escaped}'\n")
|
||||
concat_file.close()
|
||||
finally:
|
||||
concat_file.close()
|
||||
except OSError as e:
|
||||
return False, f"Failed to create concat file: {e}"
|
||||
|
||||
@@ -222,20 +228,24 @@ def encode_from_file_list(
|
||||
text=True,
|
||||
)
|
||||
|
||||
cancelled = False
|
||||
if proc.stdout:
|
||||
for line in proc.stdout:
|
||||
line = line.strip()
|
||||
m = re.match(r'^frame=(\d+)', line)
|
||||
if m and progress_callback is not None:
|
||||
current = int(m.group(1))
|
||||
if not progress_callback(current, total_frames):
|
||||
cancelled = True
|
||||
proc.terminate()
|
||||
proc.wait()
|
||||
break
|
||||
try:
|
||||
cancelled = False
|
||||
if proc.stdout:
|
||||
for line in proc.stdout:
|
||||
line = line.strip()
|
||||
m = re.match(r'^frame=(\d+)', line)
|
||||
if m and progress_callback is not None:
|
||||
current = int(m.group(1))
|
||||
if not progress_callback(current, total_frames):
|
||||
cancelled = True
|
||||
proc.terminate()
|
||||
break
|
||||
|
||||
proc.wait()
|
||||
proc.wait()
|
||||
except Exception:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
raise
|
||||
|
||||
if cancelled:
|
||||
if output_path.exists():
|
||||
|
||||
+82
-27
@@ -1597,7 +1597,9 @@ class SequenceLinkerUI(QWidget):
|
||||
fid_to_pos = {fid: i for i, fid in enumerate(self._folder_ids)}
|
||||
files_by_idx: dict[int, list[str]] = {}
|
||||
for source_dir, filename, folder_idx, file_idx, fid in files:
|
||||
pi = fid_to_pos.get(fid, 0)
|
||||
pi = fid_to_pos.get(fid)
|
||||
if pi is None:
|
||||
continue
|
||||
if pi not in files_by_idx:
|
||||
files_by_idx[pi] = []
|
||||
files_by_idx[pi].append(filename)
|
||||
@@ -2438,7 +2440,10 @@ class SequenceLinkerUI(QWidget):
|
||||
if len(self.source_folders) > 1:
|
||||
paths = [str(f) for f in self.source_folders]
|
||||
# Find common prefix
|
||||
common_prefix = os.path.commonpath(paths) if paths else ""
|
||||
try:
|
||||
common_prefix = os.path.commonpath(paths) if paths else ""
|
||||
except ValueError:
|
||||
common_prefix = ""
|
||||
# Only use prefix if it's a meaningful directory (not just "/")
|
||||
if len(common_prefix) <= 1:
|
||||
common_prefix = ""
|
||||
@@ -2805,7 +2810,10 @@ class SequenceLinkerUI(QWidget):
|
||||
fid = item.data(0, Qt.ItemDataRole.UserRole + 2) or 0
|
||||
removed_by_fid.setdefault(fid, set()).add(filename)
|
||||
|
||||
# For each fid, convert edge removals into trim adjustments
|
||||
# For each fid, convert edge removals into trim adjustments.
|
||||
# Trim operates on the full (untrimmed) file list, so we must count
|
||||
# how many trimmed-list positions to consume from each edge,
|
||||
# including any already-removed files that fall within that span.
|
||||
for fid, filenames in removed_by_fid.items():
|
||||
idx = self._folder_ids.index(fid) if fid in self._folder_ids else -1
|
||||
if idx < 0:
|
||||
@@ -2819,37 +2827,69 @@ class SequenceLinkerUI(QWidget):
|
||||
if not effective:
|
||||
continue
|
||||
|
||||
# Count contiguous removals from start
|
||||
start_bump = 0
|
||||
# Count contiguous removals from start of effective list
|
||||
start_count = 0
|
||||
for f in effective:
|
||||
if f in filenames:
|
||||
start_bump += 1
|
||||
start_count += 1
|
||||
else:
|
||||
break
|
||||
|
||||
# Count contiguous removals from end
|
||||
end_bump = 0
|
||||
# Count contiguous removals from end of effective list
|
||||
end_count = 0
|
||||
for f in reversed(effective):
|
||||
if f in filenames:
|
||||
end_bump += 1
|
||||
end_count += 1
|
||||
else:
|
||||
break
|
||||
|
||||
# Avoid double-counting if all files are removed
|
||||
if start_bump + end_bump > len(effective):
|
||||
start_bump = len(effective)
|
||||
end_bump = 0
|
||||
if start_count + end_count > len(effective):
|
||||
start_count = len(effective)
|
||||
end_count = 0
|
||||
|
||||
edge_files = set()
|
||||
if start_bump > 0:
|
||||
edge_files.update(effective[:start_bump])
|
||||
if end_bump > 0:
|
||||
edge_files.update(effective[-end_bump:])
|
||||
start_trim_bump = 0
|
||||
end_trim_bump = 0
|
||||
|
||||
if start_count > 0:
|
||||
edge_files.update(effective[:start_count])
|
||||
# Walk the trimmed list from the start, counting positions
|
||||
# until we've covered all start edge files (including
|
||||
# already-removed files that fall within that span)
|
||||
covered = 0
|
||||
for f in trimmed:
|
||||
start_trim_bump += 1
|
||||
if f in edge_files:
|
||||
covered += 1
|
||||
if covered == start_count:
|
||||
break
|
||||
# Also clear already-removed files that now fall within trim
|
||||
for f in trimmed[:start_trim_bump]:
|
||||
if f in already_removed:
|
||||
already_removed.discard(f)
|
||||
|
||||
if end_count > 0:
|
||||
edge_files.update(effective[-end_count:])
|
||||
covered = 0
|
||||
for f in reversed(trimmed):
|
||||
end_trim_bump += 1
|
||||
if f in edge_files:
|
||||
covered += 1
|
||||
if covered == end_count:
|
||||
break
|
||||
for f in trimmed[len(trimmed) - end_trim_bump:]:
|
||||
if f in already_removed:
|
||||
already_removed.discard(f)
|
||||
|
||||
# Adjust trim settings for edge removals
|
||||
if start_bump > 0 or end_bump > 0:
|
||||
if start_trim_bump > 0 or end_trim_bump > 0:
|
||||
trim_start, trim_end = self._folder_trim_settings.get(fid, (0, 0))
|
||||
self._folder_trim_settings[fid] = (trim_start + start_bump, trim_end + end_bump)
|
||||
self._folder_trim_settings[fid] = (trim_start + start_trim_bump, trim_end + end_trim_bump)
|
||||
|
||||
# Clean up empty removed sets
|
||||
if fid in self._removed_files and not self._removed_files[fid]:
|
||||
del self._removed_files[fid]
|
||||
|
||||
# Remaining removals (middle) go to _removed_files
|
||||
middle_files = filenames - edge_files
|
||||
@@ -2947,7 +2987,9 @@ class SequenceLinkerUI(QWidget):
|
||||
self, "Select Destination Folder", start_dir
|
||||
)
|
||||
if path:
|
||||
self.dst_path.blockSignals(True)
|
||||
self._add_to_path_history(self.dst_path, path)
|
||||
self.dst_path.blockSignals(False)
|
||||
self.last_directory = str(Path(path).parent)
|
||||
self._try_resume_session(path)
|
||||
|
||||
@@ -3092,10 +3134,10 @@ class SequenceLinkerUI(QWidget):
|
||||
seen_resolved: set[str] = set()
|
||||
for position_idx, (folder_str, folder_type, trim_start, trim_end) in enumerate(ordered_folders):
|
||||
folder_path = Path(folder_str)
|
||||
# Resolve symlinks for consistent path matching
|
||||
if not folder_path.exists():
|
||||
continue
|
||||
folder_path = folder_path.resolve()
|
||||
# Resolve symlinks for consistent path matching;
|
||||
# keep missing folders so they appear as placeholders
|
||||
if folder_path.exists():
|
||||
folder_path = folder_path.resolve()
|
||||
resolved_str = str(folder_path)
|
||||
fid = self._allocate_folder_id()
|
||||
self.source_folders.append(folder_path)
|
||||
@@ -3234,6 +3276,8 @@ class SequenceLinkerUI(QWidget):
|
||||
|
||||
for i, folder_path in enumerate(self.source_folders):
|
||||
fid = self._folder_ids[i]
|
||||
if not folder_path.is_dir():
|
||||
continue
|
||||
folder_str = str(folder_path)
|
||||
if folder_str not in exported_by_folder:
|
||||
continue
|
||||
@@ -3585,8 +3629,13 @@ class SequenceLinkerUI(QWidget):
|
||||
if session is None:
|
||||
return
|
||||
|
||||
# Set destination path to match the session
|
||||
# Set destination path to match the session — block signals to
|
||||
# prevent _on_destination_changed from triggering a redundant
|
||||
# _try_resume_session (which would load the *latest* session for
|
||||
# this dest, not the one the user picked).
|
||||
self.dst_path.blockSignals(True)
|
||||
self._add_to_path_history(self.dst_path, session.destination)
|
||||
self.dst_path.blockSignals(False)
|
||||
|
||||
self._restore_session_by_id(session)
|
||||
|
||||
@@ -4667,6 +4716,8 @@ class SequenceLinkerUI(QWidget):
|
||||
if sf == folder_path:
|
||||
fid = self._folder_ids[i]
|
||||
break
|
||||
if fid == 0:
|
||||
continue # Folder not in current source_folders, skip
|
||||
|
||||
# Sort files by their sequence index
|
||||
sorted_files = sorted(file_list, key=lambda x: x[0])
|
||||
@@ -5751,7 +5802,9 @@ class SequenceLinkerUI(QWidget):
|
||||
fid_to_pos = {fid: i for i, fid in enumerate(self._folder_ids)}
|
||||
files_by_idx: dict[int, list[str]] = {}
|
||||
for source_dir, filename, folder_idx, file_idx, fid in files:
|
||||
pi = fid_to_pos.get(fid, 0)
|
||||
pi = fid_to_pos.get(fid)
|
||||
if pi is None:
|
||||
continue
|
||||
if pi not in files_by_idx:
|
||||
files_by_idx[pi] = []
|
||||
files_by_idx[pi].append(filename)
|
||||
@@ -5956,11 +6009,13 @@ class SequenceLinkerUI(QWidget):
|
||||
if in_range:
|
||||
out_fmt = settings.output_format.lower()
|
||||
src_ext = source_path.suffix.lower()
|
||||
needs_convert = src_ext != f".{out_fmt}" and src_ext not in (
|
||||
'.jpg' if out_fmt == 'jpeg' else '', '.jpeg' if out_fmt == 'jpg' else ''
|
||||
needs_convert = not (
|
||||
src_ext == f".{out_fmt}"
|
||||
or (out_fmt == 'jpeg' and src_ext == '.jpg')
|
||||
or (out_fmt == 'jpg' and src_ext == '.jpeg')
|
||||
)
|
||||
|
||||
ext = f".{out_fmt}" if needs_convert else source_path.suffix
|
||||
ext = f".{out_fmt}" if needs_convert else source_path.suffix.lower()
|
||||
link_name = f"seq_{output_seq:05d}{ext}"
|
||||
link_path = trans_dest / link_name
|
||||
planned_names.add(link_name)
|
||||
|
||||
Reference in New Issue
Block a user