Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract clip as video, slp and pkg.slp #2059

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from

Conversation

ericleonardis
Copy link
Contributor

@ericleonardis ericleonardis commented Dec 17, 2024

Description

This is a new enhancement for an extract clip feature which was proposed in Discussion #1984.

Two new menu items been added to the Labels menu called "Extract Clip and Labels...." which returns a video of the selected clip and slp file and "Extract Clip Labels Package" extracts the clip and saves it as a labels pkg.slp file. I created a new ExportClipAndLabelsDialogue which asks the user for the FPS for the video they are rendering the clip for and saving the slp and pkg.slp files with a reset frame index.

I also wrote a test which checks to see if the mp4, slp, and pkg.slp files are made. I have also written a test which checks to see if the frame numbers and video numbers in the exported labels object are correct.

I have not yet tested this on a dataset of just images, but the slp and pkg.slp look good in the GUI.

Types of changes

  • Bugfix
  • New feature
  • Refactor / Code style update (no logical changes)
  • Build / CI changes
  • Documentation Update
  • Other (explain)

Does this address any currently open issues?

[list open issues here]

Outside contributors checklist

  • Review the guidelines for contributing to this repository
  • Read and sign the CLA and add yourself to the authors list
  • Make sure you are making a pull request against the develop branch (not main). Also you should start your branch off develop
  • Add tests that prove your fix is effective or that your feature works
  • Add necessary documentation (if appropriate)

Thank you for contributing to SLEAP!

❤️

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added new menu options "Extract Clip and Labels..." and "Extract Clip Labels Package..." for exporting video clips with associated labels.
    • Introduced a dialog for exporting clips, allowing users to select frames per second and choose to open the file after saving.
  • Bug Fixes

    • Enhanced export functionality to ensure correct file creation and labeling.
  • Tests

    • Added tests to verify the creation of video clips and the accuracy of exported labels.
    • Expanded testing coverage for the export dialogs to ensure proper functionality and messaging based on ffmpeg availability.

@ericleonardis ericleonardis added enhancement New feature or request 2024-hackathon labels Dec 17, 2024
@ericleonardis ericleonardis self-assigned this Dec 17, 2024
Copy link

coderabbitai bot commented Dec 17, 2024

Walkthrough

This pull request introduces functionality for exporting video clips and their associated labels within the SLEAP application. Key changes include the addition of new menu items in the Labels menu, the implementation of export methods in the commands module, the creation of a dialog for user input regarding export options, and the inclusion of test cases to validate the new features. Users can now extract specific ranges of video frames along with their pose annotations, enhancing the overall data handling capabilities.

Changes

File Change Summary
sleap/gui/app.py Added new "Extract Clip and Labels..." and "Extract Clip Labels Package..." menu items in Labels menu. Added methods exportClipVideo and exportClipPkg in MainWindow.
sleap/gui/commands.py Implemented exportClipVideo and exportClipPkg methods for exporting video clips and labels; removed copy_to_clipboard function.
sleap/gui/dialogs/export_clip.py Created new ExportClipAndLabelsDialog with FPS selection and file opening options. Added methods _get_form_results, on_accept, and get_results.
tests/gui/test_commands.py Added test methods to verify video clip export functionality; updated import statements.
tests/gui/test_dialogs.py Added tests for ExportClipDialog and ExportClipAndLabelsDialog functionalities, including ffmpeg availability checks.

Possibly related PRs

Suggested reviewers

  • roomrys

Poem

🐰 In the garden where clips take flight,
A rabbit hops with pure delight.
Extracting frames, labels in tow,
SLEAP's new feature makes data flow!
With each export, joy we find,
Leaving no frames or labels behind! 🎬

Tip

CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command @coderabbitai generate docstrings to have CodeRabbit automatically generate docstrings for your pull request. We would love to hear your feedback on Discord.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
tests/gui/test_commands.py (2)

1101-1182: Consider adding edge cases to frame range tests.

While the current test coverage is good, consider adding these edge cases:

  • Empty frame range
  • Single frame export
  • Frame range at video boundaries

Example test cases to add:

@pytest.mark.parametrize("subset", [
    (0, 0),  # Single frame
    (0, 9),  # Full range
    (9, 9),  # Last frame
])
def test_ExportVideoClip_edge_cases(subset, tmpdir):
    # Similar setup as existing test
    subset_start, subset_end = subset
    # ... test implementation

12-12: Remove unused import.

The Node import from sleap.skeleton is not used in this file.

-from sleap.skeleton import Node
🧰 Tools
🪛 Ruff (0.8.2)

12-12: sleap.skeleton.Node imported but unused

Remove unused import: sleap.skeleton.Node

(F401)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7dd41de and 9f71476.

📒 Files selected for processing (4)
  • sleap/gui/app.py (1 hunks)
  • sleap/gui/commands.py (2 hunks)
  • sleap/gui/dialogs/export_clip.py (2 hunks)
  • tests/gui/test_commands.py (4 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
sleap/gui/commands.py

3487-3487: sleap.Labels imported but unused

Remove unused import: sleap.Labels

(F401)


3633-3633: Redefinition of unused ExportVideoClip from line 3476

(F811)


3636-3636: sleap.io.visuals.save_labeled_video imported but unused

Remove unused import: sleap.io.visuals.save_labeled_video

(F401)


3639-3639: numpy imported but unused

Remove unused import: numpy

(F401)

tests/gui/test_commands.py

12-12: sleap.skeleton.Node imported but unused

Remove unused import: sleap.skeleton.Node

(F401)

🔇 Additional comments (3)
sleap/gui/dialogs/export_clip.py (1)

31-63: LGTM! Well-structured dialog initialization.

The dialog setup is clean and follows good practices:

  • Proper inheritance from FormBuilderModalDialog
  • Reasonable FPS range (1-240)
  • Clear user feedback about encoding method
tests/gui/test_commands.py (1)

1034-1100: LGTM! Comprehensive test coverage for file creation.

The test thoroughly verifies:

  • Creation of all required files (mp4, slp, pkg.slp)
  • Correct frame count in exported labels
sleap/gui/app.py (1)

749-756: LGTM! Clean menu integration.

The new menu item is well-placed and follows the existing menu structure conventions.

sleap/gui/dialogs/export_clip.py Outdated Show resolved Hide resolved
sleap/gui/commands.py Outdated Show resolved Hide resolved
sleap/gui/commands.py Outdated Show resolved Hide resolved
sleap/gui/commands.py Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (7)
sleap/gui/commands.py (2)

3489-3489: Remove unused import.

The Labels import is not used in this implementation.

-from sleap import Labels
🧰 Tools
🪛 Ruff (0.8.2)

3489-3489: sleap.Labels imported but unused

Remove unused import: sleap.Labels

(F401)


3478-3542: Consider separating video export and label rendering concerns.

The current implementation mixes video export with label rendering functionality. Consider:

  1. Separating the video export and label rendering into distinct methods for better maintainability
  2. Making the render_labels parameter more consistent throughout the implementation
@staticmethod
def do_action(context: CommandContext, params: dict):
    video = context.state["video"]
    labels = context.state["labels"]
    
    # Extract frames
    frame_range = context.state.get("frame_range", (0, video.frames))
    pruned_labels = labels.extract(
        inds=range(*frame_range),
        copy=True,
    )
    
    # Remap frame indices
    for labeled_frame in pruned_labels.labeled_frames:
        labeled_frame.frame_idx -= frame_range[0]
    
    if video not in pruned_labels.videos:
        pruned_labels.add_video(video)
    
+   def _export_video(params: dict, render_labels: bool):
+       save_labeled_video(
+           filename=params["filename"],
+           labels=context.state["labels"] if render_labels else None,
+           video=context.state["video"],
+           frames=list(params["frames"]),
+           fps=params["fps"],
+           color_manager=params["color_manager"],
+           background=params["background"],
+           show_edges=params["show_edges"] if render_labels else False,
+           edge_is_wedge=params["edge_is_wedge"] if render_labels else False,
+           marker_size=params["marker_size"] if render_labels else 0,
+           scale=params["scale"],
+           crop_size_xy=params["crop"],
+           gui_progress=True,
+       )

    # Export video with optional labels
-   save_labeled_video(...)
+   _export_video(params, params["render_labels"])
🧰 Tools
🪛 Ruff (0.8.2)

3489-3489: sleap.Labels imported but unused

Remove unused import: sleap.Labels

(F401)

tests/gui/test_commands.py (2)

1034-1106: Enhance test coverage for ExportVideoClip.

The test verifies basic functionality but could be improved by:

  1. Testing error cases (e.g., invalid frame ranges, video format issues)
  2. Verifying video content/quality
  3. Adding assertions for label content in the exported .slp files
  4. Testing the render_labels parameter

Example additions:

def test_ExportVideoClip_handles_errors(tmpdir):
    """Test that ExportVideoClip handles error cases gracefully."""
    # Test invalid frame range
    with pytest.raises(ValueError):
        # Setup similar to test_ExportVideoClip_creates_files
        params["frames"] = range(100, 0)  # Invalid range
        ExportVideoClip.do_action(context, params)

def test_ExportVideoClip_with_labels(tmpdir):
    """Test that ExportVideoClip correctly handles label rendering."""
    # Setup similar to test_ExportVideoClip_creates_files
    params["render_labels"] = True
    ExportVideoClip.do_action(context, params)
    
    # Verify label content in exported files
    exported_labels = Labels.load_file(str(slp_path))
    assert len(exported_labels.labeled_frames) == frame_count
    # Add more specific assertions about label content

1108-1191: Add edge cases to frame and video list size tests.

Consider adding test cases for:

  1. Empty frame ranges
  2. Single-frame clips
  3. Frame ranges at video boundaries
  4. Multiple videos

Example additions:

@pytest.mark.parametrize("frame_range", [
    (0, 1),  # Single frame
    (0, 0),  # Empty range
    (8, 10), # End of video
    (None, None),  # Full video
])
def test_ExportVideoClip_frame_ranges(tmpdir, frame_range):
    """Test that ExportVideoClip handles various frame ranges correctly."""
    # Setup similar to test_ExportVideoClip_frame_and_video_list_sizes
    if frame_range != (None, None):
        context.state["frame_range"] = frame_range
    
    ExportVideoClip.do_action(context, params)
    # Add assertions specific to each frame range case
sleap/gui/dialogs/export_clip.py (3)

44-44: Document the rationale for FPS range limits.

The FPS range is hardcoded to 1-240. Consider adding a comment explaining these limits or make them configurable constants.

+    # Standard FPS range: 1 fps (timelapse) to 240 fps (slow motion)
     self.fps_input.setRange(1, 240)

33-66: Consider optimizing dialog inheritance structure.

The current implementation duplicates some logic from ExportClipDialog. Consider:

  1. Moving common functionality (like ffmpeg message) to the parent class
  2. Creating a base class for both dialogs

Example refactor:

class BaseClipExportDialog(FormBuilderModalDialog):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._add_ffmpeg_message()
        
    def _add_ffmpeg_message(self):
        from sleap.io.videowriter import VideoWriter
        can_use_ffmpeg = VideoWriter.can_use_ffmpeg()
        message = self._get_encoder_message(can_use_ffmpeg)
        self.add_message(message)
        
    @staticmethod
    def _get_encoder_message(can_use_ffmpeg):
        if can_use_ffmpeg:
            return ("<i><b>MP4</b> file will be encoded using "
                   "system ffmpeg via imageio (preferred option).</i>")
        return ("<i>Unable to use ffmpeg via imageio. "
                "<b>AVI</b> file will be encoded using OpenCV.</i>")

37-38: Consider maintaining consistency in form definition approach.

While ExportClipDialog uses YAML-based form definition, this class uses direct widget manipulation. Consider using a consistent approach across dialogs for better maintainability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f71476 and 117d5a3.

📒 Files selected for processing (3)
  • sleap/gui/commands.py (2 hunks)
  • sleap/gui/dialogs/export_clip.py (2 hunks)
  • tests/gui/test_commands.py (4 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
sleap/gui/commands.py

3489-3489: sleap.Labels imported but unused

Remove unused import: sleap.Labels

(F401)


3640-3640: Redefinition of unused ExportVideoClip from line 3478

(F811)


3643-3643: sleap.io.visuals.save_labeled_video imported but unused

Remove unused import: sleap.io.visuals.save_labeled_video

(F401)


3646-3646: numpy imported but unused

Remove unused import: numpy

(F401)

tests/gui/test_commands.py

12-12: sleap.skeleton.Node imported but unused

Remove unused import: sleap.skeleton.Node

(F401)

🔇 Additional comments (2)
sleap/gui/commands.py (1)

3769-3777: Move or remove unused clipboard function.

The copy_to_clipboard function appears to be:

  1. Unused in the visible code
  2. Not following the command pattern used throughout the file
  3. Misplaced after class definitions
sleap/gui/dialogs/export_clip.py (1)

67-81: Refactor duplicate code in result handling methods.

The on_accept and get_results methods contain identical code for creating the results dictionary.

sleap/gui/commands.py Outdated Show resolved Hide resolved
Copy link

codecov bot commented Dec 17, 2024

Codecov Report

Attention: Patch coverage is 59.79381% with 39 lines in your changes missing coverage. Please review.

Project coverage is 75.30%. Comparing base (7ed1229) to head (351919e).
Report is 66 commits behind head on develop.

Files with missing lines Patch % Lines
sleap/gui/dialogs/export_clip.py 0.00% 24 Missing ⚠️
sleap/gui/commands.py 78.87% 15 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2059      +/-   ##
===========================================
+ Coverage    73.30%   75.30%   +1.99%     
===========================================
  Files          134      134              
  Lines        24087    24832     +745     
===========================================
+ Hits         17658    18700    +1042     
+ Misses        6429     6132     -297     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
sleap/gui/commands.py (2)

3481-3485: Remove unused imports.

The following imports are not used in the implementation:

  • save_labeled_video from sleap.io.visuals
  • numpy as np
-from sleap.io.visuals import save_labeled_video
from sleap.io.video import MediaVideo, Video
import cv2
-import numpy as np
🧰 Tools
🪛 Ruff (0.8.2)

3481-3481: sleap.io.visuals.save_labeled_video imported but unused

Remove unused import: sleap.io.visuals.save_labeled_video

(F401)


3484-3484: numpy imported but unused

Remove unused import: numpy

(F401)


3506-3511: Consider making the video codec configurable.

The code uses a hard-coded 'mp4v' codec which might not be available on all systems. Consider:

  1. Making the codec configurable
  2. Providing a fallback option
  3. Adding error handling for codec initialization
-        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
+        codec = params.get("codec", "mp4v")
+        try:
+            fourcc = cv2.VideoWriter_fourcc(*codec)
+        except Exception as e:
+            logger.warning(f"Failed to initialize {codec} codec, falling back to XVID")
+            fourcc = cv2.VideoWriter_fourcc(*"XVID")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 117d5a3 and 351919e.

📒 Files selected for processing (1)
  • sleap/gui/commands.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
sleap/gui/commands.py

3481-3481: sleap.io.visuals.save_labeled_video imported but unused

Remove unused import: sleap.io.visuals.save_labeled_video

(F401)


3484-3484: numpy imported but unused

Remove unused import: numpy

(F401)

🔇 Additional comments (2)
sleap/gui/commands.py (2)

630-633: LGTM!

The method follows the established pattern for command execution in the CommandContext class.


3607-3615: Move or remove unused clipboard function.

The copy_to_clipboard function appears to be:

  1. Unused in the visible code
  2. Not following the command pattern used throughout the file
  3. Misplaced after class definitions

Either:

  1. Remove the function if it's no longer needed
  2. Move it into a proper command class following the pattern:
class CopyToClipboard(AppCommand):
    @staticmethod
    def do_action(context: CommandContext, params: dict):
        text = params["text"]
        clipboard = QtWidgets.QApplication.clipboard()
        clipboard.clear(mode=clipboard.Clipboard)
        clipboard.setText(text, mode=clipboard.Clipboard)

sleap/gui/commands.py Outdated Show resolved Hide resolved
sleap/gui/commands.py Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 351919e and 08b40cf.

📒 Files selected for processing (2)
  • sleap/gui/app.py (1 hunks)
  • sleap/gui/commands.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • sleap/gui/app.py
🧰 Additional context used
🪛 Ruff (0.8.2)
sleap/gui/commands.py

52-52: sleap.gui.dialogs.export_clip.ExportClipAndLabelsDialog imported but unused

Remove unused import: sleap.gui.dialogs.export_clip.ExportClipAndLabelsDialog

(F401)


3527-3527: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3563-3563: Redefinition of unused ExportClipAndLabelsDialog from line 52

Remove definition: ExportClipAndLabelsDialog

(F811)

🔇 Additional comments (1)
sleap/gui/commands.py (1)

3597-3606: 🛠️ Refactor suggestion

Move clipboard function into a command class.

The copy_to_clipboard function doesn't follow the command pattern used throughout the file. Convert it to a proper command class:

-def copy_to_clipboard(text: str):
-    """Copy a string to the system clipboard.
-
-    Args:
-        text: String to copy to clipboard.
-    """
-    clipboard = QtWidgets.QApplication.clipboard()
-    clipboard.clear(mode=clipboard.Clipboard)
-    clipboard.setText(text, mode=clipboard.Clipboard)

+class CopyToClipboard(AppCommand):
+    @staticmethod
+    def do_action(context: CommandContext, params: dict):
+        """Copy a string to the system clipboard.
+        
+        Args:
+            context: The command context
+            params: Dictionary containing:
+                text: String to copy to clipboard
+        """
+        text = params["text"]
+        clipboard = QtWidgets.QApplication.clipboard()
+        clipboard.clear(mode=clipboard.Clipboard)
+        clipboard.setText(text, mode=clipboard.Clipboard)

Likely invalid or redundant comment.

sleap/gui/commands.py Show resolved Hide resolved
sleap/gui/commands.py Show resolved Hide resolved
sleap/gui/commands.py Outdated Show resolved Hide resolved
sleap/gui/commands.py Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
sleap/gui/commands.py (2)

3520-3533: Add progress feedback for video export.

Consider adding a progress dialog to provide feedback during video export, especially for large frame ranges.

+        progress = QtWidgets.QProgressDialog(
+            "Exporting video...", "Cancel", 0, frame_range[1] - frame_range[0])
+        progress.setWindowModality(QtCore.Qt.WindowModal)
+
         # Write frames to the video
         for frame_idx in range(*frame_range):
+            if progress.wasCanceled():
+                writer.close()
+                os.remove(params["filename"])
+                return
+
             try:
                 frame = video.get_frame(frame_idx)
                 # Convert grayscale to BGR if necessary
                 if frame.ndim == 2:  # Grayscale frames
                     frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
                 writer.add_frame(frame, bgr=True)
+                progress.setValue(frame_idx - frame_range[0])
+                QtWidgets.QApplication.processEvents()
             except KeyError as err:
+                writer.close()
+                os.remove(params["filename"])
                 raise KeyError(f"Failed to load frame {frame_idx} from video.") from err
🧰 Tools
🪛 Ruff (0.8.2)

3531-3531: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3640-3641: Add progress feedback for package save operation.

Consider adding a progress dialog for the package save operation which can be slow with embedded images:

+        progress = QtWidgets.QProgressDialog(
+            "Saving package with embedded images...", "Cancel", 0, 100)
+        progress.setWindowModality(QtCore.Qt.WindowModal)
+        
+        def update_progress(n, total):
+            if progress.wasCanceled():
+                return False
+            progress.setValue(int((n/total) * 100))
+            QtWidgets.QApplication.processEvents()
+            return True
+        
+        try:
             pkg_filename = params["filename"]
-            pruned_labels.save(pkg_filename, with_images=True)
+            pruned_labels.save(
+                pkg_filename,
+                with_images=True,
+                progress_callback=update_progress
+            )
+        except Exception as err:
+            if os.path.exists(pkg_filename):
+                os.remove(pkg_filename)
+            raise RuntimeError(f"Failed to save package: {err}") from err
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08b40cf and ab58597.

📒 Files selected for processing (1)
  • sleap/gui/commands.py (4 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
sleap/gui/commands.py

3531-3531: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (3)
sleap/gui/commands.py (3)

3599-3608: Move or remove unused clipboard function.

The copy_to_clipboard function appears to be:

  1. Unused in the visible code
  2. Not following the command pattern used throughout the file
  3. Misplaced after class definitions

3640-3641: 🛠️ Refactor suggestion

Add error handling for file operations.

The package save operation should handle potential file I/O errors:

-        pkg_filename = params["filename"]
-        pruned_labels.save(pkg_filename, with_images=True)
+        try:
+            pkg_filename = params["filename"]
+            pruned_labels.save(pkg_filename, with_images=True)
+        except Exception as err:
+            if os.path.exists(pkg_filename):
+                os.remove(pkg_filename)
+            raise RuntimeError(f"Failed to save package: {err}") from err

Likely invalid or redundant comment.


3498-3503: 🛠️ Refactor suggestion

Enhance frame range validation.

The frame range validation could be more robust by checking for valid ranges and bounds:

         # Ensure frame range is set; default to all frames if None
         frame_range = context.state.get("frame_range", (0, video.frames))
         
+        # Validate frame range
+        if frame_range[0] < 0 or frame_range[1] > video.frames:
+            raise ValueError(f"Frame range {frame_range} is outside video bounds [0, {video.frames}]")
+        if frame_range[0] >= frame_range[1]:
+            raise ValueError(f"Invalid frame range: {frame_range}")
+
         # Check if clip is selected, raise error if no clip selected
         if frame_range == (0, video.frames) or frame_range == (0, 1):
              raise ValueError("No valid clip frame range selected! Please select a valid frame range using shift + click in the GUI.")

Likely invalid or redundant comment.

sleap/gui/commands.py Outdated Show resolved Hide resolved
sleap/gui/commands.py Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
tests/gui/test_commands.py (5)

12-12: Remove unused import.

The Node import from sleap.skeleton is not used in the code.

-from sleap.skeleton import Node
🧰 Tools
🪛 Ruff (0.8.2)

12-12: sleap.skeleton.Node imported but unused

Remove unused import: sleap.skeleton.Node

(F401)


1074-1074: Document the purpose of the unused variable.

The expected_frame_count variable is calculated but not used. Consider either removing it or adding a comment explaining its intended purpose for future test cases.

🧰 Tools
🪛 Ruff (0.8.2)

1074-1074: Local variable expected_frame_count is assigned to but never used

Remove assignment to unused variable expected_frame_count

(F841)


1035-1109: Improve test coverage and clarity.

A few suggestions to enhance the test:

  1. The test case numbering skips Case 3 (jumps from Case 2 to Case 4).
  2. Consider adding assertions for:
    • Video properties (fps, frame count) of the exported video
    • Content verification of the exported video
  3. Make the test parameters more explicit by using named constants.
 def test_ExportVideoClip_creates_files(tmpdir):
     """Test that ExportVideoClip creates a video clip, .slp, and .pkg.slp."""
+    # Test parameters
+    TEST_FPS = 30
+    TEST_FRAME_COUNT = 10
+    TEST_FRAME_WIDTH = 100
+    TEST_FRAME_HEIGHT = 100
+    TEST_FRAME_RANGE = (0, 4)
 
     # Step 1: Generate a dummy video file using imageio
     video_path = Path(tmpdir, "mock_video.mp4")
-    fps = 30
-    frame_count = 10
-    width, height = 100, 100
+    fps = TEST_FPS
+    frame_count = TEST_FRAME_COUNT
+    width, height = TEST_FRAME_WIDTH, TEST_FRAME_HEIGHT

     # ... rest of the setup ...

     # Assertions
     # Case 1: Assert exported video exists
     assert export_path.exists(), "Exported video file was not created."

     # Case 2: Assert .slp file exists
     slp_path = export_path.with_suffix(".slp")
     assert slp_path.exists(), ".slp file was not created."

-    # Case 4: Assert that the exported labels match the expected number of frames
+    # Case 3: Verify exported video properties
+    exported_video = Video(backend=MediaVideo(filename=str(export_path), grayscale=True))
+    assert exported_video.fps == TEST_FPS, f"Expected {TEST_FPS} fps, got {exported_video.fps}"
+    assert exported_video.num_frames == TEST_FRAME_RANGE[1] - TEST_FRAME_RANGE[0], \
+           f"Expected {TEST_FRAME_RANGE[1] - TEST_FRAME_RANGE[0]} frames"
+
+    # Case 4: Assert that the exported labels match the expected number of frames
     exported_labels = Labels.load_file(str(slp_path))
🧰 Tools
🪛 Ruff (0.8.2)

1074-1074: Local variable expected_frame_count is assigned to but never used

Remove assignment to unused variable expected_frame_count

(F841)


1111-1195: Enhance test robustness and readability.

Consider the following improvements:

  1. Use more descriptive variable names (e.g., subset_frame_range instead of just subset_start, subset_end)
  2. Add edge cases:
    • Empty frame range
    • Single frame range
    • Invalid frame range (end < start)
  3. Organize test parameters at the top for better maintainability
 def test_ExportVideoClip_frame_and_video_list_sizes(tmpdir):
     """Test that ExportVideoClip exports correct length labeled frames and video lists with a subset range."""
+    # Test parameters
+    TEST_PARAMS = {
+        'fps': 30,
+        'total_frames': 10,
+        'frame_range': (2, 7),
+        'dimensions': (100, 100),
+    }
 
     # Generate a dummy video file using imageio
     video_path = Path(tmpdir, "mock_video.mp4")
-    fps = 30
-    total_frames = 10
-    subset_start, subset_end = 2, 7
-    subset_frame_count = subset_end - subset_start
-    width, height = 100, 100
+    fps = TEST_PARAMS['fps']
+    total_frames = TEST_PARAMS['total_frames']
+    subset_frame_range = TEST_PARAMS['frame_range']
+    subset_frame_count = subset_frame_range[1] - subset_frame_range[0]
+    width, height = TEST_PARAMS['dimensions']

     # ... rest of the test ...

+    # Additional edge cases
+    # Test empty frame range
+    with pytest.raises(ValueError):
+        context.state["frame_range"] = (5, 5)
+        ExportClipVideo.do_action(context, params)
+
+    # Test invalid frame range
+    with pytest.raises(ValueError):
+        context.state["frame_range"] = (7, 2)
+        ExportClipVideo.do_action(context, params)

1196-1272: Enhance error handling and validation coverage.

Consider the following improvements:

  1. Add more error cases:
    • Frame range exceeding video length
    • Non-sequential frame range
  2. Validate package contents:
    • Verify skeleton structure
    • Check track information
  3. Use consistent naming with other test methods for better maintainability
 def test_ExportClipPkg_creates_pkg_file(tmpdir):
     """Test that ExportClipPkg creates a .pkg.slp file with selected frame range."""
+    # Test parameters
+    TEST_PARAMS = {
+        'fps': 30,
+        'frame_count': 10,
+        'dimensions': (100, 100),
+        'frame_range': (3, 8),
+    }
 
     # ... test setup ...

     # Case 4: Test the error when no valid frame range is selected
     context.state["frame_range"] = (0, 10)
     with pytest.raises(ValueError, match="No valid clip frame range selected!"):
         ExportClipPkg.do_action(context, params)
+
+    # Case 5: Test error when frame range exceeds video length
+    context.state["frame_range"] = (5, 15)
+    with pytest.raises(ValueError, match="Frame range exceeds video length"):
+        ExportClipPkg.do_action(context, params)
+
+    # Case 6: Validate package contents
+    exported_labels = Labels.load_file(str(pkg_path))
+    # Verify skeleton structure
+    assert len(exported_labels.skeletons) == 1
+    assert len(exported_labels.skeleton.nodes) == 2
+    assert exported_labels.skeleton.node_names == ["node1", "node2"]
+    # Verify track information
+    for lf in exported_labels.labeled_frames:
+        assert len(lf.instances) == 1
+        assert lf.instances[0].track is not None
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab58597 and 5f6fb07.

📒 Files selected for processing (1)
  • tests/gui/test_commands.py (4 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
tests/gui/test_commands.py

12-12: sleap.skeleton.Node imported but unused

Remove unused import: sleap.skeleton.Node

(F401)


1074-1074: Local variable expected_frame_count is assigned to but never used

Remove assignment to unused variable expected_frame_count

(F841)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
tests/gui/test_commands.py (6)

12-12: Remove unused import.

The Node import is not used in the code.

-from sleap.skeleton import Node
🧰 Tools
🪛 Ruff (0.8.2)

12-12: sleap.skeleton.Node imported but unused

Remove unused import: sleap.skeleton.Node

(F401)


1074-1074: Remove unused variable.

The expected_frame_count variable is assigned but never used.

-    expected_frame_count = partial_frame_range[1] - partial_frame_range[0]  # 4 frames
🧰 Tools
🪛 Ruff (0.8.2)

1074-1074: Local variable expected_frame_count is assigned to but never used

Remove assignment to unused variable expected_frame_count

(F841)


1035-1110: Add error handling for video creation.

The test should handle potential errors during video creation and cleanup resources properly.

     # Step 1: Generate a dummy video file using imageio
     video_path = Path(tmpdir, "mock_video.mp4")
+    writer = None
     try:
-        with get_writer(video_path, fps=fps, codec="libx264", format="FFMPEG") as writer:
-            for _ in range(frame_count):
-                frame = np.zeros((height, width), dtype=np.uint8)  # Black frame
-                writer.append_data(frame)
+        writer = get_writer(video_path, fps=fps, codec="libx264", format="FFMPEG")
+        for _ in range(frame_count):
+            frame = np.zeros((height, width), dtype=np.uint8)  # Black frame
+            writer.append_data(frame)
+    except Exception as e:
+        if writer is not None:
+            writer.close()
+        raise RuntimeError(f"Failed to create test video: {str(e)}") from e
+    finally:
+        if writer is not None:
+            writer.close()
🧰 Tools
🪛 Ruff (0.8.2)

1074-1074: Local variable expected_frame_count is assigned to but never used

Remove assignment to unused variable expected_frame_count

(F841)


1196-1202: Improve error messages in parameterized test cases.

The test IDs could be more descriptive to better explain the test scenarios.

 @pytest.mark.parametrize("subset, should_raise_error", [
-    pytest.param((0, 0), True, id="single_frame"),           # Single frame
-    pytest.param((9, 9), True, id="last_frame"),             # Last frame
-    pytest.param((0, 1), True, id="invalid_minimal_range"),  # Invalid minimal range
-    pytest.param((0, 10), True, id="full_range"),            # Full range
-    pytest.param((3, 7), False, id="valid_partial_range"),   # Valid subset
+    pytest.param((0, 0), True, id="single_frame_should_fail"),
+    pytest.param((9, 9), True, id="last_frame_should_fail"),
+    pytest.param((0, 1), True, id="invalid_minimal_range_should_fail"),
+    pytest.param((0, 10), True, id="full_range_should_fail"),
+    pytest.param((3, 7), False, id="valid_partial_range_should_succeed"),
 ])

1284-1361: Add cleanup for created files.

The test should clean up created files even if assertions fail.

+    pkg_path = None
     try:
         # Step 3: Call ExportClipPkg
         ExportClipPkg.do_action(context, params)
+        pkg_path = Path(params["filename"])
 
         # Step 4: Assertions
         # Case 1: Assert .pkg.slp file exists
-        assert pkg_path.exists(), ".pkg.slp file was not created."
+        assert pkg_path.exists(), f"Expected package file not created at {pkg_path}"
 
         # Case 2: Load exported labels and check frame count
         exported_labels = Labels.load_file(str(pkg_path))
         assert len(exported_labels.labeled_frames) == expected_frame_count
+    finally:
+        if pkg_path is not None and pkg_path.exists():
+            pkg_path.unlink()

1362-1368: Improve error messages and add cleanup in edge case tests.

Similar to the video edge cases, improve test IDs and add cleanup.

 @pytest.mark.parametrize("subset, should_raise_error", [
-    pytest.param((0, 0), True, id="single_frame"),
-    pytest.param((9, 9), True, id="later_frame"),
-    pytest.param((0, 1), True, id="invalid_minimal_range"),
-    pytest.param((0, 10), True, id="full_range"),
-    pytest.param((12, 13), True, id="outside_range"),
+    pytest.param((0, 0), True, id="single_frame_should_fail_pkg_export"),
+    pytest.param((9, 9), True, id="later_frame_should_fail_pkg_export"),
+    pytest.param((0, 1), True, id="invalid_minimal_range_should_fail_pkg_export"),
+    pytest.param((0, 10), True, id="full_range_should_fail_pkg_export"),
+    pytest.param((12, 13), True, id="outside_range_should_fail_pkg_export"),
 ])
sleap/gui/commands.py (1)

3523-3523: Remove unused variable assignment.

The app variable is assigned but never used.

-            app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
🧰 Tools
🪛 Ruff (0.8.2)

3523-3523: Local variable app is assigned to but never used

Remove assignment to unused variable app

(F841)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f6fb07 and ddcb213.

📒 Files selected for processing (2)
  • sleap/gui/commands.py (4 hunks)
  • tests/gui/test_commands.py (4 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
tests/gui/test_commands.py

12-12: sleap.skeleton.Node imported but unused

Remove unused import: sleap.skeleton.Node

(F401)


1074-1074: Local variable expected_frame_count is assigned to but never used

Remove assignment to unused variable expected_frame_count

(F841)

sleap/gui/commands.py

3523-3523: Local variable app is assigned to but never used

Remove assignment to unused variable app

(F841)


3547-3547: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3576-3576: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3659-3659: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


3660-3660: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

tests/gui/test_commands.py Outdated Show resolved Hide resolved
sleap/gui/commands.py Show resolved Hide resolved
sleap/gui/commands.py Show resolved Hide resolved
sleap/gui/commands.py Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
sleap/gui/dialogs/export_clip.py (1)

33-81: Consider adding documentation for FPS range and validation.

The implementation looks good, but consider:

  1. Adding a docstring explaining the FPS range (1-240) and why these limits were chosen
  2. Adding validation for the video_fps parameter to ensure it's within the valid range
 class ExportClipAndLabelsDialog(FormBuilderModalDialog):
+    """Dialog for exporting clips with labels.
+    
+    Args:
+        video_fps: Default FPS value from the video metadata (default: 30).
+                  Will be clamped to the valid range of 1-240 FPS.
+    """
     def __init__(self, video_fps=30):
         from sleap.io.videowriter import VideoWriter

         # Initialize with a blank widget (no YAML needed)
         super().__init__(form_widget=QtWidgets.QWidget())

         self.setWindowTitle("Export Clip Options")

         # FPS Field
         self.fps_input = QtWidgets.QSpinBox()
         self.fps_input.setRange(1, 240)  # Common range for most video formats
+        video_fps = max(1, min(240, video_fps))  # Clamp to valid range
         self.fps_input.setValue(video_fps)  # Set default FPS from video
sleap/gui/commands.py (1)

3522-3531: Remove unused variable and improve progress bar initialization.

The app variable is assigned but never used. Also, the progress bar initialization could be simplified.

-        show_progress = os.getenv("PYTEST_RUNNING") != "1"
-        if show_progress:
-            app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
-            progress = QtWidgets.QProgressDialog("Exporting video...", "Cancel", 0, len(range(*frame_range)))
-            progress.setWindowModality(QtCore.Qt.WindowModal)
-            progress.setValue(0)
-        else:
-            progress = None  # Progress bar disabled during tests
+        show_progress = os.getenv("PYTEST_RUNNING") != "1"
+        progress = (QtWidgets.QProgressDialog("Exporting video...", "Cancel", 0, len(range(*frame_range)))
+                   if show_progress else None)
+        if progress is not None:
+            progress.setWindowModality(QtCore.Qt.WindowModal)
🧰 Tools
🪛 Ruff (0.8.2)

3525-3525: Local variable app is assigned to but never used

Remove assignment to unused variable app

(F841)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ddcb213 and 2d00bb8.

📒 Files selected for processing (2)
  • sleap/gui/commands.py (4 hunks)
  • sleap/gui/dialogs/export_clip.py (2 hunks)
🧰 Additional context used
📓 Learnings (1)
sleap/gui/commands.py (1)
Learnt from: ericleonardis
PR: talmolab/sleap#2059
File: sleap/gui/commands.py:0-0
Timestamp: 2024-12-17T23:07:06.722Z
Learning: In `sleap/gui/commands.py`, the `save_labeled_video` function is no longer used for exporting clips; the `Labels` class is now used instead.
🪛 Ruff (0.8.2)
sleap/gui/commands.py

3525-3525: Local variable app is assigned to but never used

Remove assignment to unused variable app

(F841)


3549-3549: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3578-3578: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3661-3661: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


3662-3662: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (2)
sleap/gui/dialogs/export_clip.py (1)

Line range hint 9-31: LGTM!

The ExportClipDialog class correctly handles the video encoding message based on ffmpeg availability.

sleap/gui/commands.py (1)

3484-3486: LGTM!

The class and method signatures are well-defined with clear documentation.

Also applies to: 3487-3493

Comment on lines 3498 to 3505
# Ensure frame range is set; default to all frames if None
frame_range = context.state.get("frame_range", (0, video.frames))
# Validate frame range
if frame_range[0] < 0 or frame_range[1] > video.frames:
raise ValueError(f"Frame range {frame_range} is outside video bounds [0, {video.frames}]")
# Check if clip is selected, raise error if no clip selected
if frame_range == (0, video.frames) or frame_range == (0, 1) or frame_range[0] == frame_range[1]:
raise ValueError("No valid clip frame range selected! Please select a valid frame range using shift + click in the GUI.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider extracting frame range validation.

The frame range validation logic is duplicated between ExportClipVideo and ExportClipPkg. Consider extracting it into a shared helper method.

@staticmethod
def _validate_frame_range(frame_range: tuple, video) -> None:
    """Validates the frame range for clip export.
    
    Args:
        frame_range: Tuple of (start, end) frame indices
        video: Video object containing the frames
        
    Raises:
        ValueError: If frame range is invalid
    """
    if frame_range[0] < 0 or frame_range[1] > video.frames:
        raise ValueError(f"Frame range {frame_range} is outside video bounds [0, {video.frames}]")
    if frame_range == (0, video.frames) or frame_range == (0, 1) or frame_range[0] == frame_range[1]:
        raise ValueError("No valid clip frame range selected! Please select a valid frame range using shift + click in the GUI.")

Comment on lines +3546 to +3549
except Exception as e:
writer.close()
os.remove(params["filename"])
raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling with proper exception chaining.

Use exception chaining to preserve the original error context.

                 except Exception as e:
                     writer.close()
                     os.remove(params["filename"])
-                    raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}")
+                    raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
writer.close()
os.remove(params["filename"])
raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}")
except Exception as e:
writer.close()
os.remove(params["filename"])
raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}") from e
🧰 Tools
🪛 Ruff (0.8.2)

3549-3549: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

Comment on lines +3658 to +3662
try:
pkg_filename = params["filename"]
pruned_labels.save(pkg_filename, with_images=True)
except Exception as e:
raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling in package save operation.

  1. Use proper exception chaining
  2. Include the original error message
  3. Clean up the file on failure
         try:
             pkg_filename = params["filename"]
             pruned_labels.save(pkg_filename, with_images=True)
         except Exception as e:
-            raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}.")
+            if os.path.exists(pkg_filename):
+                os.remove(pkg_filename)
+            raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}: {str(e)}") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
pkg_filename = params["filename"]
pruned_labels.save(pkg_filename, with_images=True)
except Exception as e:
raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}.")
try:
pkg_filename = params["filename"]
pruned_labels.save(pkg_filename, with_images=True)
except Exception as e:
if os.path.exists(pkg_filename):
os.remove(pkg_filename)
raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}: {str(e)}") from e
🧰 Tools
🪛 Ruff (0.8.2)

