Repository: noppanut15/depthviz

Issue:
Feature Request: Time Elapsed Overlay
**Is your feature request related to a problem? Please describe.**
A user wants another overlay video that displays the time elapsed during the dive.

**Describe the solution you'd like**
Add an option (e.g., `--time`) if the user wants to export a time overlay VDO as well.

**Describe alternatives you've considered**
N/A

**Additional context**
N/A


Discussion:

Patch:
diff --git a/README.md b/README.md
index ab93fad..deef4c6 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,8 @@ depthviz -i <input_file> -s <source> -o <output_video.mp4>
 * `--font <font_file>`: Path to a custom font file for the text.
 * `--bg-color <color>`: Background color (e.g., `green`, `'#000000'`). (Default: `black`)
   > 💡 You can use this option to set the background color to match your video editor's [chroma key](https://en.wikipedia.org/wiki/Chroma_key) color, if needed. (e.g., `--bg-color green`)
-* `--stroke-width <0,1,2,...>`: Width of the stroke around the text in pixels. (Default: `2`)
+* `--stroke-width <0,1,2,...>`: Width of the stroke around the text in pixels. (Default: `5`)
+* `--time`: Create a time overlay video. This option generates another video that displays the time elapsed during the dive.
 
 <p align="center"><img src="https://raw.githubusercontent.com/noppanut15/depthviz/main/assets/depth-decimal-places-5s-trimmed.gif" alt="decimal places comparison"/></p>
 
diff --git a/src/depthviz/core.py b/src/depthviz/core.py
deleted file mode 100644
index 6bc7f8e..0000000
--- a/src/depthviz/core.py
+++ /dev/null
@@ -1,272 +0,0 @@
-"""
-Module to create a video that reports the depth in meters from an array input.
-"""
-
-import os.path
-from typing import Tuple, cast
-from moviepy import TextClip, VideoClip, concatenate_videoclips
-from tqdm import tqdm
-from depthviz.logger import DepthVizProgessBarLogger
-from depthviz.optimizer.linear_interpolation import (
-    LinearInterpolationDepth,
-    LinearInterpolationDepthError,
-)
-
-
-BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-DEFAULT_FONT = os.path.abspath(
-    os.path.join(BASE_DIR, "assets/fonts/Open_Sans/static/OpenSans-Bold.ttf")
-)
-DEFAULT_VIDEO_SIZE = (960, 540)
-
-
-class DepthReportVideoCreatorError(Exception):
-    """Base class for exceptions in this module."""
-
-
-class VideoNotRenderError(DepthReportVideoCreatorError):
-    """Exception raised for video not rendered errors."""
-
-
-class VideoFormatError(DepthReportVideoCreatorError):
-    """Exception raised for invalid video format errors."""
-
-
-class DepthReportVideoCreator:
-    """
-    Class to create a video that reports the depth in meters from an array input.
-    """
-
-    def __init__(
-        self,
-        font: str = DEFAULT_FONT,
-        interline: int = -20,
-        color: str = "white",
-        bg_color: str = "black",
-        stroke_color: str = "black",
-        stroke_width: int = 2,
-        align: str = "center",
-        size: Tuple[int, int] = (640, 360),
-        bitrate: str = "5000k",
-        fps: int = 25,
-    ):
-        self.font = font
-        self.fontsize = int(
-            size[1] * 120 / 360
-        )  # 120 is the default font size for 640x360 resolution
-        self.interline = interline
-        self.color = color
-        self.bg_color = bg_color
-        self.stroke_color = stroke_color
-        self.stroke_width = stroke_width
-        self.align = align
-        self.size = size
-        self.bitrate = bitrate
-        self.fps = fps
-        self.final_video = None
-        self.progress_bar_logger_config = {
-            "unit": "f",
-            "color": "#3982d8",
-            "ncols": 70,
-        }
-
-        # Validate the font file
-        self.__font_validate()
-        # Validate the background color
-        self.__bg_color_validate()
-        # Validate the stroke width
-        if not isinstance(stroke_width, int) or stroke_width < 0:
-            raise DepthReportVideoCreatorError(
-                "Invalid stroke width; must be a positive number."
-            )
-
-    def __clip_duration_in_seconds(
-        self, current_pos: int, time_data: list[float]
-    ) -> float:
-        """
-        Returns the total duration of the video in seconds.
-
-        Args:
-            current_pos: The current position in the array.
-            time_data: An array of time values in seconds.
-
-        Returns:
-            The total duration of the video in seconds.
-        """
-        if current_pos == len(time_data) - 1:
-            # If it's the last element, return the difference between the last two elements
-            return abs(time_data[current_pos] - time_data[current_pos - 1])
-        # Otherwise, return the difference between the current and next element
-        return abs(time_data[current_pos + 1] - time_data[current_pos])
-
-    def render_depth_report_video(
-        self,
-        time_data: list[float],
-        depth_data: list[float],
-        decimal_places: int = 0,
-        minus_sign: bool = True,
-    ) -> None:
-        """
-        Creates a video that reports the depth in meters from an array input.
-
-        Args:
-            time_data: An array of time values in seconds.
-            depth_data: An array of depth values in meters.
-            decimal_places: The number of decimal places to round the depth values to.
-            minus_sign: A boolean value to determine if the minus sign should be displayed.
-
-        Returns:
-            The processed video.
-        """
-        # Check the decimal places value
-        if (
-            not isinstance(decimal_places, int)
-            or decimal_places < 0
-            or decimal_places > 2
-        ):
-            raise DepthReportVideoCreatorError(
-                "Invalid decimal places value; must be a number between 0 and 2."
-            )
-        # Interpolate the depth data
-        try:
-            interpolated_depth = LinearInterpolationDepth(
-                times=time_data, depths=depth_data, fps=self.fps
-            )
-            interpolated_depths = interpolated_depth.get_interpolated_depths()
-            interpolated_times = interpolated_depth.get_interpolated_times()
-            # Create a text clip for each depth value and track the progress with a progress bar
-            clips = []
-            clip_count = len(interpolated_times)
-            for i in tqdm(
-                iterable=range(clip_count),
-                desc="Rendering",
-                colour=str(self.progress_bar_logger_config["color"]),
-                unit=str(self.progress_bar_logger_config["unit"]),
-                ncols=cast(int, self.progress_bar_logger_config["ncols"]),
-                bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} ({remaining} remaining)",
-                leave=False,
-            ):
-                duration = self.__clip_duration_in_seconds(i, interpolated_times)
-                if decimal_places == 0:
-                    rounded_current_depth = round(interpolated_depths[i])
-                    if rounded_current_depth == 0:
-                        text = "0m"
-                    else:
-                        text = f"{'-' if minus_sign else ''}{rounded_current_depth}m"
-                else:
-                    current_depth = round(interpolated_depths[i], decimal_places)
-                    if current_depth == 0:
-                        text = f"{0:.{decimal_places}f}m"
-                    else:
-                        text = f"{'-' if minus_sign else ''}{current_depth:.{decimal_places}f}m"
-
-                clip = TextClip(
-                    text=text,
-                    font=self.font,
-                    font_size=self.fontsize,
-                    interline=self.interline,
-                    color=self.color,
-                    bg_color=self.bg_color,
-                    stroke_color=self.stroke_color,
-                    stroke_width=self.stroke_width,
-                    text_align=self.align,
-                    size=self.size,
-                    duration=duration,
-                )
-                clips.append(clip)
-
-            # Concatenate all the clips into a single video
-            self.final_video = concatenate_videoclips(clips)
-        except LinearInterpolationDepthError as e:
-            raise DepthReportVideoCreatorError(f"Interpolation Error; ({e})") from e
-
-    def save(self, path: str) -> None:
-        """
-        Saves the video to a file.
-
-        Args:
-            path: The path to save the video (expected file format: .mp4).
-        """
-        parent_dir = os.path.dirname(path)
-        if parent_dir == "":
-            parent_dir = "./"
-        if os.path.exists(parent_dir):
-            if os.path.isdir(path):
-                raise NameError(
-                    f"{path} is a directory, please add a file name to the path. \
-                        (e.g., path/to/mydepth_video.mp4)"
-                )
-            if self.final_video is not None:
-                if not path.endswith(".mp4"):
-                    raise VideoFormatError(
-                        "Invalid file format: The file format must be .mp4"
-                    )
-                self.final_video.write_videofile(
-                    path,
-                    fps=self.fps,
-                    bitrate=self.bitrate,
-                    logger=DepthVizProgessBarLogger(
-                        description="Exporting",
-                        unit=self.progress_bar_logger_config["unit"],
-                        color=self.progress_bar_logger_config["color"],
-                        ncols=self.progress_bar_logger_config["ncols"],
-                    ),
-                )
-            else:
-                raise VideoNotRenderError(
-                    "Cannot save video because it has not been rendered yet."
-                )
-        else:
-            raise FileNotFoundError(f"Parent directory does not exist: {parent_dir}")
-
-    def get_video(self) -> VideoClip:
-        """
-        Returns the processed video.
-
-        Returns:
-            The processed video.
-        """
-        return self.final_video
-
-    def __font_validate(self) -> None:
-        """
-        Validates the font file.
-
-        Args:
-            font: The font file path.
-        """
-        # Check if the font file exists
-        if not os.path.exists(self.font):
-            raise DepthReportVideoCreatorError(f"Font file not found: {self.font}")
-
-        # Check if the font file is a file
-        if not os.path.isfile(self.font):
-            raise DepthReportVideoCreatorError(
-                f"Font you provided is not a file: {self.font}"
-            )
-
-        # Check if the font file is a valid font file
-        try:
-            TextClip(font=self.font, text="Test", font_size=1)
-        except ValueError as e:
-            raise DepthReportVideoCreatorError(
-                f"Error loading font file: {self.font}, "
-                "make sure it's a valid font file (TrueType or OpenType font)."
-            ) from e
-
-    def __bg_color_validate(self) -> None:
-        """
-        Validates the background color.
-        """
-        # Check if the background color is a valid color
-        try:
-            TextClip(
-                text="Test",
-                font=self.font,
-                font_size=self.fontsize,
-                bg_color=self.bg_color,
-            )
-        except ValueError as e:
-            raise DepthReportVideoCreatorError(
-                f"Invalid background color: {self.bg_color}"
-            ) from e
diff --git a/src/depthviz/main.py b/src/depthviz/main.py
index 5144b66..a341304 100644
--- a/src/depthviz/main.py
+++ b/src/depthviz/main.py
@@ -14,12 +14,15 @@ from depthviz.parsers.shearwater.shearwater_xml_parser import ShearwaterXmlParse
 from depthviz.parsers.garmin.fit_parser import GarminFitParser
 from depthviz.parsers.suunto.fit_parser import SuuntoFitParser
 from depthviz.parsers.manual.csv_parser import ManualCsvParser
-from depthviz.core import (
-    DepthReportVideoCreator,
-    DepthReportVideoCreatorError,
+from depthviz.video.video_creator import (
+    OverlayVideoCreatorError,
     DEFAULT_FONT,
     DEFAULT_VIDEO_SIZE,
+    DEFAULT_BG_COLOR,
+    DEFAULT_STROKE_WIDTH,
 )
+from depthviz.video.depth import DepthReportVideoCreator
+from depthviz.video.time import TimeReportVideoCreator
 
 # Banner for the command line interface
 BANNER = """
@@ -78,17 +81,20 @@ class DepthvizApplication:
         )
         self.parser.add_argument(
             "--bg-color",
-            help="Background color of the video. (default: black)",
+            help=f"Background color of the video. (default: {DEFAULT_BG_COLOR})",
             type=str,
-            default="black",
+            default=DEFAULT_BG_COLOR,
         )
         self.parser.add_argument(
             "--stroke-width",
-            help="Width of the stroke around the text in pixels. (default: 2)",
+            help="Width of the stroke around the text in pixels. "
+            f"(default: {DEFAULT_STROKE_WIDTH})",
             type=int,
-            default=2,
+            default=DEFAULT_STROKE_WIDTH,
+        )
+        self.parser.add_argument(
+            "--time", help="Create a time overlay video.", action="store_true"
         )
-
         self.parser.add_argument(
             "-v",
             "--version",
@@ -96,15 +102,15 @@ class DepthvizApplication:
             version=f"%(prog)s version {__version__}",
         )
 
-    def create_video(
+    def create_depth_video(
         self,
         divelog_parser: DiveLogParser,
         output_path: str,
         decimal_places: int,
         font: str,
         no_minus: bool = False,
-        bg_color: str = "black",
-        stroke_width: int = 2,
+        bg_color: str = DEFAULT_BG_COLOR,
+        stroke_width: int = DEFAULT_STROKE_WIDTH,
     ) -> int:
         """
         Create the depth overlay video.
@@ -119,18 +125,51 @@ class DepthvizApplication:
                 stroke_width=stroke_width,
                 size=DEFAULT_VIDEO_SIZE,
             )
-            depth_report_video_creator.render_depth_report_video(
+            video = depth_report_video_creator.render_depth_report_video(
                 time_data=time_data_from_divelog,
                 depth_data=depth_data_from_divelog,
                 decimal_places=decimal_places,
                 minus_sign=not no_minus,
             )
-            depth_report_video_creator.save(output_path)
-        except DepthReportVideoCreatorError as e:
+            depth_report_video_creator.save(video=video, path=output_path)
+        except OverlayVideoCreatorError as e:
+            print(e)
+            return 1
+
+        print(f"Depth video successfully created: {output_path}")
+        return 0
+
+    def create_time_video(
+        self,
+        divelog_parser: DiveLogParser,
+        output_path: str,
+        font: str,
+        bg_color: str = DEFAULT_BG_COLOR,
+        stroke_width: int = DEFAULT_STROKE_WIDTH,
+    ) -> int:
+        """
+        Create the time overlay video.
+        """
+        try:
+            time_data_from_divelog = divelog_parser.get_time_data()
+            time_report_video_creator = TimeReportVideoCreator(
+                font=font,
+                bg_color=bg_color,
+                stroke_width=stroke_width,
+                size=DEFAULT_VIDEO_SIZE,
+            )
+            video = time_report_video_creator.render_time_report_video(
+                time_data=time_data_from_divelog
+            )
+            time_report_video_creator.save(video=video, path=output_path)
+        except OverlayVideoCreatorError as e:
             print(e)
             return 1
 
-        print(f"Video successfully created: {output_path}")
+        print(
+            "Time video successfully created: "
+            f"{time_report_video_creator.to_time_output_path(output_path)}"
+        )
         return 0
 
     def is_user_input_valid(self, args: argparse.Namespace) -> bool:
@@ -185,7 +224,7 @@ class DepthvizApplication:
             print(e)
             return 1
 
-        return self.create_video(
+        ret_code = self.create_depth_video(
             divelog_parser=divelog_parser,
             output_path=args.output,
             decimal_places=args.decimal_places,
@@ -195,6 +234,22 @@ class DepthvizApplication:
             stroke_width=args.stroke_width,
         )
 
+        # Exit if the depth overlay video creation failed
+        if ret_code != 0:
+            return ret_code
+
+        # Create a time overlay video
+        if args.time:
+            ret_code = self.create_time_video(
+                divelog_parser=divelog_parser,
+                output_path=args.output,
+                font=args.font,
+                bg_color=args.bg_color,
+                stroke_width=args.stroke_width,
+            )
+
+        return ret_code
+
 
 def run() -> int:
     """
diff --git a/src/depthviz/video/__init__.py b/src/depthviz/video/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/depthviz/video/depth.py b/src/depthviz/video/depth.py
new file mode 100644
index 0000000..78a0b22
--- /dev/null
+++ b/src/depthviz/video/depth.py
@@ -0,0 +1,99 @@
+"""
+Module to create a video that reports the depth in meters from an array input.
+"""
+
+from typing import Union
+from moviepy import VideoClip
+from depthviz.video.video_creator import OverlayVideoCreator, OverlayVideoCreatorError
+from depthviz.optimizer.linear_interpolation import (
+    LinearInterpolationDepth,
+    LinearInterpolationDepthError,
+)
+
+
+class DepthReportVideoCreatorError(OverlayVideoCreatorError):
+    """Base class for exceptions in this module."""
+
+
+class DepthReportVideoCreator(OverlayVideoCreator):
+    """
+    Class to create a video that reports the depth in meters from an array input.
+    """
+
+    def __clip_duration_in_seconds(
+        self, current_pos: int, time_data: list[float]
+    ) -> float:
+        """
+        Returns the total duration of the video in seconds.
+
+        Args:
+            current_pos: The current position in the array.
+            time_data: An array of time values in seconds.
+
+        Returns:
+            The total duration of the video in seconds.
+        """
+        if current_pos == len(time_data) - 1:
+            # If it's the last element, return the difference between the last two elements
+            return abs(time_data[current_pos] - time_data[current_pos - 1])
+        # Otherwise, return the difference between the current and next element
+        return abs(time_data[current_pos + 1] - time_data[current_pos])
+
+    def render_depth_report_video(
+        self,
+        time_data: list[float],
+        depth_data: list[float],
+        decimal_places: int = 0,
+        minus_sign: bool = True,
+    ) -> VideoClip:
+        """
+        Creates a video that reports the depth in meters from an array input.
+
+        Args:
+            time_data: An array of time values in seconds.
+            depth_data: An array of depth values in meters.
+            decimal_places: The number of decimal places to round the depth values to.
+            minus_sign: A boolean value to determine if the minus sign should be displayed.
+
+        Returns:
+            The processed video.
+        """
+        # Check the decimal places value
+        if (
+            not isinstance(decimal_places, int)
+            or decimal_places < 0
+            or decimal_places > 2
+        ):
+            raise DepthReportVideoCreatorError(
+                "Invalid decimal places value; must be a number between 0 and 2."
+            )
+        # Interpolate the depth data
+        try:
+            interpolated_depth = LinearInterpolationDepth(
+                times=time_data, depths=depth_data, fps=self.fps
+            )
+            interpolated_depths = interpolated_depth.get_interpolated_depths()
+            interpolated_times = interpolated_depth.get_interpolated_times()
+
+            depth_frame_list: list[dict[str, Union[str, float]]] = []
+            clip_count = len(interpolated_times)
+            for i in range(clip_count):
+                duration = self.__clip_duration_in_seconds(i, interpolated_times)
+                if decimal_places == 0:
+                    rounded_current_depth = round(interpolated_depths[i])
+                    if rounded_current_depth == 0:
+                        text = "0m"
+                    else:
+                        text = f"{'-' if minus_sign else ''}{rounded_current_depth}m"
+                else:
+                    current_depth = round(interpolated_depths[i], decimal_places)
+                    if current_depth == 0:
+                        text = f"{0:.{decimal_places}f}m"
+                    else:
+                        text = f"{'-' if minus_sign else ''}{current_depth:.{decimal_places}f}m"
+                depth_frame_list.append({"text": text, "duration": duration})
+
+            full_video = super().render_text_video(depth_frame_list)
+            return full_video
+        except LinearInterpolationDepthError as e:
+            raise DepthReportVideoCreatorError(f"Interpolation Error; ({e})") from e
diff --git a/src/depthviz/logger.py b/src/depthviz/video/logger.py
similarity index 100%
rename from src/depthviz/logger.py
rename to src/depthviz/video/logger.py
diff --git a/src/depthviz/video/time.py b/src/depthviz/video/time.py
new file mode 100644
index 0000000..b0040b6
--- /dev/null
+++ b/src/depthviz/video/time.py
@@ -0,0 +1,115 @@
+"""
+Module to create a timer video.
+"""
+
+import math
+from typing import Union
+from moviepy import VideoClip
+from depthviz.video.video_creator import (
+    OverlayVideoCreator,
+    OverlayVideoCreatorError,
+    DEFAULT_FONT,
+    DEFAULT_BG_COLOR,
+    DEFAULT_STROKE_WIDTH,
+    DEFAULT_VIDEO_SIZE,
+)
+
+
+class TimeReportVideoCreatorError(OverlayVideoCreatorError):
+    """Base class for exceptions in this module."""
+
+
+class TimeReportVideoCreator(OverlayVideoCreator):
+    """
+    Class to create a video that reports the depth in meters from an array input.
+    """
+
+    def __init__(
+        self,
+        font: str = DEFAULT_FONT,
+        bg_color: str = DEFAULT_BG_COLOR,
+        stroke_width: int = DEFAULT_STROKE_WIDTH,
+        size: tuple[int, int] = DEFAULT_VIDEO_SIZE,
+    ):
+        super().__init__(
+            font=font,
+            bg_color=bg_color,
+            stroke_width=stroke_width,
+            size=size,
+            fps=4,  # Set the frame rate to 4 fps
+        )
+
+    def _convert_time_to_text(self, time: float) -> str:
+        """
+        Converts a time value to a text string.
+
+        Args:
+            time: The time value in seconds.
+
+        Returns:
+            The time value as mm:ss.
+        """
+        minutes = int(time // 60)
+        seconds = int(time % 60)
+        return f"{minutes:02d}:{seconds:02d}"
+
+    def render_time_report_video(
+        self,
+        time_data: list[float],
+    ) -> VideoClip:
+        """
+        Creates a video that reports the depth in meters from an array input.
+
+        Args:
+            time_data: An array of time values in seconds.
+
+        Returns:
+            The processed video.
+        """
+        # Check the time data is not empty
+        if not time_data:
+            raise TimeReportVideoCreatorError("The time data is empty.")
+
+        # Check the time data is positive
+        if any(time < 0 for time in time_data):
+            raise TimeReportVideoCreatorError("The time data contains negative values.")
+
+        # Create a list of time frames
+        time_frame_list: list[dict[str, Union[str, float]]] = []
+        start = 0
+        end = math.ceil(time_data[-1] - time_data[0])
+        for time in range(start, end + 1):
+            for _ in range(self.fps):
+                time_frame_list.append(
+                    {"text": self._convert_time_to_text(time), "duration": 1 / self.fps}
+                )
+        full_video = super().render_text_video(time_frame_list)
+        return full_video
+
+    def to_time_output_path(self, path: str) -> str:
+        """
+        Convert a video path to a time output path by adding a suffix `_time`.
+
+        Args:
+            path: The video path.
+
+        Returns:
+            The time output path.
+        """
+        path_split = path.split(".")
+        path_split[-2] += "_time"
+        return ".".join(path_split)
+
+    def save(
+        self, video: VideoClip, path: str, progress_bar_desc: str = "Exporting"
+    ) -> None:
+        """
+        Save the video to a file.
+
+        Args:
+            video: The video to save.
+            path: The path to save the video to.
+        """
+        # Add a suffix `_time` to the file name
+        path = self.to_time_output_path(path)
+        super().save(video=video, path=path, progress_bar_desc=progress_bar_desc)
diff --git a/src/depthviz/video/video_creator.py b/src/depthviz/video/video_creator.py
new file mode 100644
index 0000000..7e4af02
--- /dev/null
+++ b/src/depthviz/video/video_creator.py
@@ -0,0 +1,208 @@
+"""
+Generic video creator module to create a video from an array input.
+"""
+
+import os.path
+from typing import Tuple, cast, Union
+from moviepy import TextClip, VideoClip, concatenate_videoclips
+from tqdm import tqdm
+from depthviz.video.logger import DepthVizProgessBarLogger
+
+# Default values
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+DEFAULT_FONT = os.path.abspath(
+    os.path.join(BASE_DIR, "../assets/fonts/Open_Sans/static/OpenSans-Bold.ttf")
+)
+DEFAULT_VIDEO_SIZE = (960, 540)
+DEFAULT_VIDEO_SIZE_FOR_TESTING = (640, 360)
+DEFAULT_BG_COLOR = "black"
+DEFAULT_STROKE_WIDTH = 5
+
+
+class OverlayVideoCreatorError(Exception):
+    """Base class for exceptions in this module."""
+
+
+class VideoNotRenderError(OverlayVideoCreatorError):
+    """Exception raised for video not rendered errors."""
+
+
+class VideoFormatError(OverlayVideoCreatorError):
+    """Exception raised for invalid video format errors."""
+
+
+class OverlayVideoCreator:
+    """
+    Generic class to create an overlay video from an array input.
+    """
+
+    def __init__(
+        self,
+        font: str = DEFAULT_FONT,
+        interline: int = -20,
+        color: str = "white",
+        bg_color: str = DEFAULT_BG_COLOR,
+        stroke_color: str = "black",
+        stroke_width: int = DEFAULT_STROKE_WIDTH,
+        align: str = "center",
+        size: Tuple[int, int] = DEFAULT_VIDEO_SIZE_FOR_TESTING,
+        bitrate: str = "5000k",
+        fps: int = 25,
+    ):
+        self.font = font
+        self.fontsize = int(
+            size[1] * 120 / 360
+        )  # 120 is the default font size for 640x360 resolution
+        self.interline = interline
+        self.color = color
+        self.bg_color = bg_color
+        self.stroke_color = stroke_color
+        self.stroke_width = stroke_width
+        self.align = align
+        self.size = size
+        self.bitrate = bitrate
+        self.fps = fps
+        self.progress_bar_logger_config = {
+            "unit": "f",
+            "color": "#3982d8",
+            "ncols": 70,
+        }
+
+        # Validate the font file
+        self.__font_validate()
+        # Validate the background color
+        self.__bg_color_validate()
+        # Validate the stroke width
+        if not isinstance(stroke_width, int) or stroke_width < 0:
+            raise OverlayVideoCreatorError(
+                "Invalid stroke width; must be a positive number."
+            )
+
+    def render_text_video(
+        self,
+        text_list: list[dict[str, Union[str, float]]],
+        progress_bar_desc: str = "Rendering",
+    ) -> VideoClip:
+        """
+        Creates a video from an array input.
+
+        Args:
+            text_list: A list of dictionaries containing the text and duration for each frame.
+            progress_bar_desc: The description for the progress bar.
+
+        Returns:
+            The processed video.
+        """
+        # Create a text clip for each text value and track the progress with a progress bar
+        clips = []
+        clip_count = len(text_list)
+        for i in tqdm(
+            iterable=range(clip_count),
+            desc=progress_bar_desc,
+            colour=str(self.progress_bar_logger_config["color"]),
+            unit=str(self.progress_bar_logger_config["unit"]),
+            ncols=cast(int, self.progress_bar_logger_config["ncols"]),
+            bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} ({remaining} remaining)",
+            leave=False,
+        ):
+
+            clip = TextClip(
+                text=text_list[i]["text"],
+                font=self.font,
+                font_size=self.fontsize,
+                interline=self.interline,
+                color=self.color,
+                bg_color=self.bg_color,
+                stroke_color=self.stroke_color,
+                stroke_width=self.stroke_width,
+                text_align=self.align,
+                size=self.size,
+                duration=text_list[i]["duration"],
+            )
+            clips.append(clip)
+
+        # Concatenate all the clips into a single video
+        full_video = concatenate_videoclips(clips)
+        return full_video
+
+    def save(
+        self, video: VideoClip, path: str, progress_bar_desc: str = "Exporting"
+    ) -> None:
+        """
+        Saves the video to a file.
+
+        Args:
+            video: The video to save.
+            path: The path to save the video (expected file format: .mp4).
+        """
+        parent_dir = os.path.dirname(path)
+        if parent_dir == "":
+            parent_dir = "./"
+        if os.path.exists(parent_dir):
+            if os.path.isdir(path):
+                raise NameError(
+                    f"{path} is a directory, please add a file name to the path. \
+                        (e.g., path/to/mydepth_video.mp4)"
+                )
+            if video is not None:
+                if not path.endswith(".mp4"):
+                    raise VideoFormatError(
+                        "Invalid file format: The file format must be .mp4"
+                    )
+                video.write_videofile(
+                    path,
+                    fps=self.fps,
+                    bitrate=self.bitrate,
+                    logger=DepthVizProgessBarLogger(
+                        description=progress_bar_desc,
+                        unit=cast(str, self.progress_bar_logger_config["unit"]),
+                        color=cast(str, self.progress_bar_logger_config["color"]),
+                        ncols=cast(int, self.progress_bar_logger_config["ncols"]),
+                    ),
+                )
+            else:
+                raise VideoNotRenderError(
+                    "Cannot save video because it has not been rendered yet."
+                )
+        else:
+            raise FileNotFoundError(f"Parent directory does not exist: {parent_dir}")
+
+    def __font_validate(self) -> None:
+        """
+        Validates the font file.
+        """
+        # Check if the font file exists
+        if not os.path.exists(self.font):
+            raise OverlayVideoCreatorError(f"Font file not found: {self.font}")
+
+        # Check if the font file is a file
+        if not os.path.isfile(self.font):
+            raise OverlayVideoCreatorError(
+                f"Font you provided is not a file: {self.font}"
+            )
+
+        # Check if the font file is a valid font file
+        try:
+            TextClip(font=self.font, text="Test", font_size=1)
+        except ValueError as e:
+            raise OverlayVideoCreatorError(
+                f"Error loading font file: {self.font}, "
+                "make sure it's a valid font file (TrueType or OpenType font)."
+            ) from e
+
+    def __bg_color_validate(self) -> None:
+        """
+        Validates the background color.
+        """
+        # Check if the background color is a valid color
+        try:
+            TextClip(
+                text="Test",
+                font=self.font,
+                font_size=self.fontsize,
+                bg_color=self.bg_color,
+            )
+        except ValueError as e:
+            raise OverlayVideoCreatorError(
+                f"Invalid background color: {self.bg_color}"
+            ) from e


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 51d7cfa..7af93fe 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -10,7 +10,12 @@ from unittest import mock
 import pytest
 from depthviz.main import DepthvizApplication, run
 from depthviz.parsers.generic.generic_divelog_parser import DiveLogParser
-from depthviz.core import DepthReportVideoCreatorError, DEFAULT_FONT
+from depthviz.video.video_creator import (
+    OverlayVideoCreatorError,
+    DEFAULT_FONT,
+    DEFAULT_STROKE_WIDTH,
+)
+from depthviz.video.time import TimeReportVideoCreatorError
 
 
 # Mock the DEFAULT_VIDEO_SIZE constant to lower the resolution for faster tests.
@@ -20,16 +25,16 @@ class TestMainCLI:
     Test suite for the main CLI.
     """
 
-    def _mock_depthviz_create_video(
+    def _mock_depthviz_create_depth_video(
         self,
         output_path: str,
         *_args: Any,
         **_kwargs: Any,
     ) -> None:
         """
-        Mock the DepthvizApplication create_video method.
+        Mock the DepthvizApplication create_depth_video method.
         """
-        print(f"Video successfully created: {output_path}")
+        print(f"Depth video successfully created: {output_path}")
 
     def test_main(self, capsys: pytest.CaptureFixture[str]) -> None:
         """
@@ -72,7 +77,10 @@ class TestMainCLI:
         app = DepthvizApplication()
         app.main()
         captured = capsys.readouterr()
-        assert f"Video successfully created: {output_path.as_posix()}" in captured.out
+        assert (
+            f"Depth video successfully created: {output_path.as_posix()}"
+            in captured.out
+        )
 
     def test_main_with_invalid_csv(
         self,
@@ -234,7 +242,10 @@ class TestMainCLI:
         app = DepthvizApplication()
         app.main()
         captured = capsys.readouterr()
-        assert f"Video successfully created: {output_path.as_posix()}" in captured.out
+        assert (
+            f"Depth video successfully created: {output_path.as_posix()}"
+            in captured.out
+        )
 
     def test_main_with_args_manual_mode(
         self,
@@ -262,7 +273,10 @@ class TestMainCLI:
         app = DepthvizApplication()
         app.main()
         captured = capsys.readouterr()
-        assert f"Video successfully created: {output_path.as_posix()}" in captured.out
+        assert (
+            f"Depth video successfully created: {output_path.as_posix()}"
+            in captured.out
+        )
 
     def test_main_with_args_no_minus_option(
         self,
@@ -291,14 +305,17 @@ class TestMainCLI:
         app = DepthvizApplication()
         app.main()
         captured = capsys.readouterr()
-        assert f"Video successfully created: {output_path.as_posix()}" in captured.out
+        assert (
+            f"Depth video successfully created: {output_path.as_posix()}"
+            in captured.out
+        )
 
-    @mock.patch("depthviz.main.DepthvizApplication.create_video")
+    @mock.patch("depthviz.main.DepthvizApplication.create_depth_video")
     @mock.patch("depthviz.parsers.garmin.fit_parser.GarminFitParser.parse")
     def test_main_with_args_garmin(
         self,
         mock_parse: mock.MagicMock,
-        mock_create_video: mock.MagicMock,
+        mock_create_depth_video: mock.MagicMock,
         capsys: pytest.CaptureFixture[str],
         tmp_path: pathlib.Path,
     ) -> None:
@@ -321,21 +338,24 @@ class TestMainCLI:
 
         # Mock the Garmin FIT parser parse method.
         mock_parse.side_effect = mock.Mock()
-        # Mock the create_video method.
-        mock_create_video.side_effect = self._mock_depthviz_create_video
+        # Mock the create_depth_video method.
+        mock_create_depth_video.side_effect = self._mock_depthviz_create_depth_video
         app.main()
         captured = capsys.readouterr()
 
-        assert f"Video successfully created: {output_path.as_posix()}" in captured.out
+        assert (
+            f"Depth video successfully created: {output_path.as_posix()}"
+            in captured.out
+        )
         mock_parse.assert_called_once_with(file_path=input_path.as_posix())
-        mock_create_video.assert_called_once_with(
+        mock_create_depth_video.assert_called_once_with(
             divelog_parser=mock.ANY,
             output_path=output_path.as_posix(),
             decimal_places=0,
             no_minus=False,
             font=DEFAULT_FONT,
             bg_color="black",
-            stroke_width=2,
+            stroke_width=DEFAULT_STROKE_WIDTH,
         )
 
     @pytest.mark.parametrize(
@@ -391,7 +411,7 @@ class TestMainCLI:
     @mock.patch("depthviz.main.DepthReportVideoCreator.save")
     @mock.patch("depthviz.main.DepthReportVideoCreator.render_depth_report_video")
     @mock.patch("depthviz.main.DepthReportVideoCreator")
-    def test_create_video_failure(
+    def test_create_depth_video_failure(
         self,
         mock_depth_report_video_creator: mock.Mock,
         mock_render_depth_report_video: mock.Mock,
@@ -399,7 +419,7 @@ class TestMainCLI:
         capsys: pytest.CaptureFixture[str],
     ) -> None:
         """
-        Test the create_video method for failure in video creation.
+        Test the create_depth_video method for failure in video creation.
         """
         # Create an instance of the main application.
         app = DepthvizApplication()
@@ -411,16 +431,16 @@ class TestMainCLI:
 
         # Mock the DepthReportVideoCreator class and its methods.
         mock_depth_report_video_creator.return_value = mock_depth_report_video_creator
-        mock_render_depth_report_video.side_effect = DepthReportVideoCreatorError(
+        mock_render_depth_report_video.side_effect = OverlayVideoCreatorError(
             "Error rendering video"
         )
 
-        # Call the create_video method with the mocked objects.
+        # Call the create_depth_video method with the mocked objects.
         output_path = "test.mp4"
         decimal_places = 2
         no_minus = False
 
-        ret_code = app.create_video(
+        ret_code = app.create_depth_video(
             divelog_parser=divelog_parser,
             output_path=output_path,
             decimal_places=decimal_places,
@@ -444,12 +464,12 @@ class TestMainCLI:
         )
         mock_save.assert_not_called()
 
-    @mock.patch("depthviz.main.DepthvizApplication.create_video")
+    @mock.patch("depthviz.main.DepthvizApplication.create_depth_video")
     @mock.patch("depthviz.parsers.suunto.fit_parser.SuuntoFitParser.parse")
     def test_main_with_args_suunto(
         self,
         mock_parse: mock.MagicMock,
-        mock_create_video: mock.MagicMock,
+        mock_create_depth_video: mock.MagicMock,
         capsys: pytest.CaptureFixture[str],
         tmp_path: pathlib.Path,
     ) -> None:
@@ -472,21 +492,24 @@ class TestMainCLI:
 
         # Mock the Suunto FIT parser parse method.
         mock_parse.side_effect = mock.Mock()
-        # Mock the create_video method.
-        mock_create_video.side_effect = self._mock_depthviz_create_video
+        # Mock the create_depth_video method.
+        mock_create_depth_video.side_effect = self._mock_depthviz_create_depth_video
         app.main()
         captured = capsys.readouterr()
 
-        assert f"Video successfully created: {output_path.as_posix()}" in captured.out
+        assert (
+            f"Depth video successfully created: {output_path.as_posix()}"
+            in captured.out
+        )
         mock_parse.assert_called_once_with(file_path=input_path.as_posix())
-        mock_create_video.assert_called_once_with(
+        mock_create_depth_video.assert_called_once_with(
             divelog_parser=mock.ANY,
             output_path=output_path.as_posix(),
             decimal_places=0,
             no_minus=False,
             font=DEFAULT_FONT,
             bg_color="black",
-            stroke_width=2,
+            stroke_width=DEFAULT_STROKE_WIDTH,
         )
 
     def test_main_with_args_font(
@@ -520,7 +543,10 @@ class TestMainCLI:
         app = DepthvizApplication()
         app.main()
         captured = capsys.readouterr()
-        assert f"Video successfully created: {output_path.as_posix()}" in captured.out
+        assert (
+            f"Depth video successfully created: {output_path.as_posix()}"
+            in captured.out
+        )
 
     def test_main_with_args_bg_color(
         self,
@@ -550,7 +576,10 @@ class TestMainCLI:
         app = DepthvizApplication()
         app.main()
         captured = capsys.readouterr()
-        assert f"Video successfully created: {output_path.as_posix()}" in captured.out
+        assert (
+            f"Depth video successfully created: {output_path.as_posix()}"
+            in captured.out
+        )
 
     def test_main_with_args_stroke_width(
         self,
@@ -582,4 +611,93 @@ class TestMainCLI:
         app = DepthvizApplication()
         app.main()
         captured = capsys.readouterr()
-        assert f"Video successfully created: {output_path.as_posix()}" in captured.out
+        assert (
+            f"Depth video successfully created: {output_path.as_posix()}"
+            in captured.out
+        )
+
+    def test_main_with_args_time_video(
+        self,
+        capsys: pytest.CaptureFixture[str],
+        tmp_path: pathlib.Path,
+        request: pytest.FixtureRequest,
+    ) -> None:
+        """
+        Test the main function with arguments for time overlay video.
+        """
+
+        input_path = (
+            request.path.parent / "data" / "apnealizer" / "valid_depth_data_trimmed.csv"
+        )
+        output_path = tmp_path / "FIM36m.mp4"
+        time_output_path = tmp_path / "FIM36m_time.mp4"
+        sys.argv = [
+            "main",
+            "-i",
+            str(input_path.as_posix()),
+            "-s",
+            "apnealizer",
+            "-o",
+            str(output_path.as_posix()),
+            "--time",
+        ]
+        app = DepthvizApplication()
+        app.main()
+        captured = capsys.readouterr()
+        assert (
+            f"Time video successfully created: {time_output_path.as_posix()}"
+            in captured.out
+        )
+        assert time_output_path.exists()
+
+    @mock.patch("depthviz.main.TimeReportVideoCreator.save")
+    @mock.patch("depthviz.main.TimeReportVideoCreator.render_time_report_video")
+    @mock.patch("depthviz.main.TimeReportVideoCreator")
+    def test_create_time_video_failure(
+        self,
+        mock_time_report_video_creator: mock.Mock,
+        mock_render_time_report_video: mock.Mock,
+        mock_save: mock.Mock,
+        capsys: pytest.CaptureFixture[str],
+    ) -> None:
+        """
+        Test the create_time_video method for failure in video creation.
+        """
+        # Create an instance of the main application.
+        app = DepthvizApplication()
+
+        # Mock the DiveLogParser class and its methods.
+        divelog_parser = mock.Mock(spec=DiveLogParser)
+        divelog_parser.get_time_data = mock.Mock(return_value=[0, -1, 2, 3])
+
+        # Mock the TimeReportVideoCreator class and its methods.
+        mock_time_report_video_creator.return_value = mock_time_report_video_creator
+        mock_render_time_report_video.side_effect = TimeReportVideoCreatorError(
+            "The time data contains negative values."
+        )
+
+        # Call the create_time_video method with the mocked objects.
+        output_path = "test.mp4"
+
+        ret_code = app.create_time_video(
+            divelog_parser=divelog_parser,
+            output_path=output_path,
+            font=DEFAULT_FONT,
+        )
+
+        # Check the return code and the captured output.
+        assert ret_code == 1
+        captured = capsys.readouterr()
+        assert "The time data contains negative values." in captured.out
+
+        # Check the exception type whether it is compatible with the expected exception.
+        assert isinstance(
+            mock_render_time_report_video.side_effect, OverlayVideoCreatorError
+        )
+
+        # Check the method calls.
+        divelog_parser.get_time_data.assert_called_once()
+        mock_render_time_report_video.assert_called_once_with(
+            time_data=[0, -1, 2, 3],
+        )
+        mock_save.assert_not_called()
diff --git a/tests/test_core.py b/tests/test_video_depth.py
similarity index 89%
rename from tests/test_core.py
rename to tests/test_video_depth.py
index 5fbf4f2..cbb283f 100644
--- a/tests/test_core.py
+++ b/tests/test_video_depth.py
@@ -1,17 +1,17 @@
 """
-Unit tests for the core module.
+Unit tests for the depth module.
 """
 
 import os.path
 import pathlib
 from typing import Any
 import pytest
-from depthviz.core import (
-    DepthReportVideoCreator,
+from depthviz.video.video_creator import (
     VideoNotRenderError,
     VideoFormatError,
-    DepthReportVideoCreatorError,
+    OverlayVideoCreatorError,
 )
+from depthviz.video.depth import DepthReportVideoCreator, DepthReportVideoCreatorError
 
 
 class TestDepthReportVideoCreator:
@@ -42,10 +42,9 @@ class TestDepthReportVideoCreator:
             3.0,
         ]
         depth_data = [0.1, 0.2, 0.3, 0.4, 0.5, 0.9, 1.3, 1.7, 2.1, 2.5, 2.9, 3.3]
-        depth_report_video_creator.render_depth_report_video(
+        video = depth_report_video_creator.render_depth_report_video(
             time_data=time_data, depth_data=depth_data
         )
-        video = depth_report_video_creator.get_video()
 
         # Check the video is not None
         assert video is not None
@@ -66,13 +65,13 @@ class TestDepthReportVideoCreator:
         # Create a depth report video
         time_data = [1.0, 2.0, 3.0]
         depth_data = [0.1, 0.2, 0.3]
-        depth_report_video_creator.render_depth_report_video(
+        video = depth_report_video_creator.render_depth_report_video(
             time_data=time_data, depth_data=depth_data
         )
 
         # Save the video to a file in directory tmp_path
         path = tmp_path / "test_depth_report_video.mp4"
-        depth_report_video_creator.save(path=path.as_posix())
+        depth_report_video_creator.save(video=video, path=path.as_posix())
 
         assert path.exists()
         assert path.stat().st_size > 0
@@ -88,13 +87,13 @@ class TestDepthReportVideoCreator:
         # Create a depth report video
         time_data = [0.25, 0.5, 0.75, 1.0]
         depth_data = [0.0, 1.0, 2.0, 3.0]
-        depth_report_video_creator.render_depth_report_video(
+        video = depth_report_video_creator.render_depth_report_video(
             time_data=time_data, depth_data=depth_data
         )
 
         # Save the video to a file in the current directory
         path = ".depth_overlay.mp4"
-        depth_report_video_creator.save(path=path)
+        depth_report_video_creator.save(video=video, path=path)
 
         assert os.path.exists(".depth_overlay.mp4")
 
@@ -108,14 +107,14 @@ class TestDepthReportVideoCreator:
         # Create a depth report video
         time_data = [1.0, 2.0, 3.0]
         depth_data = [0.1, 0.2, 0.3]
-        depth_report_video_creator.render_depth_report_video(
+        video = depth_report_video_creator.render_depth_report_video(
             time_data=time_data, depth_data=depth_data
         )
 
         # Save the video to a nonexistent path
         wrong_path = tmp_path / "non_existent_dir" / "test_depth_report_video.mp4"
         with pytest.raises(FileNotFoundError) as e:
-            depth_report_video_creator.save(wrong_path.as_posix())
+            depth_report_video_creator.save(video=video, path=wrong_path.as_posix())
         assert str(e.value) == f"Parent directory does not exist: {wrong_path.parent}"
 
     def test_save_without_file_name(self, tmp_path: pathlib.Path) -> None:
@@ -128,14 +127,14 @@ class TestDepthReportVideoCreator:
         # Create a depth report video
         time_data = [1.0, 2.0, 3.0]
         depth_data = [0.1, 0.2, 0.3]
-        depth_report_video_creator.render_depth_report_video(
+        video = depth_report_video_creator.render_depth_report_video(
             time_data=time_data, depth_data=depth_data
         )
 
         # Save the video to a file without a file name
         wrong_path = tmp_path
         with pytest.raises(NameError) as e:
-            depth_report_video_creator.save(wrong_path.as_posix())
+            depth_report_video_creator.save(video=video, path=wrong_path.as_posix())
         assert (
             str(e.value)
             == f"{wrong_path} is a directory, please add a file name to the path. \
@@ -148,11 +147,11 @@ class TestDepthReportVideoCreator:
         """
         # Create a DepthReportVideoCreator instance
         depth_report_video_creator = DepthReportVideoCreator(fps=1)
-
+        video = None
         # Save the video to a file without rendering it
         path = tmp_path / "test_depth_report_video.mp4"
         with pytest.raises(VideoNotRenderError) as e:
-            depth_report_video_creator.save(path=path.as_posix())
+            depth_report_video_creator.save(video=video, path=path.as_posix())
         assert str(e.value) == "Cannot save video because it has not been rendered yet."
 
     def test_save_video_wrong_format(self, tmp_path: pathlib.Path) -> None:
@@ -165,14 +164,14 @@ class TestDepthReportVideoCreator:
         # Create a depth report video
         time_data = [1.0, 2.0, 3.0]
         depth_data = [0.1, 0.2, 0.3]
-        depth_report_video_creator.render_depth_report_video(
+        video = depth_report_video_creator.render_depth_report_video(
             time_data=time_data, depth_data=depth_data
         )
 
         # Save the video to a file with a wrong file format
         wrong_path = tmp_path / "test_depth_report_video.avi"
         with pytest.raises(VideoFormatError) as e:
-            depth_report_video_creator.save(wrong_path.as_posix())
+            depth_report_video_creator.save(video=video, path=wrong_path.as_posix())
         assert str(e.value) == "Invalid file format: The file format must be .mp4"
 
     def test_render_depth_video_with_invalid_input(self) -> None:
@@ -187,7 +186,7 @@ class TestDepthReportVideoCreator:
         time_data = [0.1, 0.2, 0.3]
         depth_data = [0.1, 0.2]
         with pytest.raises(DepthReportVideoCreatorError) as e:
-            depth_report_video_creator.render_depth_report_video(
+            _ = depth_report_video_creator.render_depth_report_video(
                 time_data=time_data, depth_data=depth_data
             )
         assert (
@@ -215,10 +214,9 @@ class TestDepthReportVideoCreator:
         # Create a depth report video with decimal places
         time_data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
         depth_data = [0.001, 0.13344, 0.26667, 1.345, 1.54444, 3343.32343]
-        depth_report_video_creator.render_depth_report_video(
+        video = depth_report_video_creator.render_depth_report_video(
             time_data=time_data, depth_data=depth_data, decimal_places=decimal_places
         )
-        video = depth_report_video_creator.get_video()
 
         # Check the video is not None
         assert video is not None
@@ -250,7 +248,7 @@ class TestDepthReportVideoCreator:
         time_data = [1.0, 2.0, 3.0]
         depth_data = [0.1, 0.2, 0.3]
         with pytest.raises(DepthReportVideoCreatorError) as e:
-            depth_report_video_creator.render_depth_report_video(
+            _ = depth_report_video_creator.render_depth_report_video(
                 time_data=time_data,
                 depth_data=depth_data,
                 decimal_places=decimal_places,
@@ -283,13 +281,12 @@ class TestDepthReportVideoCreator:
         # Create a depth report video with no minus option
         time_data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
         depth_data = [0.1, 0.2, 0.3, 1.233, 2.0, 3343.33]
-        depth_report_video_creator.render_depth_report_video(
+        video = depth_report_video_creator.render_depth_report_video(
             time_data=time_data,
             depth_data=depth_data,
             decimal_places=decimal_places,
             minus_sign=minus_sign,
         )
-        video = depth_report_video_creator.get_video()
 
         # Check the video is not None
         assert video is not None
@@ -323,12 +320,11 @@ class TestDepthReportVideoCreator:
         # Create a depth report video with a user font
         time_data = [0.0, 1.0, 2.0, 3.0]
         depth_data = [0.0, 1.0, 2.0, 3.0]
-        depth_report_video_creator.render_depth_report_video(
+        video = depth_report_video_creator.render_depth_report_video(
             time_data=time_data, depth_data=depth_data
         )
 
         # Check the video is not None
-        video = depth_report_video_creator.get_video()
         assert video is not None
 
         # Check the duration of the video (seconds)
@@ -359,7 +355,7 @@ class TestDepthReportVideoCreator:
 
         file_path = str(request.path.parent.joinpath("data", "assets", "fonts", font))
 
-        with pytest.raises(DepthReportVideoCreatorError) as e:
+        with pytest.raises(OverlayVideoCreatorError) as e:
             # Create a DepthReportVideoCreator instance
             _ = DepthReportVideoCreator(fps=1, font=file_path)
 
@@ -389,7 +385,7 @@ class TestDepthReportVideoCreator:
         """
         Test the render_depth_report_video method with an invalid background color.
         """
-        with pytest.raises(DepthReportVideoCreatorError) as e:
+        with pytest.raises(OverlayVideoCreatorError) as e:
             _ = DepthReportVideoCreator(fps=1, bg_color=bg_color)
 
         assert "Invalid background color: " in str(e.value)
@@ -416,7 +412,7 @@ class TestDepthReportVideoCreator:
         """
         Test the render_depth_report_video method with an invalid stroke width.
         """
-        with pytest.raises(DepthReportVideoCreatorError) as e:
+        with pytest.raises(OverlayVideoCreatorError) as e:
             _ = DepthReportVideoCreator(fps=1, stroke_width=stroke_width)
 
         assert "Invalid stroke width; must be a positive number." in str(e.value)
diff --git a/tests/test_logger.py b/tests/test_video_logger.py
similarity index 93%
rename from tests/test_logger.py
rename to tests/test_video_logger.py
index e010b4e..fcc9d14 100644
--- a/tests/test_logger.py
+++ b/tests/test_video_logger.py
@@ -3,7 +3,7 @@ Unit tests for the progress bar logger class.
 """
 
 import pytest
-from depthviz.logger import DepthVizProgessBarLogger
+from depthviz.video.logger import DepthVizProgessBarLogger
 
 
 def test_depthviz_progress_bar_logger(monkeypatch: pytest.MonkeyPatch) -> None:
diff --git a/tests/test_video_time.py b/tests/test_video_time.py
new file mode 100644
index 0000000..f6b8ae9
--- /dev/null
+++ b/tests/test_video_time.py
@@ -0,0 +1,184 @@
+"""
+Unit tests for the depth module.
+"""
+
+import pytest
+from depthviz.video.time import TimeReportVideoCreator, TimeReportVideoCreatorError
+
+
+class TestTimeReportVideoCreator:
+    """
+    Test the TimeReportVideoCreator class.
+    """
+
+    @pytest.mark.parametrize(
+        "time_data, expected_duration, expected_num_clips, expected_clip_texts",
+        [
+            (
+                [
+                    0.0,
+                    0.25,
+                    0.5,
+                    0.75,
+                    1.0,
+                    1.25,
+                    1.5,
+                    1.75,
+                    2.0,
+                    2.25,
+                    2.5,
+                    2.75,
+                    3.0,
+                ],
+                4,
+                16,
+                [
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                    "00:02",
+                    "00:02",
+                    "00:02",
+                    "00:02",
+                    "00:03",
+                    "00:03",
+                    "00:03",
+                    "00:03",
+                ],
+            ),
+            ([0.0], 1, 4, ["00:00", "00:00", "00:00", "00:00"]),
+            (
+                [0.0, 1.0],
+                2,
+                8,
+                [
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                ],
+            ),
+            (
+                [123123412, 123123413, 123123414],
+                3,
+                12,
+                [
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                    "00:02",
+                    "00:02",
+                    "00:02",
+                    "00:02",
+                ],
+            ),
+            (
+                [1.0, 2.0, 3.0, 4.0],
+                4,
+                16,
+                [
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                    "00:02",
+                    "00:02",
+                    "00:02",
+                    "00:02",
+                    "00:03",
+                    "00:03",
+                    "00:03",
+                    "00:03",
+                ],
+            ),
+            (
+                [0.25, 0.5, 0.75, 1.0, 1.25, 1.5],
+                3,
+                12,
+                [
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:00",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                    "00:01",
+                    "00:02",
+                    "00:02",
+                    "00:02",
+                    "00:02",
+                ],
+            ),
+        ],
+    )
+    def test_render_time_report_video(
+        self,
+        time_data: list[float],
+        expected_duration: float,
+        expected_num_clips: int,
+        expected_clip_texts: list[str],
+    ) -> None:
+        """
+        Test the render_time_report_video method.
+        """
+        # Create a TimeReportVideoCreator instance
+        time_report_video_creator = TimeReportVideoCreator()
+
+        # Create a video with the time data
+        video = time_report_video_creator.render_time_report_video(time_data=time_data)
+
+        # Check the video is not None
+        assert video is not None
+
+        # Check the duration of the video (seconds)
+        assert video.duration == pytest.approx(expected_duration)
+
+        # Check the number of clips in the video
+        assert len(video.clips) == expected_num_clips
+
+        # Check the text of all clips in the video
+        clip_texts = [clip.text for clip in video.clips]
+        assert clip_texts == expected_clip_texts
+
+    @pytest.mark.parametrize(
+        "time_data, expected_error_message",
+        [
+            ([], "The time data is empty."),
+            ([-1.0], "The time data contains negative values."),
+            ([0.0, -1.0], "The time data contains negative values."),
+            ([-1.0, -2.0], "The time data contains negative values."),
+        ],
+    )
+    def test_render_time_report_video_error(
+        self, time_data: list[float], expected_error_message: str
+    ) -> None:
+        """
+        Test the render_time_report_video method with error cases.
+        """
+        # Create a TimeReportVideoCreator instance
+        time_report_video_creator = TimeReportVideoCreator()
+
+        # Check the expected error message is raised
+        with pytest.raises(TimeReportVideoCreatorError) as e:
+            time_report_video_creator.render_time_report_video(time_data=time_data)
+
+        assert str(e.value) == expected_error_message