Repository: Zulko/moviepy

Issue:
`vfx.TimeSymmetrize` cutoffs 1 second at the end and makes picture jump
<!--
You can format code by putting ``` (that's 3 backticks) on a line by itself at the beginning and end of each code block. For example:

```
from moviepy import *
clip = ColorClip((600, 400), color=(255, 100, 0), duration=2)
```
-->
Hi,
While trying to use `vfx.TimeSymmetrize` I found out that it cutoffs 1 second at the end (start for backward part)

Issue lies in how slices are calculated in Clip class.

#### Expected Behavior

`TimeSymmetrize` Will add exactly same video but in reverse

#### Actual Behavior

Reversed part is cut by 1 second

#### Steps to Reproduce the Problem
<!-- Please include code that demonstrates this problem so that we can reproduce it. For advice on how to do this, see https://stackoverflow.com/help/mcve -->

Just use this effect `TimeSymmetrize`

#### Specifications

  - Python Version: 3.13
  - MoviePy Version: 2.1.1
  - Platform Name: Windows
  - Platform Version: 11

Hints from issue comments:
@chenhaoch: I encountered the same issue. Did you find a solution?
I tried `TimeMirror` and  `TimeSymmetrize`. Both have this issue.
@OsaAjani: Hi, that's the next one on my list, I'll take a look when I can !@OsaAjani: Hi, @chenhaoch and @AndrewFucher could any of you provide some code example triggering the bug, including the media used in the code example.
Did you use `subclipped` before symmetrize ?@chenhaoch: I didn't use `subclipped ` 。

my code: 
```python
    video_files = [os.path.join('videos', f) for f in os.listdir('videos') if f.endswith('.mp4')]
    video_clips = [VideoFileClip(f) for f in video_files]
    
    # 创建正序和逆序视频片段
    processed_clips = []
    for clip in video_clips:
        processed_clips.append(clip)
        mirrored_clip = clip[::-1]
        # processed_clips.append(clip.with_effects([vfx.TimeSymmetrize()]))
        processed_clips.append(mirrored_clip)
        # mirrored_clip.write_videofile('output.mp4', codec='libx264', audio_codec='aac', audio_bitrate='192k')
    
    # 合并视频
    final_video = concatenate_videoclips(processed_clips)
    final_video.write_videofile('video_output.mp4', codec='libx264', audio_codec='aac', audio_bitrate='192k')

```@chenhaoch: and I checked the first and last frames, they don't correspond.   
mirrored_first_0.jpg and original_last_0.jpg are different.

```python
processed_clips = []
    for i, new_clip in enumerate(video_clips):
        # clip = new_clip.subclipped(0,5)
        # 保存原始视频第一帧和最后一帧
        frame = clip.get_frame(0)
        img = Image.fromarray(frame)
        img.save(f'frames/original_first_{i}.jpg')
        
        # 使用iter_frames获取原始视频最后一帧
        clip_frames = list(clip.iter_frames())
        clip_last_frame = clip_frames[-1]
        img = Image.fromarray(clip_last_frame)
        img.save(f'frames/original_last_{i}.jpg')
        
        processed_clips.append(clip)
        
        # 保存TimeMirror处理后的第一帧和最后一帧
        mirrored_clip = clip.with_effects([vfx.TimeMirror()])
        frame = mirrored_clip.get_frame(0)
        img = Image.fromarray(frame)
        img.save(f'frames/mirrored_first_{i}.jpg')
        
        # 使用iter_frames获取TimeMirror处理后的最后一帧
        mirrored_clip_frames = list(mirrored_clip.iter_frames())
        mirrored_clip_last_frame = mirrored_clip_frames[-1]
        img = Image.fromarray(mirrored_clip_last_frame)
        img.save(f'frames/mirrored_last_{i}.jpg')
        
        processed_clips.append(mirrored_clip)
```@OsaAjani: Hi, I think I found the problem, but another matter seems to appear when applying some sub clipping, so I will need a bit of time to fix both.@OsaAjani: PR #2328 fix this.

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1fd0bbf1b..bb3d1c79e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fix GPU h264_nvenc encoding not working.
 - Improve perfs of decorator by pre-computing arguments
 - Fix textclip being cut or of impredictable height (see issues #2325, #2260 and #2268)
+- Fix TimeMirror and TimeSymmetrize cutting last second of clip
 
 ## [v2.1.2](https://github.com/zulko/moviepy/tree/master)
 
diff --git a/moviepy/Clip.py b/moviepy/Clip.py
index 871ed8e3e..e931f5551 100644
--- a/moviepy/Clip.py
+++ b/moviepy/Clip.py
@@ -652,7 +652,7 @@ def __getitem__(self, key):
                 if key.step < 0:
                     # time mirror
                     clip = clip.time_transform(
-                        lambda t: clip.duration - t - 1,
+                        lambda t: clip.duration - t - 1 / self.fps,
                         keep_duration=True,
                         apply_to=apply_to,
                     )
diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py
index 41de84142..8f6835aa6 100644
--- a/moviepy/video/io/ffmpeg_reader.py
+++ b/moviepy/video/io/ffmpeg_reader.py
@@ -476,12 +476,12 @@ def parse(self):
                 # for default streams, set their numbers globally, so it's
                 # easy to get without iterating all
                 if self._current_stream["default"]:
-                    self.result[f"default_{stream_type_lower}_input_number"] = (
-                        input_number
-                    )
-                    self.result[f"default_{stream_type_lower}_stream_number"] = (
-                        stream_number
-                    )
+                    self.result[
+                        f"default_{stream_type_lower}_input_number"
+                    ] = input_number
+                    self.result[
+                        f"default_{stream_type_lower}_stream_number"
+                    ] = stream_number
 
                 # exit chapter
                 if self._current_chapter:


Test patch:
diff --git a/tests/test_fx.py b/tests/test_fx.py
index 746b84913..f7a6e75e5 100644
--- a/tests/test_fx.py
+++ b/tests/test_fx.py
@@ -1057,7 +1057,17 @@ def test_time_mirror():
 
 
 def test_time_symmetrize():
-    clip = BitmapClip([["AA", "AA"], ["BB", "BB"], ["CC", "CC"]], fps=1)
+    clip = BitmapClip(
+        [
+            ["AA", "AA"],
+            ["BB", "BB"],
+            ["CC", "CC"],
+            ["DD", "DD"],
+            ["EE", "EE"],
+            ["FF", "FF"],
+        ],
+        fps=2,
+    )
 
     clip1 = clip.with_effects([vfx.TimeSymmetrize()])
     target1 = BitmapClip(
@@ -1065,6 +1075,12 @@ def test_time_symmetrize():
             ["AA", "AA"],
             ["BB", "BB"],
             ["CC", "CC"],
+            ["DD", "DD"],
+            ["EE", "EE"],
+            ["FF", "FF"],
+            ["FF", "FF"],
+            ["EE", "EE"],
+            ["DD", "DD"],
             ["CC", "CC"],
             ["BB", "BB"],
             ["AA", "AA"],
diff --git a/tests/test_videotools.py b/tests/test_videotools.py
index 52c4e24ae..4a1db9a37 100644
--- a/tests/test_videotools.py
+++ b/tests/test_videotools.py
@@ -283,10 +283,10 @@ def test_FramesMatches_best(n, percent, expected_result):
             0,
             FramesMatches(
                 [
-                    FramesMatch(0.08, 2.92, 0, 0),
-                    FramesMatch(0.2, 2.8, 0, 0),
-                    FramesMatch(0.32, 2.68, 0, 0),
-                    FramesMatch(0.44, 2.56, 0, 0),
+                    FramesMatch(0.52, 3.44, 0, 0),
+                    FramesMatch(0.6400, 3.3200, 0.0000, 0.0000),
+                    FramesMatch(0.7600, 3.2000, 0.0000, 0.0000),
+                    FramesMatch(0.9200, 3.0400, 0.0000, 0.0000),
                 ]
             ),
             id="(media/chaplin.mp4)(1, 3).fx(time_mirror)",
@@ -317,6 +317,7 @@ def test_FramesMatches_select_scenes(
     assert result == expected_result
 
 
+@pytest.mark.skip
 def test_FramesMatches_write_gifs(util):
     video_clip = VideoFileClip("media/chaplin.mp4").subclipped(0, 0.2)
     clip = concatenate_videoclips(