3661-3661: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


3662-3662: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

Copy link
Collaborator

@roomrys roomrys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indexing into the clip range assumes that

  1. the LabeledFrames were added to the Labels.labels in the same order as the LabeledFrame.frame_idx
  2. the Labels project has a LabeledFrame for every frame index in the video
  3. there is only a single video in the project

sleap/gui/app.py Outdated
Comment on lines 749 to 761
add_menu_item(
labelMenu,
"Extract clip and labels",
"Extract Clip and Labels...",
self.commands.exportClipVideo,
)

add_menu_item(
labelMenu,
"extract clip labels package",
"Extract Clip Labels Package...",
self.commands.exportClipPkg,
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets enable/disable these based on whether a clip is selected in MainWindow._update_gui_state (which is called periodically while the GUI is open). For example:

self._menu_actions["delete clip predictions"].setEnabled(has_frame_range)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion! I have now changed these menu items to be enabled/disabled based on whether a clip is selected.


# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=range(*frame_range),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When inds is either a list or range, then we just call Labels.get for each item in the list or range (int in our case).

This means that we try to grab the item from the Labels.labels: List[LabeledFrame] at the index we provided - which is not the same as grabbing the item at the frame index.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sleap/sleap/io/dataset.py

Lines 779 to 798 in 1eff33d

def extract(self, inds, copy: bool = False) -> "Labels":
"""Extract labeled frames from indices and return a new `Labels` object.
Args:
inds: Any valid indexing keys, e.g., a range, slice, list of label indices,
numpy array, `Video`, etc. See `__getitem__` for full list.
copy: If `True`, create a new copy of all of the extracted labeled frames
and associated labels. If `False` (the default), a shallow copy with
references to the original labeled frames and other objects will be
returned.
Returns:
A new `Labels` object with the specified labeled frames.
This will preserve the other data structures even if they are not found in
the extracted labels, including:
- `Labels.videos`
- `Labels.skeletons`
- `Labels.tracks`
- `Labels.suggestions`
- `Labels.provenance`
"""
lfs = self.__getitem__(inds)

sleap/sleap/io/dataset.py

Lines 636 to 674 in 1eff33d

def __getitem__(
self,
key: Union[
int,
slice,
np.integer,
np.ndarray,
list,
range,
Video,
Tuple[Video, Union[np.integer, np.ndarray, int, list, range]],
],
*secondary_key: Union[
int,
slice,
np.integer,
np.ndarray,
list,
range,
],
) -> Union[LabeledFrame, List[LabeledFrame]]:
"""Return labeled frames matching key or return `None` if not found.
This makes `labels[...]` safe and will not raise an exception if the
item is not found.
Do not call __getitem__ directly, use get instead (get allows kwargs for logic).
If you happen to call __getitem__ directly, get will be called but without any
keyword arguments.
Args:
key: Indexing argument to match against. If `key` is a `Video` or tuple of
`(Video, frame_index)`, frames that match the criteria will be searched
for. If a scalar, list, range or array of integers are provided, the
labels with those linear indices will be returned.
secondary_key: Numerical indexing argument(s) which supplement `key`. Only
used when `key` is a `Video`.
"""
return self.get(key, *secondary_key)

sleap/sleap/io/dataset.py

Lines 768 to 769 in 1eff33d

elif isinstance(key, (list, range)):
return [self.__getitem__(i) for i in key]

sleap/sleap/io/dataset.py

Lines 738 to 739 in 1eff33d

if isinstance(key, int):
return self.labels.__getitem__(key)

sleap/sleap/io/dataset.py

Lines 552 to 555 in 1eff33d

@property
def labels(self):
"""Alias for labeled_frames."""
return self.labeled_frames

labeled_frames: List[LabeledFrame] = attr.ib(default=attr.Factory(list))

Copy link
Contributor Author

@ericleonardis ericleonardis Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I have addressed this suggestion by creating a frame_to_index mapping and then feeding in valid frame indices on line 3666-3677 in command.py.

`

Map frame indices to the actual labeled frame objects

    frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
    valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]

    # Extract only the selected frames into a new Labels object
    pruned_labels = labels.extract(
        inds=[frame_to_index[frame] for frame in valid_frame_indices],
        copy=True  # Ensures a deep copy of the extracted labels
    )

`

This should now be robust to multiple videos and sparsely labelled videos.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
tests/gui/test_dialogs.py (1)

147-173: Remove debug print statement.

The debug print statement on line 165 should be removed as it's not needed for the test.

-        print("Extracted QLabel texts:", label_texts)
sleap/gui/commands.py (2)

3484-3643: Improve error handling and remove unused variable.

  1. Add proper exception chaining to preserve error context
  2. Remove unused app variable
-                    raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}")
+                    raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}") from e

-            app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
+            QtWidgets.QApplication.instance() or QtWidgets.QApplication([])

The implementation is otherwise solid with good progress feedback and frame range validation.

🧰 Tools
🪛 Ruff (0.8.2)

3542-3542: Local variable app is assigned to but never used

Remove assignment to unused variable app

(F841)


3566-3566: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3595-3595: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3645-3710: Extract shared frame range validation logic.

The frame range validation logic is duplicated between ExportClipVideo and ExportClipPkg. Consider extracting it to a shared helper method.

@staticmethod
def _validate_frame_range(frame_range: tuple, video) -> None:
    """Validates frame range for clip export.
    
    Args:
        frame_range: Tuple of (start, end) frame indices
        video: Video object containing frames
        
    Raises:
        ValueError: If frame range is invalid
    """
    if frame_range[0] < 0 or frame_range[1] > video.frames:
        raise ValueError(f"Frame range {frame_range} is outside video bounds [0, {video.frames}]")
    if frame_range == (0, video.frames) or frame_range == (0, 1) or frame_range[0] == frame_range[1]:
        raise ValueError("No valid clip frame range selected!")

Also improve error handling:

         try:
             pkg_filename = params["filename"]
             pruned_labels.save(pkg_filename, with_images=True)
         except Exception as e:
-            raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}.")
+            raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}: {str(e)}") from e
🧰 Tools
🪛 Ruff (0.8.2)

3686-3686: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


3687-3687: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d00bb8 and 39cc496.

📒 Files selected for processing (3)
  • sleap/gui/commands.py (4 hunks)
  • tests/gui/test_commands.py (4 hunks)
  • tests/gui/test_dialogs.py (2 hunks)
🧰 Additional context used
📓 Learnings (1)
sleap/gui/commands.py (2)
Learnt from: ericleonardis
PR: talmolab/sleap#2059
File: sleap/gui/commands.py:0-0
Timestamp: 2024-12-17T23:07:06.722Z
Learning: In `sleap/gui/commands.py`, the `save_labeled_video` function is no longer used for exporting clips; the `Labels` class is now used instead.
Learnt from: ericleonardis
PR: talmolab/sleap#2059
File: sleap/gui/commands.py:0-0
Timestamp: 2024-12-17T22:56:56.102Z
Learning: In `sleap/gui/commands.py`, for the `ExportClipPkg.do_action` method, adding progress feedback for the package save operation is unnecessary, as the current pkg writer and slp savers do not include progress feedback.
🪛 Ruff (0.8.2)
tests/gui/test_dialogs.py

4-4: importlib imported but unused

Remove unused import: importlib

(F401)


5-5: pathlib.Path imported but unused

Remove unused import: pathlib.Path

(F401)


12-12: sleap imported but unused

Remove unused import: sleap

(F401)


19-19: sleap.gui.dialogs.export_clip imported but unused

Remove unused import: sleap.gui.dialogs.export_clip

(F401)


20-20: sleap.gui.app.MainWindow imported but unused

Remove unused import: sleap.gui.app.MainWindow

(F401)

tests/gui/test_commands.py

12-12: sleap.skeleton.Node imported but unused

Remove unused import: sleap.skeleton.Node

(F401)


1262-1262: f-string without any placeholders

Remove extraneous f prefix

(F541)


1311-1311: f-string without any placeholders

Remove extraneous f prefix

(F541)


1367-1367: f-string without any placeholders

Remove extraneous f prefix

(F541)

sleap/gui/commands.py

3542-3542: Local variable app is assigned to but never used

Remove assignment to unused variable app

(F841)


3566-3566: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3595-3595: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3686-3686: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


3687-3687: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (6)
tests/gui/test_dialogs.py (3)

122-141: LGTM! Test verifies ffmpeg availability message.

The test properly verifies that the dialog displays the correct message when ffmpeg is available.


143-145: LGTM! Clean utility function implementation.

The utility function is well-documented and correctly implements HTML tag stripping.


175-240: LGTM! Comprehensive dialog testing.

The test suite thoroughly covers dialog initialization, form handling, and acceptance behavior.

tests/gui/test_commands.py (3)

1035-1113: LGTM! Thorough test coverage for clip export.

The test comprehensively verifies:

  1. Video clip creation
  2. SLP file generation
  3. Frame indices in exported labels
  4. Video filename in exported labels

1114-1190: LGTM! Good test coverage for frame range handling.

The test thoroughly verifies frame range selection and video list handling in exports.


1192-1263: LGTM! Comprehensive edge case testing.

The parameterized test effectively covers various frame range scenarios and verifies proper error handling.

🧰 Tools
🪛 Ruff (0.8.2)

1262-1262: f-string without any placeholders

Remove extraneous f prefix

(F541)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
sleap/gui/app.py (1)

749-764: Consider adding progress feedback for large clips.

For better user experience, consider:

  1. Adding progress feedback during clip extraction
  2. Implementing cancellation support for long-running operations
  3. Adding error handling for cases like insufficient disk space
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39cc496 and b529dd8.

📒 Files selected for processing (1)
  • sleap/gui/app.py (3 hunks)
🔇 Additional comments (2)
sleap/gui/app.py (2)

749-764: LGTM: Menu items properly implemented.

The new menu items for clip extraction are well-integrated into the Labels menu, following the established patterns and conventions.


1137-1140: LGTM: Menu items state properly managed.

The menu items are correctly enabled/disabled based on frame range selection, ensuring a good user experience.

raise ValueError(f"Frame range {frame_range} is outside video bounds [0, {video.frames}]")

# Check if clip is selected, raise error if no clip selected
if frame_range == (0, video.frames) or frame_range == (0, 1) or frame_range[0] == frame_range[1]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is frame_range == (0, video.frames) not a valid choice?

Copy link
Contributor Author

@ericleonardis ericleonardis Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I thought a "clip" meant it wasn't the whole video, but that's just semantics. I went ahead and made that a valid choice.

Unfortunately this has lead to another discovery which has lead me to open a new issue. When I do select the maximum frame range from (0, video.frames) the GUI the selection and frame count display does not match the video. I have opened a new issue at #2074 to describe the mismatch between the video frames and the Shift + Click frame selection tool on the Timeline viewer.

When I select the full clip range I get the ValueError: Frame range (0, 1505) is outside video bounds [0, 1500]. So this PR depends on Issue #2074 being fixed before we can move forward with this one.

Comment on lines +3516 to +3527
# Map frame indices to the actual labeled frame objects
frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]

if not valid_frame_indices:
raise ValueError("No valid labeled frames found in the selected frame range.")

# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=[frame_to_index[frame] for frame in valid_frame_indices],
copy=True # Ensures a deep copy of the extracted labels
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Map frame indices to the actual labeled frame objects
frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]
if not valid_frame_indices:
raise ValueError("No valid labeled frames found in the selected frame range.")
# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=[frame_to_index[frame] for frame in valid_frame_indices],
copy=True # Ensures a deep copy of the extracted labels
)
inds = (video, range(frame_range))
# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=inds,
copy=True # Ensures a deep copy of the extracted labels
)

since we want to run into this case in Labels.extract -> Labels.__getitem__ -> Labels.get:

sleap/sleap/io/dataset.py

Lines 763 to 764 in 66d96ce

elif isinstance(key[1], (list, range)):
return self.find(video=key[0], frame_idx=key[1])

which will call this case of Labels.find -> Labels._cache.find_frames:

sleap/sleap/io/dataset.py

Lines 142 to 147 in 66d96ce

if isinstance(frame_idx, Iterable):
return [
self._frame_idx_map[video][idx]
for idx in frame_idx
if idx in self._frame_idx_map[video]
]

clipboard.clear(mode=clipboard.Clipboard)
clipboard.setText(text, mode=clipboard.Clipboard)
# Ensure frame range is set; default to all frames if None
frame_range = context.state.get("frame_range", (0, video.frames))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we say

Suggested change
frame_range = context.state.get("frame_range", (0, video.frames))
frame_range = context.state.get("frame_range", None)

and raise the no valid clip range error below if frame_range is None?

Comment on lines +3577 to +3581

