Skip to content

fix: scenedetect API compat, color overflow, list mutation, temp cleanup - #1

Open
numbpilled2133 wants to merge 1 commit into
numbpill3d:mainfrom
numbpilled2133:fix/scenedetect-color-temp
Open

fix: scenedetect API compat, color overflow, list mutation, temp cleanup#1
numbpilled2133 wants to merge 1 commit into
numbpill3d:mainfrom
numbpilled2133:fix/scenedetect-color-temp

Conversation

@numbpilled2133

@numbpilled2133 numbpilled2133 commented Jun 9, 2026

Copy link
Copy Markdown

PR Summary by Qodo

fix: scenedetect API compat, color overflow, list mutation, temp cleanup
🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

Walkthroughs

User Description

changes

🐛 bug fixes

  • scenedetect compat: handles both v1 (old detect()) and v2 API (SceneManager). Falls back to 5-second segments on any failure.
  • color overflow: _rgb_shift and _datamosh now use np.clip(0, 255).astype(np.uint8) — shifted pixel values could previously overflow uint8, causing visual artifacts.
  • list mutation: update_creatures no longer modifies self.creatures while iterating. Uses a deferred _dead flag + post-loop filter.
  • temp cleanup: gradio UI registers atexit handler to clean up temporary directories.

🧹 cleanup

  • added import shutil and ensured cv2.VideoCapture always receives a string path.
AI Description
• Adds dual-API support for scenedetect v1 (detect()) and v2 (SceneManager), with graceful
  fallback to 5-second segments on failure.
• Fixes uint8 overflow in _rgb_shift and _datamosh by casting to int16 before operations and
  clipping back to [0, 255] before returning.
• Fixes concurrent list mutation in update_creatures by using a _dead flag and post-loop filter
  instead of removing items during iteration.
• Registers an atexit handler in the Gradio UI to clean up temporary output directories after
  process exit.
Diagram
graph TD
    A["gradio_ui.py"] -->|"calls"| B["OrithetCore"]
    B --> C["detect_scenes()"]
    C -->|"SCENEDETECT_V2=True"| D["SceneManager v2 API"]
    C -->|"SCENEDETECT_V2=False"| E["detect() v1 API"]
    C -->|"exception / unavailable"| F["5-sec fallback segments"]
    B --> G["_rgb_shift / _datamosh"]
    G -->|"int16 cast + np.clip"| H["uint8 frame output"]
    B --> I["update_creatures()"]
    I -->|"_dead flag + list filter"| J["safe creature removal"]
    A -->|"atexit.register"| K["shutil.rmtree temp_dir"]

    subgraph Legend
      direction LR
      _mod["Module"] ~~~ _fn(["Function"]) ~~~ _out{{"Output"}}
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Immediate temp cleanup with try/finally
  • ➕ Cleans up after each request rather than at process exit
  • ➕ No handler accumulation across many requests
  • ➖ Gradio may still be serving the file when cleanup runs
  • ➖ Requires knowing when Gradio has finished streaming the response

Recommendation: The PR's approach is sound for all four fixes. One minor concern: registering atexit inside the process_video callback means a new handler is registered on every invocation, accumulating closures over the process lifetime. A cleaner alternative would be to clean up the temp dir immediately after Gradio finishes serving the file (e.g., using a finally block or a Gradio close event), but this is non-trivial with Gradio's async serving model. The atexit approach is a pragmatic tradeoff.

Grey Divider

File Changes

Bug fix (2)
core.py Fix scenedetect API compat, uint8 overflow, and list mutation during iteration +35/-9

Fix scenedetect API compat, uint8 overflow, and list mutation during iteration

• Replaces the single 'detect()' import with a version-detection block that sets 'SCENEDETECT_V2' and routes 'detect_scenes' to either the v2 'SceneManager' path or the legacy 'detect()' call. Ensures 'cv2.VideoCapture' always receives a 'str'. Fixes pixel overflow in '_rgb_shift' (cast to 'int16' before channel shift) and '_datamosh' (clip after 'addWeighted'). Replaces in-loop 'self.creatures.remove(c2)' with a '_dead' flag checked in a post-loop list comprehension.

orithet/core.py


gradio_ui.py Add shutil import and atexit cleanup for temporary output directories +4/-0

Add shutil import and atexit cleanup for temporary output directories

• Imports 'shutil' at module level and registers an 'atexit' handler after each successful video generation to remove the temporary directory via 'shutil.rmtree(..., ignore_errors=True)', preventing temp file accumulation across sessions.

orithet/gradio_ui.py


Grey Divider

Qodo Logo

- fixes scenedetect import to handle both v1 and v2 APIs gracefully
- _rgb_shift and _datamosh now use np.clip to prevent uint8 overflow on shifted pixels
- update_creatures uses deferred kill list instead of modifying list while iterating
- gradio UI registers atexit cleanup for temp directories
- properly passes str to cv2.VideoCapture (was passing Path in some paths)
@devloai

devloai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Unable to trigger custom agent "Code Reviewer". You have run out of credits 😔
Please upgrade your plan or buy additional credits from the subscription page.

@sourcery-ai

sourcery-ai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Updates scenedetect integration to support both v1 and v2 APIs with a robust fallback, fixes image processing overflow and creature list mutation bugs, and adds temporary directory cleanup plus minor API/cleanup adjustments.

File-Level Changes

Change Details Files
Support both scenedetect v1 and v2 APIs with robust fallback behavior and correct VideoCapture usage.
  • Replace direct import of detect() with version-detection logic that tries SceneManager + OpencvVideoStream for v2 and falls back to detect() for v1.
  • Track SCENEDETECT_V2 alongside SCENEDETECT_AVAILABLE to decide which API to call.
  • In detect_scenes, use SceneManager with open_video() when v2 is available, and otherwise call detect() with a string path.
  • On any scenedetect error, fall back to fixed 5-second segments using OpenCV.
  • Ensure cv2.VideoCapture is always constructed with str(video_path).
orithet/core.py
Fix mutation of the creatures list during iteration by deferring removals with a _dead flag.
  • Introduce a _dead flag on creatures instead of removing list elements inside interaction logic.
  • Skip interactions for creatures already marked _dead when iterating pairs.
  • After all interactions, filter self.creatures to remove any with _dead set.
  • Update fuse_creatures to mark c2 as _dead instead of removing it immediately and to replace c1 in-place.
orithet/core.py
Prevent color overflow artifacts in glitch effects by operating in wider integer space and clipping back to uint8.
  • Change _rgb_shift to work on an int16 copy of the frame and then clip results to [0, 255] and cast back to uint8.
  • Change _datamosh to clip the blended frame to [0, 255] and cast back to uint8 before returning.
orithet/core.py
Ensure temporary directories created by the Gradio UI are cleaned up on process exit.
  • Register an atexit handler in process_video to delete temp_dir with shutil.rmtree(ignore_errors=True) after the process exits.
  • Rely on shutil (now imported in the module) to perform recursive directory removal.
orithet/gradio_ui.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@qodo-code-review

qodo-code-review Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0)

Grey Divider


Remediation recommended

1. Unclosed SceneDetect stream 🐞 Bug ☼ Reliability
Description
In detect_scenes(), the SceneDetect v2 path opens a video stream via sd_open_video() but never
closes it, which can leak file handles/resources when many videos are processed. Since
process_videos() calls detect_scenes() for each input video, this can accumulate and eventually
cause failures (e.g., “too many open files”).
Code

orithet/core.py[R129-133]

+                    video_manager = sd_open_video(str(video_path))
+                    sm = SceneManager()
+                    sm.add_detector(ContentDetector())
+                    sm.detect_scenes(video_manager)
+                    scene_list = sm.get_scene_list()
Evidence
process_videos() loops over all video_paths and calls detect_scenes() for each. In the
SceneDetect v2 branch, sd_open_video() is called and the resulting object is used but never
closed/released, unlike the OpenCV fallback which explicitly releases the capture.

orithet/core.py[99-154]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`detect_scenes()` opens a SceneDetect v2 video stream (`sd_open_video(...)`) but never closes it. This can leak file handles/resources when iterating across many input videos.

### Issue Context
`process_videos()` calls `detect_scenes()` once per video. The fallback path properly releases `cv2.VideoCapture`, but the SceneDetect v2 path does not perform any equivalent cleanup.

### Fix Focus Areas
- orithet/core.py[99-154]

### Suggested fix
Wrap the SceneDetect v2 usage in a `try/finally` and close the opened object.

Example pattern:
```py
video = sd_open_video(str(video_path))
try:
   sm = SceneManager()
   sm.add_detector(ContentDetector())
   sm.detect_scenes(video)
   scene_list = sm.get_scene_list()
finally:
   # close/release if available
   for m in ("close", "release"):
       fn = getattr(video, m, None)
       if callable(fn):
           try:
               fn()
           except Exception:
               pass
           break
```
(Use the correct cleanup method for the returned object; the guarded approach avoids hard-coding a single method name.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Temp dir leaks on error 🐞 Bug ☼ Reliability
Description
gradio_ui.process_video() creates a temp directory but only registers atexit cleanup after
engine.run() succeeds; if engine.run() throws, the temp directory is never removed. This can
accumulate orphaned directories on repeated failures.
Code

orithet/gradio_ui.py[R56-60]

                result_path = engine.run(output_path=output_path)
+                # Schedule temp dir cleanup — on return, register for later removal
+                import atexit
+                atexit.register(lambda: shutil.rmtree(temp_dir, ignore_errors=True))
                return result_path, "Video generated successfully!"
Evidence
The code creates temp_dir before calling engine.run(), but the cleanup registration happens only
after the run call. The except block returns an error string and performs no cleanup, leaving
temp_dir behind.

orithet/gradio_ui.py[35-63]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`process_video()` creates `temp_dir` but only schedules cleanup via `atexit.register(...)` after `engine.run(...)` returns successfully. If `engine.run(...)` raises, the exception handler returns an error without removing the created temp directory.

### Issue Context
The UI needs the produced file to remain available after returning, so deleting the directory immediately on success may not be viable. However, on failure there is no output to preserve and the directory can be removed right away.

### Fix Focus Areas
- orithet/gradio_ui.py[35-63]

### Suggested fix
Register the `atexit` handler immediately after creating `temp_dir` (so it always gets registered), and additionally remove the directory in the `except` path.

Example:
```py
temp_dir = tempfile.mkdtemp()
import atexit
atexit.register(lambda: shutil.rmtree(temp_dir, ignore_errors=True))
...
try:
   result_path = engine.run(output_path=output_path)
   return result_path, "Video generated successfully!"
except Exception as e:
   shutil.rmtree(temp_dir, ignore_errors=True)
   return None, f"Error: {str(e)}"
```

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Unused v2 import gate 🐞 Bug ⚙ Maintainability
Description
SCENEDETECT_V2 is set based on importing OpencvVideoStream, but the v2 execution path never uses
OpencvVideoStream; it uses open_video + SceneManager instead. This unnecessary import can disable
the v2 path in environments where OpencvVideoStream is unavailable/moved even though
open_video/SceneManager would work.
Code

orithet/core.py[R29-35]

+    try:
+        from scenedetect import SceneManager
+        from scenedetect.backends import OpencvVideoStream
+        SCENEDETECT_V2 = True
+    except ImportError:
+        from scenedetect import detect as sd_detect
+        SCENEDETECT_V2 = False
Evidence
OpencvVideoStream is imported only to set SCENEDETECT_V2, yet the v2 branch in detect_scenes()
relies on sd_open_video and SceneManager only, so the extra import is not needed and can
incorrectly disable the v2 branch.

orithet/core.py[24-36]
orithet/core.py[124-137]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The code gates `SCENEDETECT_V2` on importing `OpencvVideoStream`, but the v2 path does not reference `OpencvVideoStream` at all. This adds an unnecessary compatibility constraint and can incorrectly flip `SCENEDETECT_V2` to `False`.

### Issue Context
`detect_scenes()` uses `sd_open_video(...)` and `SceneManager()` when `SCENEDETECT_V2` is true; it never touches `OpencvVideoStream`.

### Fix Focus Areas
- orithet/core.py[24-36]
- orithet/core.py[124-137]

### Suggested fix
Only gate `SCENEDETECT_V2` on the APIs actually used (e.g., `SceneManager` and `open_video`), and drop the unused backend import.

Example:
```py
try:
   from scenedetect import ContentDetector, SceneManager, open_video as sd_open_video
   SCENEDETECT_AVAILABLE = True
   SCENEDETECT_V2 = True
except ImportError:
   try:
       from scenedetect import detect as sd_detect, ContentDetector
       SCENEDETECT_AVAILABLE = True
       SCENEDETECT_V2 = False
   except ImportError:
       SCENEDETECT_AVAILABLE = False
       SCENEDETECT_V2 = False
```

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@numbpilled2133, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 51 minutes and 6 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f464e2ef-c60e-4f01-9063-3a95163a6431

📥 Commits

Reviewing files that changed from the base of the PR and between 9b379d0 and 51beaf6.

📒 Files selected for processing (2)
  • orithet/core.py
  • orithet/gradio_ui.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread orithet/core.py
@@ -22,10 +22,21 @@
warnings.filterwarnings('ignore')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suppressing all warnings globally with warnings.filterwarnings('ignore') can mask important runtime or dependency issues, making debugging and maintenance more difficult. It is safer to suppress only specific, known benign warnings, or to do so in a more targeted manner.

Recommendation:
Replace with selective suppression, e.g.:

warnings.filterwarnings('ignore', category=DeprecationWarning)

or restrict the scope to only the libraries or code sections where warnings are expected and harmless.

Comment thread orithet/core.py
Comment on lines 333 to 348
for i in range(len(self.creatures)):
for j in range(i + 1, len(self.creatures)):
c1, c2 = self.creatures[i], self.creatures[j]
if c1.get('_dead') or c2.get('_dead'):
continue
dx = c1['position'][0] - c2['position'][0]
dy = c1['position'][1] - c2['position'][1]
if (dx * dx + dy * dy) < 4.0:
self.handle_creature_interaction(c1, c2, step)

# apply deferred removals
self.creatures = [c for c in self.creatures if not c.get('_dead')]

def handle_creature_interaction(self, c1, c2, step):
color_sim = self.color_similarity(
c1['clip']['metadata']['avg_color'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The nested loop over all pairs of creatures results in O(n^2) complexity per simulation step, which can significantly degrade performance as the number of creatures increases.

Recommendation:
Consider optimizing with a spatial partitioning structure (e.g., grid, quadtree) to reduce the number of pairwise checks, or limit the number of interactions per step to improve scalability.

Comment thread orithet/gradio_ui.py
Comment on lines +57 to +59
# Schedule temp dir cleanup — on return, register for later removal
import atexit
atexit.register(lambda: shutil.rmtree(temp_dir, ignore_errors=True))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The use of atexit.register for cleaning up the temporary directory may lead to resource leaks, especially in a long-running or multi-user environment, as cleanup only occurs on process exit. If multiple requests are handled, temporary directories may accumulate. Consider using a context manager or explicit cleanup after the video is served to ensure timely resource release:

try:
    # ...
    result_path = engine.run(output_path=output_path)
    return result_path, "Video generated successfully!"
finally:
    shutil.rmtree(temp_dir, ignore_errors=True)

Alternatively, schedule cleanup after the file is sent to the client.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • In detect_scenes, the v2 scenedetect path creates a video_manager via sd_open_video but never releases it; consider explicitly calling video_manager.release() (or using a context manager if available) to avoid leaking video resources.
  • The kill_ids = set() variable in update_creatures is declared but never used; it can be removed or wired into the deferred removal logic if it was intended for tracking.
  • You already determine SCENEDETECT_V2 at import time, so the repeated from scenedetect import SceneManager / detect imports inside detect_scenes could be simplified by relying on the module-level imports to reduce per-call overhead and keep the control flow clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `detect_scenes`, the v2 scenedetect path creates a `video_manager` via `sd_open_video` but never releases it; consider explicitly calling `video_manager.release()` (or using a context manager if available) to avoid leaking video resources.
- The `kill_ids = set()` variable in `update_creatures` is declared but never used; it can be removed or wired into the deferred removal logic if it was intended for tracking.
- You already determine `SCENEDETECT_V2` at import time, so the repeated `from scenedetect import SceneManager` / `detect` imports inside `detect_scenes` could be simplified by relying on the module-level imports to reduce per-call overhead and keep the control flow clearer.

## Individual Comments

### Comment 1
<location path="orithet/core.py" line_range="129-132" />
<code_context>
-                scene_list = detect(video_path, ContentDetector())
+                if SCENEDETECT_V2:
+                    from scenedetect import SceneManager
+                    video_manager = sd_open_video(str(video_path))
+                    sm = SceneManager()
+                    sm.add_detector(ContentDetector())
+                    sm.detect_scenes(video_manager)
+                    scene_list = sm.get_scene_list()
+                else:
</code_context>
<issue_to_address>
**issue (bug_risk):** Video manager from scenedetect v2 is never released, which can leak resources.

In the v2 branch, `video_manager` is created but never closed, which can leak file handles/decoder resources over many runs. Please ensure it’s explicitly cleaned up (e.g., `video_manager.release()` or equivalent, ideally in a `finally` block) after `detect_scenes` completes.
</issue_to_address>

### Comment 2
<location path="orithet/gradio_ui.py" line_range="59" />
<code_context>
                 result_path = engine.run(output_path=output_path)
+                # Schedule temp dir cleanup — on return, register for later removal
+                import atexit
+                atexit.register(lambda: shutil.rmtree(temp_dir, ignore_errors=True))
                 return result_path, "Video generated successfully!"

</code_context>
<issue_to_address>
**issue (bug_risk):** Lambda captures `temp_dir` by late binding, so all handlers may target the same directory.

Because the closure captures `temp_dir` by reference, all registered lambdas will point to whatever `temp_dir` was last set to when the process exits. That means earlier temp dirs may never be cleaned up, and the last one may be deleted multiple times. You can fix this by binding the current value into the lambda default, e.g. `atexit.register(lambda d=temp_dir: shutil.rmtree(d, ignore_errors=True))`.
</issue_to_address>

### Comment 3
<location path="orithet/core.py" line_range="25" />
<code_context>
 warnings.filterwarnings('ignore')

 try:
-    from scenedetect import detect, ContentDetector
+    import scenedetect
+    from scenedetect import ContentDetector
</code_context>
<issue_to_address>
**issue (complexity):** Consider centralizing the scenedetect version handling into a single backend helper and removing the unused `kill_ids` variable to streamline the code and reduce duplication.

You can simplify the scenedetect integration and remove a small bit of noise in the creature logic without changing behavior.

### 1. Centralize scenedetect version handling

Right now the v1/v2 branching and imports are split between the module top and `detect_scenes`, and you’re re-importing inside the method. You can keep all functionality and fallbacks intact by resolving the backend once at import time and exposing a single callable used by `detect_scenes`.

For example:

```python
# scenedetect setup
try:
    import scenedetect
    from scenedetect import ContentDetector
    from scenedetect import open_video as sd_open_video

    try:
        # v2 backend
        from scenedetect import SceneManager

        SCENEDETECT_AVAILABLE = True
        SCENEDETECT_BACKEND = "v2"

        def _run_scenedetect(video_path):
            video_manager = sd_open_video(str(video_path))
            sm = SceneManager()
            sm.add_detector(ContentDetector())
            sm.detect_scenes(video_manager)
            return sm.get_scene_list()

    except ImportError:
        # v1 backend
        from scenedetect import detect as sd_detect

        SCENEDETECT_AVAILABLE = True
        SCENEDETECT_BACKEND = "v1"

        def _run_scenedetect(video_path):
            return sd_detect(str(video_path), ContentDetector())

except ImportError:
    SCENEDETECT_AVAILABLE = False
    SCENEDETECT_BACKEND = None

    def _run_scenedetect(video_path):
        return None
```

Then `detect_scenes` becomes simpler and doesn’t need to know about v1 vs v2 or re-import anything:

```python
def detect_scenes(self, video_path):
    if SCENEDETECT_AVAILABLE:
        try:
            scene_list = _run_scenedetect(video_path)
            if scene_list:
                return [(s.get_frames(), e.get_frames()) for s, e in scene_list]
        except Exception:
            # keep broad catch if you want robustness, but it's now localized
            pass

    # fallback: fixed 5-second segments
    cap = cv2.VideoCapture(str(video_path))
    fps = cap.get(cv2.CAP_PROP_FPS)
    frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    cap.release()

    if fps == 0:
        return [(0, frame_count)]

    segment_length = int(fps * 5)
    return [(i, min(i + segment_length, frame_count))
            for i in range(0, frame_count, segment_length)]
```

This removes duplicated imports, centralizes version detection, and limits the places that “know” about the backend, while preserving your v1/v2 behavior and fallback.

### 2. Remove unused variable in `update_creatures`

`kill_ids` is currently unused and just adds noise:

```python
def update_creatures(self, step):
    for c in self.creatures:
        c['position'][0] = max(0, min(30, c['position'][0] + c['velocity'][0] + random.uniform(-0.1, 0.1)))
        c['position'][1] = max(0, min(30, c['position'][1] + c['velocity'][1] + random.uniform(-0.1, 0.1)))
        c['age'] += 1

    for i in range(len(self.creatures)):
        for j in range(i + 1, len(self.creatures)):
            c1, c2 = self.creatures[i], self.creatures[j]
            if c1.get('_dead') or c2.get('_dead'):
                continue
            dx = c1['position'][0] - c2['position'][0]
            dy = c1['position'][1] - c2['position'][1]
            if (dx * dx + dy * dy) < 4.0:
                self.handle_creature_interaction(c1, c2, step)

    # apply deferred removals
    self.creatures = [c for c in self.creatures if not c.get('_dead')]
```

The `_dead` flag + deferred removal pattern is fine and clear as-is; the main win is just removing the unused variable and consolidating scenedetect logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread orithet/core.py
Comment on lines +129 to +132
video_manager = sd_open_video(str(video_path))
sm = SceneManager()
sm.add_detector(ContentDetector())
sm.detect_scenes(video_manager)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Video manager from scenedetect v2 is never released, which can leak resources.

In the v2 branch, video_manager is created but never closed, which can leak file handles/decoder resources over many runs. Please ensure it’s explicitly cleaned up (e.g., video_manager.release() or equivalent, ideally in a finally block) after detect_scenes completes.

Comment thread orithet/gradio_ui.py
result_path = engine.run(output_path=output_path)
# Schedule temp dir cleanup — on return, register for later removal
import atexit
atexit.register(lambda: shutil.rmtree(temp_dir, ignore_errors=True))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Lambda captures temp_dir by late binding, so all handlers may target the same directory.

Because the closure captures temp_dir by reference, all registered lambdas will point to whatever temp_dir was last set to when the process exits. That means earlier temp dirs may never be cleaned up, and the last one may be deleted multiple times. You can fix this by binding the current value into the lambda default, e.g. atexit.register(lambda d=temp_dir: shutil.rmtree(d, ignore_errors=True)).

Comment thread orithet/core.py
warnings.filterwarnings('ignore')

try:
from scenedetect import detect, ContentDetector

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (complexity): Consider centralizing the scenedetect version handling into a single backend helper and removing the unused kill_ids variable to streamline the code and reduce duplication.

You can simplify the scenedetect integration and remove a small bit of noise in the creature logic without changing behavior.

1. Centralize scenedetect version handling

Right now the v1/v2 branching and imports are split between the module top and detect_scenes, and you’re re-importing inside the method. You can keep all functionality and fallbacks intact by resolving the backend once at import time and exposing a single callable used by detect_scenes.

For example:

# scenedetect setup
try:
    import scenedetect
    from scenedetect import ContentDetector
    from scenedetect import open_video as sd_open_video

    try:
        # v2 backend
        from scenedetect import SceneManager

        SCENEDETECT_AVAILABLE = True
        SCENEDETECT_BACKEND = "v2"

        def _run_scenedetect(video_path):
            video_manager = sd_open_video(str(video_path))
            sm = SceneManager()
            sm.add_detector(ContentDetector())
            sm.detect_scenes(video_manager)
            return sm.get_scene_list()

    except ImportError:
        # v1 backend
        from scenedetect import detect as sd_detect

        SCENEDETECT_AVAILABLE = True
        SCENEDETECT_BACKEND = "v1"

        def _run_scenedetect(video_path):
            return sd_detect(str(video_path), ContentDetector())

except ImportError:
    SCENEDETECT_AVAILABLE = False
    SCENEDETECT_BACKEND = None

    def _run_scenedetect(video_path):
        return None

Then detect_scenes becomes simpler and doesn’t need to know about v1 vs v2 or re-import anything:

def detect_scenes(self, video_path):
    if SCENEDETECT_AVAILABLE:
        try:
            scene_list = _run_scenedetect(video_path)
            if scene_list:
                return [(s.get_frames(), e.get_frames()) for s, e in scene_list]
        except Exception:
            # keep broad catch if you want robustness, but it's now localized
            pass

    # fallback: fixed 5-second segments
    cap = cv2.VideoCapture(str(video_path))
    fps = cap.get(cv2.CAP_PROP_FPS)
    frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    cap.release()

    if fps == 0:
        return [(0, frame_count)]

    segment_length = int(fps * 5)
    return [(i, min(i + segment_length, frame_count))
            for i in range(0, frame_count, segment_length)]

This removes duplicated imports, centralizes version detection, and limits the places that “know” about the backend, while preserving your v1/v2 behavior and fallback.

2. Remove unused variable in update_creatures

kill_ids is currently unused and just adds noise:

def update_creatures(self, step):
    for c in self.creatures:
        c['position'][0] = max(0, min(30, c['position'][0] + c['velocity'][0] + random.uniform(-0.1, 0.1)))
        c['position'][1] = max(0, min(30, c['position'][1] + c['velocity'][1] + random.uniform(-0.1, 0.1)))
        c['age'] += 1

    for i in range(len(self.creatures)):
        for j in range(i + 1, len(self.creatures)):
            c1, c2 = self.creatures[i], self.creatures[j]
            if c1.get('_dead') or c2.get('_dead'):
                continue
            dx = c1['position'][0] - c2['position'][0]
            dy = c1['position'][1] - c2['position'][1]
            if (dx * dx + dy * dy) < 4.0:
                self.handle_creature_interaction(c1, c2, step)

    # apply deferred removals
    self.creatures = [c for c in self.creatures if not c.get('_dead')]

The _dead flag + deferred removal pattern is fine and clear as-is; the main win is just removing the unused variable and consolidating scenedetect logic.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for the scenedetect v2 API with a fallback to v1, implements deferred removal of dead creatures to prevent modification of lists during iteration, and schedules temporary directory cleanup in the Gradio UI. The review feedback highlights several critical issues: importing open_video at the top level breaks the v1 fallback on older versions of scenedetect, registering atexit handlers inside the request handler causes a memory leak in persistent Gradio servers, and using .index() on dictionaries is inefficient and risky. Additionally, the feedback points out an unused variable kill_ids and redundant casting and clipping operations in the image processing functions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread orithet/core.py
Comment on lines +27 to +35
from scenedetect import open_video as sd_open_video
# try new v2 API
try:
from scenedetect import SceneManager
from scenedetect.backends import OpencvVideoStream
SCENEDETECT_V2 = True
except ImportError:
from scenedetect import detect as sd_detect
SCENEDETECT_V2 = False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Importing open_video at the top level of the try block will raise an ImportError on older versions of scenedetect (such as v0.5) where open_video does not exist. This will cause the outer except ImportError block to trigger, setting SCENEDETECT_AVAILABLE = False and completely disabling the v1 API fallback. Moving the open_video import inside the inner try block ensures that the v1 fallback remains functional when v2 is unavailable.

Suggested change
from scenedetect import open_video as sd_open_video
# try new v2 API
try:
from scenedetect import SceneManager
from scenedetect.backends import OpencvVideoStream
SCENEDETECT_V2 = True
except ImportError:
from scenedetect import detect as sd_detect
SCENEDETECT_V2 = False
# try new v2 API
try:
from scenedetect import SceneManager
from scenedetect.backends import OpencvVideoStream
from scenedetect import open_video as sd_open_video
SCENEDETECT_V2 = True
except ImportError:
from scenedetect import detect as sd_detect
SCENEDETECT_V2 = False

Comment thread orithet/gradio_ui.py
Comment on lines 6 to 9
import os
import shutil
import tempfile
from .core import OrithetCore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

To prevent memory leaks from registering a new atexit handler on every request, we can define a global list of temporary directories to clean up and register a single atexit handler once at startup.

import os
import shutil
import tempfile
import atexit
from .core import OrithetCore

_TEMP_DIRS_TO_CLEAN = []

def _cleanup_temp_dirs():
    for d in _TEMP_DIRS_TO_CLEAN:
        shutil.rmtree(d, ignore_errors=True)

atexit.register(_cleanup_temp_dirs)

Comment thread orithet/gradio_ui.py
Comment on lines +57 to +59
# Schedule temp dir cleanup — on return, register for later removal
import atexit
atexit.register(lambda: shutil.rmtree(temp_dir, ignore_errors=True))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Registering a new atexit handler on every request creates a memory leak because the handlers accumulate in a global list for the lifetime of the persistent Gradio server. Instead, append the directory to the global _TEMP_DIRS_TO_CLEAN list, which is processed by a single registered atexit handler.

Suggested change
# Schedule temp dir cleanup — on return, register for later removal
import atexit
atexit.register(lambda: shutil.rmtree(temp_dir, ignore_errors=True))
# Schedule temp dir cleanup safely without leaking atexit handlers
_TEMP_DIRS_TO_CLEAN.append(temp_dir)

Comment thread orithet/core.py
Comment on lines 326 to 328
def update_creatures(self, step):
kill_ids = set()
for c in self.creatures:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The kill_ids variable is initialized but never used in update_creatures. It can be safely removed to clean up the code.

Suggested change
def update_creatures(self, step):
kill_ids = set()
for c in self.creatures:
def update_creatures(self, step):
for c in self.creatures:

Comment thread orithet/core.py
Comment on lines +404 to +407
# replace c1 with fused, mark c2 as dead (deferred removal)
idx1 = self.creatures.index(c1)
self.creatures[idx1] = fused
if c2 in self.creatures:
self.creatures.remove(c2)
c2['_dead'] = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using self.creatures.index(c1) is inefficient ($O(N)$ search) and risky because it uses value equality (==) on dictionaries. If two creatures happen to have identical attributes, .index() could return the wrong index and overwrite the wrong creature. Mutating c1 in-place using .clear() and .update() is safer, faster, and avoids the need for index lookups entirely.

Suggested change
# replace c1 with fused, mark c2 as dead (deferred removal)
idx1 = self.creatures.index(c1)
self.creatures[idx1] = fused
if c2 in self.creatures:
self.creatures.remove(c2)
c2['_dead'] = True
# replace c1 with fused, mark c2 as dead (deferred removal)
c1.clear()
c1.update(fused)
c2['_dead'] = True

Comment thread orithet/core.py
Comment on lines +430 to +433
result = frame.copy().astype(np.int16)
result[:, shift:, 0] = frame[:, :-shift, 0]
result[:, :-shift, 2] = frame[:, shift:, 2]
return result
return np.clip(result, 0, 255).astype(np.uint8)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In _rgb_shift, there are no arithmetic operations performed on the pixel values (only slicing and copying). Since the source values are already uint8 (0-255), they cannot overflow or underflow. Casting to np.int16 and calling np.clip is redundant and adds unnecessary performance overhead.

Suggested change
result = frame.copy().astype(np.int16)
result[:, shift:, 0] = frame[:, :-shift, 0]
result[:, :-shift, 2] = frame[:, shift:, 2]
return result
return np.clip(result, 0, 255).astype(np.uint8)
result = frame.copy()
result[:, shift:, 0] = frame[:, :-shift, 0]
result[:, :-shift, 2] = frame[:, shift:, 2]
return result

Comment thread orithet/core.py
Comment on lines +452 to +453
blended = cv2.addWeighted(frame, 0.75, shifted.astype(np.uint8), 0.25, 0)
return np.clip(blended, 0, 255).astype(np.uint8)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In _datamosh, cv2.addWeighted automatically performs saturation clipping and returns a uint8 array when given uint8 inputs. Therefore, calling np.clip and .astype(np.uint8) on the result is redundant. Additionally, shifted is already uint8 because np.roll preserves the array type, so shifted.astype(np.uint8) is also unnecessary.

Suggested change
blended = cv2.addWeighted(frame, 0.75, shifted.astype(np.uint8), 0.25, 0)
return np.clip(blended, 0, 255).astype(np.uint8)
return cv2.addWeighted(frame, 0.75, shifted, 0.25, 0)

@llamapreview llamapreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Code Review by LlamaPReview

🎯 TL;DR & Recommendation

Recommendation: Approve with suggestions
This PR addresses four key bugs: scenedetect API compatibility, color overflow, list mutation, and temp cleanup. The fixes are sound and well-implemented, with minor suggestions for maintainability.

📄 Documentation Diagram

This diagram documents the refactored scene detection flow with dual-API support.

sequenceDiagram
    participant Core as OrithetCore
    participant SM as SceneManager (v2 API)
    participant Detect as detect() (v1 API)
    participant Fallback as Fallback (5-sec segments)

    Core->>Core: Check SCENEDETECT_AVAILABLE
    alt SCENEDETECT_V2=True
        Core->>SM: sd_open_video(str(video_path))
        Core->>SM: SceneManager() and add_detector(ContentDetector())
        SM->>SM: detect_scenes()
        SM-->>Core: scene_list
        Core->>Core: Return frames from get_scene_list()
    else SCENEDETECT_V2=False
        Core->>Detect: detect(str(video_path), ContentDetector())
        Detect-->>Core: scene_list
        Core->>Core: Return frames
    else Exception or unavailable
        note over Core: This PR added the v2 path and graceful fallback
        Core->>Fallback: Use fixed 5-second segments
        Fallback-->>Core: list of frame ranges
    end
Loading

🌟 Strengths

  • Solid bug fixes that address real runtime issues (uint8 overflow, list mutation) with safe patterns.
  • Graceful fallback for scenedetect failures ensures robustness.

💡 Suggestions

  • orithet/core.py: Unnecessary import of OpencvVideoStream may cause spurious fallback to fixed segments on valid scenedetect v2 installations; remove the unused import.
  • orithet/gradio_ui.py: atexit handlers accumulate on each invocation; consider deduplication or immediate cleanup after serving the file.

💡 Have feedback? We'd love to hear it in our GitHub Discussions.
✨ This review was generated by LlamaPReview Advanced, which is free for all open-source projects. Learn more.

Comment thread orithet/core.py
Comment on lines +29 to +35
try:
from scenedetect import SceneManager
from scenedetect.backends import OpencvVideoStream
SCENEDETECT_V2 = True
except ImportError:
from scenedetect import detect as sd_detect
SCENEDETECT_V2 = False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 | Confidence: High

The import guard for scenedetect v2 API includes OpencvVideoStream (from scenedetect.backends import OpencvVideoStream), but this symbol is never used in the v2 code path (the code uses sd_open_video from scenedetect directly). If a legitimate scenedetect v2 installation lacks the OpencvVideoStream backend (e.g., an older v2 release or a stripped install), the condition fails, SCENEDETECT_V2 is set to False, and the code falls back to the v1 detect() function. In scenedetect v2 the detect() function may not exist, causing an ImportError at runtime inside detect_scenes’s else branch, which is caught and silently degrades to fixed 5‑second segments. The user loses scene detection without a clear warning. Removing the unnecessary import of OpencvVideoStream would allow the v2 API to be used in all v2 installations. The existing try block should only check for SceneManager.

Suggested change
try:
from scenedetect import SceneManager
from scenedetect.backends import OpencvVideoStream
SCENEDETECT_V2 = True
except ImportError:
from scenedetect import detect as sd_detect
SCENEDETECT_V2 = False
try:
from scenedetect import SceneManager
SCENEDETECT_V2 = True
except ImportError:
from scenedetect import detect as sd_detect
SCENEDETECT_V2 = False

Evidence: search:OpencvVideoStream

Comment thread orithet/gradio_ui.py
Comment on lines +57 to +59
# Schedule temp dir cleanup — on return, register for later removal
import atexit
atexit.register(lambda: shutil.rmtree(temp_dir, ignore_errors=True))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 | Confidence: High

Each successful call to process_video registers a new atexit handler capturing the current temp_dir lambda. Over time, these handlers accumulate (one per invocation). While ignore_errors=True prevents failures, this pattern wastes resources and can cause confusion if multiple temp directories are removed during shutdown (order is LIFO, but the latest handler’s temp_dir may have been cleaned earlier). A more robust approach is to use a finally block or a Gradio close event to clean up immediately after the file is served. The PR description acknowledges this tradeoff. For a low-traffic tool this is acceptable, but it should be flagged for future improvement.

Code Suggestion:

# Alternative: deduplicate handlers
import atexit
_temp_dirs = set()
@atexit.register
def _cleanup_temp_dirs():
    for d in list(_temp_dirs):
        shutil.rmtree(d, ignore_errors=True)
# In process_video: _temp_dirs.add(temp_dir)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant