media-toolkit 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ from media_toolkit.core import MediaFile, ImageFile, VideoFile, AudioFile
@@ -0,0 +1,4 @@
1
+ from .media_file import MediaFile
2
+ from .image_file import ImageFile
3
+ from .audio_file import AudioFile
4
+ from media_toolkit.core.video.video_file import VideoFile
@@ -0,0 +1,38 @@
1
+ from media_toolkit.dependency_requirements import requires
2
+ from media_toolkit.core.media_file import MediaFile
3
+ import io
4
+
5
+ try:
6
+ import soundfile
7
+ import numpy as np
8
+ except ImportError:
9
+ pass
10
+
11
+
12
+ class AudioFile(MediaFile):
13
+ """
14
+ Has file conversions that make it easy to work with image files across the web.
15
+ Internally it uses numpy and librosa.
16
+ """
17
+ @requires('soundfile')
18
+ def to_soundfile(self):
19
+ return soundfile.read(self._content_buffer)
20
+
21
+ @requires('soundfile')
22
+ def to_np_array(self, sr: int = None, return_sample_rate: bool = False):
23
+ self._content_buffer.seek(0)
24
+ audio, sr = soundfile.read(self._content_buffer, samplerate=sr) # sr=None returns the native sample rate
25
+ if return_sample_rate:
26
+ return audio, sr
27
+ return audio
28
+
29
+ @requires('soundfile')
30
+ def from_np_array(self, np_array, sr: int = None, file_type: str = "wav"):
31
+ sr = 22050 if sr is None else sr
32
+ # write to virtual file with librosa
33
+ virtual_file = io.BytesIO()
34
+ virtual_file.name = f"audio_file.{file_type}"
35
+ soundfile.write(virtual_file, np_array, samplerate=sr, format=file_type)
36
+
37
+ super().from_file(virtual_file)
38
+
@@ -0,0 +1,89 @@
1
+ from media_toolkit.dependency_requirements import requires_numpy, requires_cv2, requires
2
+ from media_toolkit.core.media_file import MediaFile
3
+
4
+ try:
5
+ import cv2
6
+ import numpy as np
7
+ except ImportError:
8
+ pass
9
+
10
+
11
+ class ImageFile(MediaFile):
12
+ """
13
+ Has file conversions that make it easy to work with image files across the web.
14
+ Internally it uses cv2 file format.
15
+ """
16
+ @requires('cv2', 'numpy')
17
+ def from_np_array(self, np_array, img_type: str = None):
18
+ if isinstance(np_array, list):
19
+ np_array = np.array(np_array)
20
+
21
+ if img_type is None:
22
+ if "image/" not in self.content_type:
23
+ img_type, self._channels = self.detect_image_type_and_channels(np_array)
24
+ else:
25
+ img_type = self.content_type.split("/")[1]
26
+ self.content_type = f"image/{img_type}"
27
+
28
+ is_success, buffer = cv2.imencode(f".{img_type}", np_array)
29
+ if is_success:
30
+ # avoid to check again for image type by calling super().from_bytes instead of self.from_bytes
31
+ return super().from_bytes(buffer)
32
+ else:
33
+ raise ValueError(f"Could not convert np_array to {img_type} image")
34
+
35
+
36
+ @requires('numpy', 'cv2')
37
+ def to_np_array(self):
38
+ bytes = self.to_bytes()
39
+ return cv2.imdecode(np.frombuffer(bytes, np.uint8), -1)
40
+
41
+ @requires_numpy()
42
+ def to_cv2_img(self):
43
+ return self.to_np_array()
44
+
45
+ @requires_cv2()
46
+ def save(self, path: str):
47
+ cv2.imwrite(path, self.to_np_array())
48
+
49
+ def _file_info(self):
50
+ super()._file_info()
51
+ np_array = self.to_np_array()
52
+ img_type, self._channels = self.detect_image_type_and_channels(np_array)
53
+ if img_type is not None:
54
+ self.content_type = f"image/{img_type}"
55
+
56
+
57
+ @staticmethod
58
+ def detect_image_type_and_channels(image) -> (str, int):
59
+ """Detect the image type and number of _channels from a numpy array."""
60
+ if isinstance(image, list):
61
+ image = np.array(image)
62
+
63
+ # Check the number of _channels
64
+ if len(image.shape) == 2:
65
+ channels = 1 # Grayscale
66
+ elif len(image.shape) == 3:
67
+ channels = image.shape[2]
68
+ else:
69
+ #raise ValueError("Unsupported image shape: {}".format(image.shape))
70
+ return None, None
71
+
72
+ # Detect image type by checking for specific markers
73
+ image_type = None
74
+
75
+ # Convert to bytes and inspect file signatures for format detection
76
+ success, encoded_image = cv2.imencode('.png', image)
77
+ if success:
78
+ encoded_bytes = encoded_image.tobytes()
79
+ if encoded_bytes.startswith(b'\x89PNG\r\n\x1a\n'):
80
+ image_type = 'png'
81
+ elif encoded_bytes[0:2] == b'\xff\xd8':
82
+ image_type = 'jpeg'
83
+ elif encoded_bytes.startswith(b'BM'):
84
+ image_type = 'bmp'
85
+ elif encoded_bytes.startswith(b'GIF'):
86
+ image_type = 'gif'
87
+
88
+ return image_type, channels
89
+
@@ -0,0 +1,259 @@
1
+ import base64
2
+ import io
3
+ import mimetypes
4
+ from typing import Union, BinaryIO
5
+ import os
6
+
7
+ from media_toolkit.dependency_requirements import requires_numpy
8
+
9
+
10
+ try:
11
+ import numpy as np
12
+ except ImportError:
13
+ pass
14
+
15
+
16
+ class MediaFile:
17
+ """
18
+ Has file conversions that make it easy to work standardized with files across the web and in the sdk.
19
+ Works natively with bytesio, base64 and binary data.
20
+ """
21
+ def __init__(self, file_name: str = "file", content_type: str = "application/octet-stream"):
22
+ """
23
+ :param file_name: The name of the file. Note it is overwritten if you use from_file/from_starlette.
24
+ :param content_type: The content type of the file. Note it is overwritten if you use from_file/from_starlette.
25
+ """
26
+ self.content_type = content_type
27
+ self.file_name = file_name # the name of the file also when specified in bytesio
28
+ self._content_buffer = io.BytesIO()
29
+
30
+ def from_any(self, data):
31
+ """
32
+ Load a file from any supported data type. The file is loaded into the memory as bytes.
33
+ """
34
+ # it is already converted
35
+ if isinstance(data, MediaFile):
36
+ return data
37
+
38
+ # conversion factory
39
+ if type(data) in [io.BufferedReader, io.BytesIO]:
40
+ self.from_bytesio_or_handle(data)
41
+ elif isinstance(data, str):
42
+ if self._is_valid_file_path(data):
43
+ self.from_file(data)
44
+ else:
45
+ self.from_base64(data)
46
+ elif isinstance(data, bytes):
47
+ self.from_bytes(data)
48
+ elif type(data).__name__ == 'ndarray':
49
+ self.from_np_array(data)
50
+ elif data.__module__ == 'starlette.datastructures' and type(data).__name__ == 'UploadFile':
51
+ self.from_starlette_upload_file(data)
52
+
53
+ return self
54
+
55
+ def from_bytesio_or_handle(self, buffer: Union[io.BytesIO, BinaryIO, io.BufferedReader], copy: bool = True):
56
+ """
57
+ Set the content of the file from a BytesIO or a file handle.
58
+ :params buffer: The buffer to read from.
59
+ :params copy: If true, the buffer is completely read to bytes and the bytes copied to this file.
60
+ """
61
+ self._reset_buffer()
62
+ if type(buffer) in [io.BytesIO, io.BufferedReader]:
63
+ buffer.seek(0)
64
+ if not copy:
65
+ self._content_buffer = buffer
66
+ self._file_info()
67
+ else:
68
+ self.from_bytes(buffer.read())
69
+ buffer.seek(0)
70
+
71
+ return self
72
+
73
+ def from_bytesio(self, buffer: Union[io.BytesIO, BinaryIO], copy: bool = True):
74
+ return self.from_bytesio_or_handle(buffer=buffer, copy=copy)
75
+
76
+ def from_file(self, path_or_handle: Union[str, io.BytesIO, io.BufferedReader]):
77
+ """
78
+ Load a file from a file path, file handle or base64 and convert it to BytesIO.
79
+ """
80
+ if type(path_or_handle) in [io.BufferedReader, io.BytesIO]:
81
+ self.from_bytesio_or_handle(path_or_handle)
82
+ elif isinstance(path_or_handle, str):
83
+ # read file from path
84
+ self.file_name = os.path.basename(path_or_handle)
85
+ self.content_type = mimetypes.guess_type(self.file_name)[0] or "application/octet-stream"
86
+ with open(path_or_handle, 'rb') as file:
87
+ self.from_bytesio_or_handle(file)
88
+
89
+ return self
90
+
91
+ def from_bytes(self, data: bytes):
92
+ self._reset_buffer()
93
+ self._content_buffer.write(data)
94
+ self._content_buffer.seek(0)
95
+ self._file_info()
96
+ return self
97
+
98
+ def from_starlette_upload_file(self, starlette_upload_file):
99
+ """
100
+ Load a file from a starlette upload file.
101
+ :param starlette_upload_file:
102
+ :return:
103
+ """
104
+ content = starlette_upload_file.file.read()
105
+
106
+ self.file_name = starlette_upload_file.filename
107
+ self.content_type = starlette_upload_file.content_type
108
+ self.from_bytes(content)
109
+ return self
110
+
111
+ def from_base64(self, base64_str: str):
112
+ decoded = self._decode_base_64_if_is(base64_str)
113
+ if decoded is not None:
114
+ return self.from_bytes(base64.b64decode(base64_str))
115
+ else:
116
+ err_str = base64_str if len(base64_str) <= 50 else base64_str[:50] + "..."
117
+ raise ValueError(f"Decoding from base64 like string {err_str} was not possible. Check your data.")
118
+
119
+ @requires_numpy()
120
+ def from_np_array(self, np_array: np.array):
121
+ """
122
+ Convert a numpy array to a file which is saved as bytes b"\x93NUMPY" into the buffer.
123
+ """
124
+ self._reset_buffer()
125
+ np.save(self._content_buffer, np_array)
126
+ return self
127
+
128
+ def from_dict(self, file_result_json: dict):
129
+ """
130
+ Load a file from a dictionary.
131
+ :param d: The dictionary to load from formatted as FileResult.to_json().
132
+ """
133
+ self.file_name = file_result_json["file_name"]
134
+ self.content_type = file_result_json["content_type"]
135
+ # ToDo: the from_base64 might overwrite name and content type (ImageFile). Check if this always is intended.
136
+ self.from_base64(file_result_json["content"])
137
+ return self
138
+
139
+ @requires_numpy()
140
+ def to_np_array(self, shape=None, dtype=np.uint8):
141
+ """
142
+ If file was created with from_np_array it will return the numpy array.
143
+ Else it will try to convert the file to a numpy array (note this is converted bytes representation of the file).
144
+ :param shape: The shape of the numpy array. If None it will be returned flat.
145
+ :param dtype: The dtype of the numpy array. If None it will be uint8.
146
+ """
147
+ bytes = self.to_bytes()
148
+ # check if was saved with np.save so bytes contains NUMPY
149
+ if bytes.startswith(b"\x93NUMPY"):
150
+ self._content_buffer.seek(0)
151
+ return np.load(self._content_buffer, allow_pickle=False)
152
+
153
+ shape = shape or (1, len(bytes))
154
+ dtype = dtype or np.uint8
155
+
156
+ arr_flat = np.frombuffer(bytes, dtype=dtype)
157
+ return arr_flat.reshape(shape)
158
+
159
+ def to_bytes(self) -> bytes:
160
+ return self.read()
161
+
162
+ def read(self) -> bytes:
163
+ self._content_buffer.seek(0)
164
+ res = self._content_buffer.read()
165
+ self._content_buffer.seek(0)
166
+ return res
167
+
168
+ def to_bytes_io(self) -> io.BytesIO:
169
+ return self._content_buffer
170
+
171
+ def to_base64(self):
172
+ return base64.b64encode(self.to_bytes()).decode()
173
+
174
+ def to_httpx_send_able_tuple(self):
175
+ return self.file_name, self.read(), self.content_type
176
+
177
+ def _reset_buffer(self):
178
+ self._content_buffer.seek(0)
179
+ self._content_buffer.truncate(0)
180
+
181
+ def save(self, path: str):
182
+ """
183
+ Methods saves the file to disk.
184
+ If path is a folder it will save it in folder/self.filename.
185
+ If path is a file it will save it there.
186
+ :param path:
187
+ :return:
188
+ """
189
+ # create folder if not exists
190
+ if os.path.dirname(path) != "" and not os.path.exists(os.path.dirname(path)):
191
+ os.makedirs(os.path.dirname(path))
192
+ # check if path contains a file name
193
+ if os.path.basename(path) == "":
194
+ path = os.path.join(path, self.file_name)
195
+
196
+ with open(path, 'wb') as file:
197
+ file.write(self.read())
198
+
199
+ def _file_info(self):
200
+ """
201
+ After writing the file to the buffer, this method is called to determine additional file informations.
202
+ For videos this might be length, frame rate...
203
+ If you subclass don't forget to call super()._file_info() to set the file name and content type.
204
+ """
205
+ # set file name and type
206
+ if hasattr(self._content_buffer, "name"):
207
+ self.file_name = os.path.basename(self._content_buffer.name)
208
+
209
+ if self.file_name != "file":
210
+ self.content_type = mimetypes.guess_type(self.file_name)[0] or "application/octet-stream"
211
+ else:
212
+ self.content_type = "application/octet-stream"
213
+
214
+ def __bytes__(self):
215
+ return self.to_bytes()
216
+
217
+ def __array__(self):
218
+ return self.to_np_array()
219
+
220
+ def to_json(self):
221
+ """
222
+ Returns the file as a json serializable dictionary.
223
+ :return: { "file_name": str, "content_type": str, "content": str }
224
+ """
225
+ return {
226
+ "file_name": self.file_name,
227
+ "content_type": self.content_type,
228
+ "content": self.to_base64()
229
+ }
230
+
231
+ @staticmethod
232
+ def _decode_base_64_if_is(data: Union[bytes, str]):
233
+ """
234
+ Checks if a string is base64. If it is, it returns the base64 string as bytes; else returns None.
235
+ """
236
+ if isinstance(data, str):
237
+ data = data.encode()
238
+
239
+ try:
240
+ # Decode the data
241
+ decoded = base64.b64decode(data, validate=True)
242
+ # Re-encode the decoded data
243
+ back_encoded = base64.b64encode(decoded)
244
+ # Compare with the original encoded data
245
+ if back_encoded == data:
246
+ return decoded
247
+ except Exception:
248
+ pass
249
+
250
+ return None
251
+
252
+ @staticmethod
253
+ def _is_valid_file_path(path: str):
254
+ try:
255
+ is_file = os.path.isfile(path)
256
+ return is_file
257
+ except:
258
+ return False
259
+
File without changes
@@ -0,0 +1,283 @@
1
+ import glob
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ from io import BytesIO
6
+ from typing import List, Union
7
+
8
+ from media_toolkit.core.video.video_utils import (add_audio_to_video_file, audio_array_to_audio_file,
9
+ video_from_image_generator, get_sample_rate_from_audio_file)
10
+ from media_toolkit.dependency_requirements import requires
11
+ from media_toolkit.core.media_file import MediaFile
12
+
13
+
14
+ try:
15
+ import cv2
16
+ import numpy as np
17
+ except ImportError:
18
+ pass
19
+
20
+ try:
21
+ from vidgear.gears import VideoGear, WriteGear
22
+ except:
23
+ pass
24
+
25
+ try:
26
+ from pydub import AudioSegment
27
+ from pydub.utils import mediainfo
28
+ except ImportError:
29
+ pass
30
+
31
+
32
+ class VideoFile(MediaFile):
33
+ """
34
+ A class to represent a video file.
35
+ """
36
+ def __init__(self):
37
+ super().__init__()
38
+ self.content_type = "video"
39
+ self.frame_count = None # an estimated value based on cv2.VideoCapture.get(cv2.CAP_PROP_FRAME_COUNT)
40
+ self.frame_rate = None
41
+ self.shape = None
42
+ self.audio_sample_rate = None
43
+
44
+ def from_files(self, image_files: Union[List[str], list], frame_rate: int = 30, audio_file=None):
45
+ """
46
+ Creates a video based of a list of image files and an audio_file file.
47
+ :param image_files: A list of image files to convert to a video. Either paths or numpy arrays.
48
+ :param frame_rate: The frame rate of the video.
49
+ :param audio_file: The audio_file file to add to the video, as path, bytes or AudioSegment.
50
+ """
51
+ # Check if there are images in the list
52
+ if not image_files:
53
+ raise ValueError("The list of image files is empty.")
54
+
55
+ # Create a temporary file to store the video
56
+ temp_vid_file_path = video_from_image_generator(image_files, frame_rate=frame_rate, save_path=None)
57
+ # Merge video and audio_file using pydub
58
+ if audio_file is not None:
59
+ combined = add_audio_to_video_file(video_file=temp_vid_file_path, audio_file=audio_file)
60
+ self.from_file(combined)
61
+ os.remove(combined)
62
+ os.remove(temp_vid_file_path)
63
+ return self
64
+
65
+ # Init self from the temp file
66
+ self.from_file(temp_vid_file_path)
67
+ # remove tempfile
68
+ os.remove(temp_vid_file_path)
69
+
70
+ return self
71
+
72
+ def from_image_files(self, image_files: List[str], frame_rate: int = 30):
73
+ """
74
+ Converts a list of image files into a video file.
75
+ """
76
+ return self.from_files(image_files, frame_rate, audio_file=None)
77
+
78
+ def from_dir(self, dir_path: str, audio: Union[str, list] = None, frame_rate: int = 30):
79
+ """
80
+ Converts all images in a directory into a video file.
81
+ """
82
+ image_types = ["*.png", "*.jpg", "*.jpeg"]
83
+ image_files = []
84
+ for image_type in image_types:
85
+ image_files.extend(glob.glob(os.path.join(dir_path, image_type)))
86
+ # sort by date to make sure the order is correct
87
+ image_files.sort(key=lambda x: os.path.getmtime(x))
88
+
89
+ # if audio_file is none, take the first audio_file file in the directory
90
+ if audio is None:
91
+ audio_types = ["*.wav", "*.mp3"]
92
+ for audio_type in audio_types:
93
+ audio = glob.glob(os.path.join(dir_path, audio_type))
94
+ if len(audio) > 0:
95
+ audio = audio[0]
96
+ break
97
+
98
+ return self.from_files(image_files=image_files, frame_rate=frame_rate, audio_file=audio)
99
+
100
+ def add_audio(self, audio_file: Union[str, list], sample_rate: int = 44100):
101
+ if self.audio_sample_rate is None:
102
+ if self.frame_rate is None:
103
+ raise Exception("The frame rate of the video file is not set. Read a video file first.")
104
+
105
+ if os.path.isfile(audio_file):
106
+ self.audio_sample_rate = get_sample_rate_from_audio_file(audio_file)
107
+ else:
108
+
109
+ self.audio_sample_rate = int(mediainfo(self._to_temp_file())['sample_rate'])
110
+
111
+
112
+ if isinstance(audio_file, list) or isinstance(audio_file, np.ndarray):
113
+ audio_file = audio_array_to_audio_file(audio_file, sample_rate=self.audio_sample_rate)
114
+
115
+ tmp = self._to_temp_file()
116
+ combined = add_audio_to_video_file(tmp, audio_file)
117
+ self.from_file(combined)
118
+ os.remove(tmp)
119
+ os.remove(combined)
120
+ return self
121
+
122
+ def _to_temp_file(self):
123
+ # get suffix
124
+ if self.content_type is None:
125
+ raise ValueError("The content type of the video file is not set.")
126
+ if "/" not in self.content_type:
127
+ raise ValueError("The content type of the video file is not valid. Read a video file first.")
128
+ suffix = self.content_type.split("/")[1]
129
+ if suffix == 'octet-stream':
130
+ raise ValueError("The content type of the video file is not valid. Read a video file first.")
131
+
132
+ with tempfile.NamedTemporaryFile(delete=False, suffix=f".{suffix}") as temp_video_file:
133
+ temp_video_file.write(self._content_buffer.getvalue())
134
+ temp_video_file_path = temp_video_file.name
135
+
136
+ return temp_video_file_path
137
+
138
+ @requires('vidgear', 'numpy', 'pydub')
139
+ def from_video_stream(self, video_audio_stream, frame_rate: int = 30):
140
+ """
141
+ Given a generator that yields video frames and audio_file data as numpy arrays, this creates a video.
142
+ The generator is expected to be in the form of: VideoFile().to_video_stream()
143
+ """
144
+ # Reset and pre-settings
145
+ self._reset_buffer()
146
+
147
+ # new generator, to extract audio_file
148
+ audio_frames = []
149
+ def _frame_gen():
150
+ for frame in video_audio_stream:
151
+ # check if is video and audio_file stream or only video stream
152
+ if len(frame) == 2:
153
+ frame, audio_data = frame
154
+ audio_frames.append(audio_data)
155
+ yield frame
156
+
157
+ # Create video
158
+ temp_video_file_path = video_from_image_generator(_frame_gen(), frame_rate=frame_rate, save_path=None)
159
+
160
+ # Add audio_file
161
+ if len(audio_frames) > 0:
162
+ temp_audio_file = audio_array_to_audio_file(audio_frames, sample_rate=self.audio_sample_rate)
163
+ combined = add_audio_to_video_file(temp_video_file_path, temp_audio_file)
164
+ self.from_file(combined)
165
+ # cleanup
166
+ os.remove(temp_audio_file)
167
+ os.remove(temp_video_file_path)
168
+ os.remove(combined)
169
+
170
+ @requires('cv2', 'pydub')
171
+ def _file_info(self):
172
+ super()._file_info()
173
+
174
+ #if file_path is not None:
175
+ # temp = file_path
176
+ #else:
177
+ self._content_buffer.seek(0)
178
+ temp = self._to_temp_file()
179
+
180
+ cap = cv2.VideoCapture(temp)
181
+ self.frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # is an estimated value.
182
+ # determine content codec
183
+ # https://stackoverflow.com/questions/61659346/how-to-get-4-character-codec-code-for-videocapture-object-in-opencv
184
+ # h = int(cap.get(cv2.CAP_PROP_FOURCC))
185
+ # b = h.to_bytes(4, byteorder=sys.byteorder)
186
+ # codec = b.decode() # results in the codec
187
+ self.content_type = f"video/mp4"
188
+ self.frame_rate = cap.get(cv2.CAP_PROP_FPS)
189
+ self.shape = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
190
+ cap.release()
191
+ # get audio sample rate
192
+ info = mediainfo(temp)
193
+ if 'sample_rate' in info:
194
+ try:
195
+ self.audio_sample_rate = int(info['sample_rate'])
196
+ except ValueError:
197
+ self.audio_sample_rate = 44100
198
+ os.remove(temp)
199
+
200
+ @requires('vidgear')
201
+ def to_image_stream(self):
202
+ return self.to_video_stream(include_audio=False)
203
+
204
+ @requires('pydub', 'vidgear')
205
+ def to_video_stream(self, include_audio=True):
206
+ """
207
+ Yields video frames and audio_file data as numpy arrays.
208
+ :param include_audio: if the audio_file is included in the video stream. If not it will only yield the video frames.
209
+ :return:
210
+ """
211
+ self._content_buffer.seek(0)
212
+ # because CamGear does not support reading from a BytesIO buffer, we need to save the buffer to a temporary file
213
+ temp_video_file_path = self._to_temp_file()
214
+ stream = VideoGear(source=temp_video_file_path).start()
215
+
216
+ if include_audio:
217
+ # Extract audio_file using pydub
218
+ audio = AudioSegment.from_file(temp_video_file_path)
219
+ # Calculate the audio_file segment duration per frame
220
+ audio_per_frame_duration = 1000 / stream.framerate # duration of each video frame in ms
221
+ # Initialize frame counter for audio_file
222
+ frame_count = 0
223
+
224
+ try:
225
+ while True:
226
+ # Read frame
227
+ frame = stream.read()
228
+ if frame is None:
229
+ break
230
+
231
+ if not include_audio:
232
+ yield frame
233
+ continue
234
+
235
+ # Calculate the start and end times for the corresponding audio_file segment
236
+ start_time = frame_count * audio_per_frame_duration
237
+ end_time = start_time + audio_per_frame_duration
238
+ frame_audio = audio[start_time:end_time]
239
+
240
+ # Convert audio_file segment to raw data
241
+ audio_data = np.array(frame_audio.get_array_of_samples())
242
+
243
+ # Yield the frame and the corresponding audio_file data
244
+ yield frame, audio_data
245
+
246
+ # Increment frame counter
247
+ frame_count += 1
248
+ finally:
249
+ # Safely close the video stream
250
+ stream.stop()
251
+ # Remove the temporary video file
252
+ os.remove(temp_video_file_path)
253
+ # accurate value instead of using cv2.CAP_PROP_FRAME_COUNT
254
+ self.frame_count = frame_count
255
+
256
+ @requires('pydub')
257
+ def extract_audio(self, path: str = None, export_type: str = 'mp4') -> Union[bytes, None]:
258
+ temp_video_file_path = self._to_temp_file()
259
+ audio = AudioSegment.from_file(temp_video_file_path)
260
+
261
+ if path is not None and len(path) > 0:
262
+ dirname = os.path.dirname(path)
263
+ if len(dirname) > 0 and not os.path.isdir(dirname):
264
+ os.makedirs(dirname)
265
+ audio.export(path, format=export_type)
266
+ os.remove(temp_video_file_path)
267
+ return None
268
+
269
+ # return as bytes
270
+ file = BytesIO()
271
+ file = audio.export(file, format=export_type)
272
+ file.seek(0)
273
+ data = file.read()
274
+ file.close()
275
+ # remove tempfile
276
+ os.remove(temp_video_file_path)
277
+ return data
278
+
279
+ def __iter__(self):
280
+ return self.to_video_stream()
281
+
282
+ def __len__(self):
283
+ return self.frame_count