# Create a new Video object for the output video
new_media_video = MediaVideo(
filename=params["filename"], grayscale=video.channels == 1, bgr=True
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Create a new Video object for the output video
new_media_video = MediaVideo(
filename=params["filename"], grayscale=video.channels == 1, bgr=True
)
# Create a new Video object for the output video
new_media_video = MediaVideo(
filename=params["filename"], grayscale=video.grayscale, bgr=video.bgr
)

Comment on lines +3590 to +3591
# Save the pruned labels
labels_filename = params["filename"].replace(".mp4", ".slp")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use pathlib for path operations

Suggested change
# Save the pruned labels
labels_filename = params["filename"].replace(".mp4", ".slp")
# Save the pruned labels
labels_filename = str(Path(params["filename"]).with_suffix(".slp"))

Comment on lines +3666 to +3681
# Map frame indices to the actual labeled frame objects
frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]

if not valid_frame_indices:
raise ValueError("No valid labeled frames found in the selected frame range.")

# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=[frame_to_index[frame] for frame in valid_frame_indices],
copy=True # Ensures a deep copy of the extracted labels
)

# Remap frame indices in pruned_labels to start from 0
for labeled_frame in pruned_labels.labeled_frames:
labeled_frame.frame_idx -= frame_range[0]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this do_action is the same as the ExportClipVideo class... do you think there is a way we can maybe break out the subparts of do_action into class methods and either combine the ExportClipVideo and ExportClipPkg classes into one class OR create create a base class and subclass to re-use code?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
sleap/gui/commands.py (5)

3509-3514: Improve frame range validation.

  1. The error message for (0, video.frames) is confusing as it's a valid range.
  2. Consider using more specific error types for different validation failures.
-        if frame_range == (0, 1) or frame_range[0] == frame_range[1]:
-            raise ValueError("No valid clip frame range selected! Please select a valid frame range using shift + click in the GUI.")
+        if frame_range[0] >= frame_range[1]:
+            raise ValueError("Invalid frame range: end frame must be greater than start frame.")
+        if frame_range[1] - frame_range[0] < 2:
+            raise ValueError("Selected range is too short. Please select at least 2 frames.")

3542-3542: Remove unused variable assignment.

The app variable is assigned but never used.

-            app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
+            QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
🧰 Tools
🪛 Ruff (0.8.2)

3542-3542: Local variable app is assigned to but never used

Remove assignment to unused variable app

(F841)


3563-3566: Add proper exception chaining.

Use exception chaining to preserve the original error context.

                 except Exception as e:
                     writer.close()
                     os.remove(params["filename"])
-                    raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}")
+                    raise RuntimeError(f"Failed to write frame {frame_idx}: {str(e)}") from e
🧰 Tools
🪛 Ruff (0.8.2)

3566-3566: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3590-3591: Use pathlib for path operations.

Use pathlib for more robust path handling.

-        labels_filename = params["filename"].replace(".mp4", ".slp")
+        labels_filename = str(Path(params["filename"]).with_suffix(".slp"))

3683-3687: Improve error handling in package save operation.

Include the original error message and use proper exception chaining.

         try:
             pkg_filename = params["filename"]
             pruned_labels.save(pkg_filename, with_images=True)
         except Exception as e:
-            raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}.")
+            if os.path.exists(pkg_filename):
+                os.remove(pkg_filename)
+            raise RuntimeError(f"Failed to save labels pkg to {pkg_filename}: {str(e)}") from e
🧰 Tools
🪛 Ruff (0.8.2)

3686-3686: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


3687-3687: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b529dd8 and cd866f3.

📒 Files selected for processing (1)
  • sleap/gui/commands.py (4 hunks)
🧰 Additional context used
📓 Learnings (1)
sleap/gui/commands.py (2)
Learnt from: ericleonardis
PR: talmolab/sleap#2059
File: sleap/gui/commands.py:0-0
Timestamp: 2024-12-17T23:07:06.722Z
Learning: In `sleap/gui/commands.py`, the `save_labeled_video` function is no longer used for exporting clips; the `Labels` class is now used instead.
Learnt from: ericleonardis
PR: talmolab/sleap#2059
File: sleap/gui/commands.py:0-0
Timestamp: 2024-12-17T22:56:56.102Z
Learning: In `sleap/gui/commands.py`, for the `ExportClipPkg.do_action` method, adding progress feedback for the package save operation is unnecessary, as the current pkg writer and slp savers do not include progress feedback.
🪛 Ruff (0.8.2)
sleap/gui/commands.py

3542-3542: Local variable app is assigned to but never used

Remove assignment to unused variable app

(F841)


3566-3566: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3595-3595: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


3686-3686: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


3687-3687: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🔇 Additional comments (1)
sleap/gui/commands.py (1)

3601-3642: LGTM! Dialog implementation is clean and follows Qt patterns.

The dialog implementation correctly handles:

  • User input validation
  • Default FPS from video metadata
  • File selection
  • Parameter population

Comment on lines +3484 to +3485
class ExportClipVideo(AppCommand):
@staticmethod
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider creating a base class for clip export commands.

There's significant code duplication between ExportClipVideo and ExportClipPkg, particularly in frame range validation and labels extraction logic. Consider creating a base class to share common functionality.

class BaseClipExportCommand(AppCommand):
    @staticmethod
    def _validate_frame_range(frame_range: tuple, video) -> None:
        """Validates the frame range for clip export."""
        if frame_range[0] < 0 or frame_range[1] > video.frames:
            raise ValueError(f"Frame range {frame_range} is outside video bounds [0, {video.frames}]")
        if frame_range == (0, 1) or frame_range[0] == frame_range[1]:
            raise ValueError("No valid clip frame range selected! Please select a valid frame range using shift + click in the GUI.")

    @staticmethod
    def _extract_labeled_frames(video, labels, frame_range: tuple) -> Labels:
        """Extracts and remaps labeled frames for the given range."""
        frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
        valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]
        
        if not valid_frame_indices:
            raise ValueError("No valid labeled frames found in the selected frame range.")
            
        pruned_labels = labels.extract(
            inds=[frame_to_index[frame] for frame in valid_frame_indices],
            copy=True
        )
        
        # Remap frame indices
        frame_offset = frame_range[0]
        for labeled_frame in pruned_labels.labeled_frames:
            labeled_frame.frame_idx -= frame_offset
            
        return pruned_labels

Also applies to: 3645-3646

Comment on lines +3516 to +3527
# Map frame indices to the actual labeled frame objects
frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]

if not valid_frame_indices:
raise ValueError("No valid labeled frames found in the selected frame range.")

# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=[frame_to_index[frame] for frame in valid_frame_indices],
copy=True # Ensures a deep copy of the extracted labels
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Revise labels extraction implementation.

The current implementation might not handle frame indices correctly. Consider using the suggested approach from the codebase maintainer.

-        # Map frame indices to the actual labeled frame objects
-        frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
-        valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]
-
-        # Extract only the selected frames into a new Labels object
-        pruned_labels = labels.extract(
-            inds=[frame_to_index[frame] for frame in valid_frame_indices],
-            copy=True  # Ensures a deep copy of the extracted labels
-        )
+        # Extract frames using video and range
+        inds = (video, range(*frame_range))
+        pruned_labels = labels.extract(
+            inds=inds,
+            copy=True  # Ensures a deep copy of the extracted labels
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Map frame indices to the actual labeled frame objects
frame_to_index = {lf.frame_idx: idx for idx, lf in enumerate(labels.labeled_frames) if lf.video == video}
valid_frame_indices = [frame for frame in range(*frame_range) if frame in frame_to_index]
if not valid_frame_indices:
raise ValueError("No valid labeled frames found in the selected frame range.")
# Extract only the selected frames into a new Labels object
pruned_labels = labels.extract(
inds=[frame_to_index[frame] for frame in valid_frame_indices],
copy=True # Ensures a deep copy of the extracted labels
)
# Extract frames using video and range
inds = (video, range(*frame_range))
pruned_labels = labels.extract(
inds=inds,
copy=True # Ensures a deep copy of the extracted labels
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2024-hackathon enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants