openvisionkit 0.4.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.
- openvisionkit/__init__.py +1 -0
- openvisionkit/_version.py +24 -0
- openvisionkit/capture/draw_object.py +296 -0
- openvisionkit/capture/image_template.py +61 -0
- openvisionkit/capture/screen_capture.py +13 -0
- openvisionkit/capture/video_recorder.py +128 -0
- openvisionkit/capture/video_template.py +336 -0
- openvisionkit/lib/classifier.py +186 -0
- openvisionkit/lib/face_detector.py +587 -0
- openvisionkit/lib/face_mesh_detector.py +913 -0
- openvisionkit/lib/form_detector.py +465 -0
- openvisionkit/lib/form_roi_annotator.py +679 -0
- openvisionkit/lib/form_roi_detector.py +1078 -0
- openvisionkit/lib/fps_counter.py +38 -0
- openvisionkit/lib/hair_segmentation.py +298 -0
- openvisionkit/lib/hand_detector.py +1230 -0
- openvisionkit/lib/image_detector.py +1095 -0
- openvisionkit/lib/object_detector.py +401 -0
- openvisionkit/lib/pose_detector.py +919 -0
- openvisionkit/lib/selfie_segmentation.py +528 -0
- openvisionkit/lib/text_detector.py +1229 -0
- openvisionkit/utility/live_plot.py +141 -0
- openvisionkit/utility/vision_utilis.py +871 -0
- openvisionkit-0.4.0.dist-info/METADATA +1018 -0
- openvisionkit-0.4.0.dist-info/RECORD +26 -0
- openvisionkit-0.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = "0.1.dev13+ga17e5ab6a.d20260610"
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, "dev13", "ga17e5ab6a.d20260610")
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DrawingObject:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
kind="circle",
|
|
11
|
+
origin=None,
|
|
12
|
+
placement="top-left",
|
|
13
|
+
size=(100, 100),
|
|
14
|
+
color=(0, 255, 0),
|
|
15
|
+
margin=20,
|
|
16
|
+
thickness=-1,
|
|
17
|
+
label=None,
|
|
18
|
+
):
|
|
19
|
+
self.id = str(uuid.uuid4())
|
|
20
|
+
|
|
21
|
+
self.kind = kind
|
|
22
|
+
self.origin = origin
|
|
23
|
+
self.placement = placement
|
|
24
|
+
self.size = size
|
|
25
|
+
self.color = color
|
|
26
|
+
self.margin = margin
|
|
27
|
+
self.thickness = thickness
|
|
28
|
+
self.label = label
|
|
29
|
+
|
|
30
|
+
self.initialized = False
|
|
31
|
+
|
|
32
|
+
self.position = None
|
|
33
|
+
self.initial_position = None
|
|
34
|
+
self.center_point = None
|
|
35
|
+
self.bounding_box = None
|
|
36
|
+
|
|
37
|
+
# interaction state (useful for gestures)
|
|
38
|
+
self.is_hovered = False
|
|
39
|
+
self.is_selected = False
|
|
40
|
+
|
|
41
|
+
# ----------------------------
|
|
42
|
+
# POSITION RESOLUTION
|
|
43
|
+
# ----------------------------
|
|
44
|
+
def resolve_position(self, frame_shape):
|
|
45
|
+
frame_h, frame_w = frame_shape[:2]
|
|
46
|
+
obj_w, obj_h = self.size
|
|
47
|
+
|
|
48
|
+
positions = {
|
|
49
|
+
"top-left": (self.margin, self.margin),
|
|
50
|
+
"top": ((frame_w - obj_w) // 2, self.margin),
|
|
51
|
+
"top-right": (frame_w - obj_w - self.margin, self.margin),
|
|
52
|
+
"left": (self.margin, (frame_h - obj_h) // 2),
|
|
53
|
+
"center": ((frame_w - obj_w) // 2, (frame_h - obj_h) // 2),
|
|
54
|
+
"right": (frame_w - obj_w - self.margin, (frame_h - obj_h) // 2),
|
|
55
|
+
"bottom-left": (self.margin, frame_h - obj_h - self.margin),
|
|
56
|
+
"bottom": ((frame_w - obj_w) // 2, frame_h - obj_h - self.margin),
|
|
57
|
+
"bottom-right": (
|
|
58
|
+
frame_w - obj_w - self.margin,
|
|
59
|
+
frame_h - obj_h - self.margin,
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return positions.get(self.placement, positions["top-left"])
|
|
64
|
+
|
|
65
|
+
# ----------------------------
|
|
66
|
+
# UPDATE INTERNAL REFERENCES
|
|
67
|
+
# ----------------------------
|
|
68
|
+
def update_reference_points(self):
|
|
69
|
+
if self.origin is None:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
x, y = self.origin
|
|
73
|
+
w, h = self.size
|
|
74
|
+
|
|
75
|
+
self.position = {"x": x, "y": y}
|
|
76
|
+
|
|
77
|
+
self.center_point = {
|
|
78
|
+
"x": x + w // 2,
|
|
79
|
+
"y": y + h // 2,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
self.bounding_box = {
|
|
83
|
+
"x1": x,
|
|
84
|
+
"y1": y,
|
|
85
|
+
"x2": x + w,
|
|
86
|
+
"y2": y + h,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ----------------------------
|
|
90
|
+
# INITIALIZATION
|
|
91
|
+
# ----------------------------
|
|
92
|
+
def initialize_position(self, frame_shape):
|
|
93
|
+
if self.origin is None:
|
|
94
|
+
self.origin = self.resolve_position(frame_shape)
|
|
95
|
+
|
|
96
|
+
self.initial_position = {
|
|
97
|
+
"x": self.origin[0],
|
|
98
|
+
"y": self.origin[1],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
self.update_reference_points()
|
|
102
|
+
self.initialized = True
|
|
103
|
+
|
|
104
|
+
# ----------------------------
|
|
105
|
+
# MOVE / RESET
|
|
106
|
+
# ----------------------------
|
|
107
|
+
def move_to(self, x, y):
|
|
108
|
+
self.origin = (int(x), int(y))
|
|
109
|
+
self.update_reference_points()
|
|
110
|
+
|
|
111
|
+
def reset_position(self):
|
|
112
|
+
if self.initial_position:
|
|
113
|
+
self.origin = (
|
|
114
|
+
self.initial_position["x"],
|
|
115
|
+
self.initial_position["y"],
|
|
116
|
+
)
|
|
117
|
+
self.update_reference_points()
|
|
118
|
+
|
|
119
|
+
# ----------------------------
|
|
120
|
+
# INTERACTION HELPERS
|
|
121
|
+
# ----------------------------
|
|
122
|
+
def contains_point(self, point):
|
|
123
|
+
if not self.bounding_box:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
px, py = point
|
|
127
|
+
return (
|
|
128
|
+
self.bounding_box["x1"] <= px <= self.bounding_box["x2"]
|
|
129
|
+
and self.bounding_box["y1"] <= py <= self.bounding_box["y2"]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def set_hover(self, state: bool):
|
|
133
|
+
self.is_hovered = state
|
|
134
|
+
|
|
135
|
+
def set_selected(self, state: bool):
|
|
136
|
+
self.is_selected = state
|
|
137
|
+
|
|
138
|
+
# ----------------------------
|
|
139
|
+
# GETTERS
|
|
140
|
+
# ----------------------------
|
|
141
|
+
def get_position(self):
|
|
142
|
+
return self.position
|
|
143
|
+
|
|
144
|
+
def get_center(self):
|
|
145
|
+
return self.center_point
|
|
146
|
+
|
|
147
|
+
def get_bounds(self):
|
|
148
|
+
return self.bounding_box
|
|
149
|
+
|
|
150
|
+
def get_size(self):
|
|
151
|
+
return self.size
|
|
152
|
+
|
|
153
|
+
def get_id(self):
|
|
154
|
+
return self.id
|
|
155
|
+
|
|
156
|
+
# ----------------------------
|
|
157
|
+
# SERIALIZATION
|
|
158
|
+
# ----------------------------
|
|
159
|
+
def to_dict(self):
|
|
160
|
+
return {
|
|
161
|
+
"id": self.id,
|
|
162
|
+
"kind": self.kind,
|
|
163
|
+
"origin": self.origin,
|
|
164
|
+
"placement": self.placement,
|
|
165
|
+
"size": self.size,
|
|
166
|
+
"color": self.color,
|
|
167
|
+
"label": self.label,
|
|
168
|
+
"initialized": self.initialized,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# optional reverse (useful for restoring state)
|
|
172
|
+
@staticmethod
|
|
173
|
+
def from_dict(data: dict):
|
|
174
|
+
obj = DrawingObject(
|
|
175
|
+
kind=data.get("kind", "circle"),
|
|
176
|
+
origin=data.get("origin"),
|
|
177
|
+
placement=data.get("placement", "top-left"),
|
|
178
|
+
size=tuple(data.get("size", (100, 100))),
|
|
179
|
+
color=tuple(data.get("color", (0, 255, 0))),
|
|
180
|
+
label=data.get("label"),
|
|
181
|
+
)
|
|
182
|
+
obj.initialized = data.get("initialized", False)
|
|
183
|
+
obj.update_reference_points()
|
|
184
|
+
return obj
|
|
185
|
+
|
|
186
|
+
# ----------------------------
|
|
187
|
+
# LAYOUT ENGINE
|
|
188
|
+
# ----------------------------
|
|
189
|
+
@staticmethod
|
|
190
|
+
def distribute_evenly(
|
|
191
|
+
drawings,
|
|
192
|
+
frame_shape,
|
|
193
|
+
row="top",
|
|
194
|
+
margin=20,
|
|
195
|
+
padding=10,
|
|
196
|
+
):
|
|
197
|
+
frame_h, frame_w = frame_shape[:2]
|
|
198
|
+
|
|
199
|
+
count = len(drawings)
|
|
200
|
+
if count == 0:
|
|
201
|
+
return drawings # IMPORTANT FIX
|
|
202
|
+
|
|
203
|
+
total_object_width = sum(d.size[0] for d in drawings)
|
|
204
|
+
|
|
205
|
+
available_width = frame_w - (margin * 2) - total_object_width
|
|
206
|
+
|
|
207
|
+
gap = max(padding, available_width // max(count - 1, 1))
|
|
208
|
+
|
|
209
|
+
current_x = margin
|
|
210
|
+
|
|
211
|
+
for drawing in drawings:
|
|
212
|
+
obj_w, obj_h = drawing.size
|
|
213
|
+
|
|
214
|
+
if row == "top":
|
|
215
|
+
y = margin
|
|
216
|
+
elif row == "center":
|
|
217
|
+
y = (frame_h - obj_h) // 2
|
|
218
|
+
elif row == "bottom":
|
|
219
|
+
y = frame_h - obj_h - margin
|
|
220
|
+
else:
|
|
221
|
+
y = margin
|
|
222
|
+
|
|
223
|
+
drawing.origin = (int(current_x), int(y))
|
|
224
|
+
drawing.initial_position = {"x": int(current_x), "y": int(y)}
|
|
225
|
+
drawing.update_reference_points()
|
|
226
|
+
drawing.initialized = True
|
|
227
|
+
|
|
228
|
+
current_x += obj_w + gap
|
|
229
|
+
|
|
230
|
+
return drawings
|
|
231
|
+
|
|
232
|
+
# ----------------------------
|
|
233
|
+
# DRAWING
|
|
234
|
+
# ----------------------------
|
|
235
|
+
def draw(self, frame):
|
|
236
|
+
if not self.initialized or self.origin is None:
|
|
237
|
+
self.initialize_position(frame.shape)
|
|
238
|
+
|
|
239
|
+
x, y = self.origin
|
|
240
|
+
w, h = self.size
|
|
241
|
+
|
|
242
|
+
frame_h, frame_w = frame.shape[:2]
|
|
243
|
+
|
|
244
|
+
# clamp inside frame
|
|
245
|
+
x = max(0, min(int(x), frame_w - w))
|
|
246
|
+
y = max(0, min(int(y), frame_h - h))
|
|
247
|
+
|
|
248
|
+
self.origin = (x, y)
|
|
249
|
+
self.update_reference_points()
|
|
250
|
+
|
|
251
|
+
# optional highlight when selected/hovered
|
|
252
|
+
stroke = self.thickness
|
|
253
|
+
if self.is_selected:
|
|
254
|
+
stroke = 3
|
|
255
|
+
|
|
256
|
+
if self.kind == "circle":
|
|
257
|
+
center = (x + w // 2, y + h // 2)
|
|
258
|
+
radius = min(w, h) // 2
|
|
259
|
+
|
|
260
|
+
cv2.circle(frame, center, radius, self.color, stroke)
|
|
261
|
+
|
|
262
|
+
elif self.kind == "square":
|
|
263
|
+
side = min(w, h)
|
|
264
|
+
cv2.rectangle(frame, (x, y), (x + side, y + side), self.color, stroke)
|
|
265
|
+
|
|
266
|
+
elif self.kind == "rectangle":
|
|
267
|
+
cv2.rectangle(frame, (x, y), (x + w, y + h), self.color, stroke)
|
|
268
|
+
|
|
269
|
+
elif self.kind == "triangle":
|
|
270
|
+
points = np.array(
|
|
271
|
+
[
|
|
272
|
+
[x + w // 2, y],
|
|
273
|
+
[x, y + h],
|
|
274
|
+
[x + w, y + h],
|
|
275
|
+
],
|
|
276
|
+
dtype=np.int32,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if stroke == -1:
|
|
280
|
+
cv2.fillPoly(frame, [points], self.color)
|
|
281
|
+
else:
|
|
282
|
+
cv2.polylines(frame, [points], True, self.color, stroke)
|
|
283
|
+
|
|
284
|
+
# label
|
|
285
|
+
if self.label:
|
|
286
|
+
cv2.putText(
|
|
287
|
+
frame,
|
|
288
|
+
self.label,
|
|
289
|
+
(x, y - 10),
|
|
290
|
+
cv2.FONT_HERSHEY_SIMPLEX,
|
|
291
|
+
0.6,
|
|
292
|
+
self.color,
|
|
293
|
+
2,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return frame
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pyautogui
|
|
6
|
+
|
|
7
|
+
window_centered = False # Used to center window only once
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def image_template(
|
|
11
|
+
image_path: str,
|
|
12
|
+
custom_logic: Callable[[cv2.typing.MatLike], cv2.typing.MatLike] | None = None,
|
|
13
|
+
window_name: str = "Demo",
|
|
14
|
+
center_window: bool = True,
|
|
15
|
+
show_window: bool = True,
|
|
16
|
+
resolution: tuple[int, int] = (1280, 720),
|
|
17
|
+
):
|
|
18
|
+
"""
|
|
19
|
+
REUSABLE TEMPLATE for displaying an image with optional custom processing.
|
|
20
|
+
|
|
21
|
+
Parameters:
|
|
22
|
+
image_path (str): Path to the image file.
|
|
23
|
+
custom_logic (callable, optional): Function that receives the image and returns the modified image.
|
|
24
|
+
window_name (str): Name of the OpenCV window.
|
|
25
|
+
center_window (bool): If True, automatically centers the window on screen. Default = True
|
|
26
|
+
show_window (bool): If True, displays the image window. Default = True
|
|
27
|
+
resolution (tuple[int, int]): Desired image resolution (width, height). Default = (1280, 720)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
image = cv2.imread(image_path)
|
|
31
|
+
print(image_path)
|
|
32
|
+
if custom_logic is not None:
|
|
33
|
+
image = custom_logic(image)
|
|
34
|
+
|
|
35
|
+
if image is None:
|
|
36
|
+
raise ValueError("Image is None. Check file path or loading logic.")
|
|
37
|
+
|
|
38
|
+
if not isinstance(image, np.ndarray):
|
|
39
|
+
raise TypeError(f"Invalid image type: {type(image)}")
|
|
40
|
+
|
|
41
|
+
if image.size == 0:
|
|
42
|
+
raise ValueError("Empty image array")
|
|
43
|
+
|
|
44
|
+
# Resize image
|
|
45
|
+
hWIDTH, hHEIGHT = resolution
|
|
46
|
+
resized_img = cv2.resize(image, (hWIDTH, hHEIGHT))
|
|
47
|
+
|
|
48
|
+
# ======================
|
|
49
|
+
# NEW: Auto-center window on screen (only once)
|
|
50
|
+
# ======================
|
|
51
|
+
if center_window:
|
|
52
|
+
screen_width, screen_height = pyautogui.size()
|
|
53
|
+
x = int((screen_width - hWIDTH) / 2)
|
|
54
|
+
y = int((screen_height - hHEIGHT) / 2)
|
|
55
|
+
cv2.moveWindow(window_name, x, y)
|
|
56
|
+
# ======================
|
|
57
|
+
|
|
58
|
+
if show_window:
|
|
59
|
+
cv2.imshow(window_name, resized_img)
|
|
60
|
+
cv2.waitKey(0)
|
|
61
|
+
cv2.destroyAllWindows()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import mss
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ScreenCapture:
|
|
7
|
+
def __init__(self, monitor_index=1):
|
|
8
|
+
self.sct = mss.mss()
|
|
9
|
+
self.monitor = self.sct.monitors[monitor_index]
|
|
10
|
+
|
|
11
|
+
def grab(self):
|
|
12
|
+
frame = np.array(self.sct.grab(self.monitor))
|
|
13
|
+
return cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
import cv2
|
|
7
|
+
import imageio
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class VideoRecorder:
|
|
12
|
+
"""
|
|
13
|
+
Advanced Video Recorder Engine:
|
|
14
|
+
- MP4 or GIF export
|
|
15
|
+
- Pause / Resume
|
|
16
|
+
- Timer tracking
|
|
17
|
+
- Multi-source ready (webcam + screen overlay)
|
|
18
|
+
- Plug & play with video_capture_template
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
output_path: str = "recordings"
|
|
22
|
+
fps: int = 20
|
|
23
|
+
codec: str = "mp4v"
|
|
24
|
+
output_format: str = "mp4" # "mp4" or "gif"
|
|
25
|
+
|
|
26
|
+
def __post_init__(self):
|
|
27
|
+
os.makedirs(self.output_path, exist_ok=True)
|
|
28
|
+
|
|
29
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
30
|
+
|
|
31
|
+
self.file_path = os.path.join(
|
|
32
|
+
self.output_path, f"record_{timestamp}.{self.output_format}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# VideoWriter (MP4 mode)
|
|
36
|
+
self.writer = None
|
|
37
|
+
|
|
38
|
+
# GIF buffer mode
|
|
39
|
+
self.frames = []
|
|
40
|
+
|
|
41
|
+
self.is_recording = False
|
|
42
|
+
self.is_paused = False
|
|
43
|
+
|
|
44
|
+
self.start_time = None
|
|
45
|
+
self.elapsed_time = 0
|
|
46
|
+
|
|
47
|
+
# ----------------------------
|
|
48
|
+
# START RECORDING
|
|
49
|
+
# ----------------------------
|
|
50
|
+
def start(self, frame_shape=None):
|
|
51
|
+
self.is_recording = True
|
|
52
|
+
self.is_paused = False
|
|
53
|
+
self.start_time = time.time()
|
|
54
|
+
|
|
55
|
+
self.frames = []
|
|
56
|
+
|
|
57
|
+
if self.output_format == "mp4" and frame_shape is not None:
|
|
58
|
+
h, w = frame_shape[:2]
|
|
59
|
+
fourcc = cv2.VideoWriter_fourcc(*self.codec)
|
|
60
|
+
self.writer = cv2.VideoWriter(self.file_path, fourcc, self.fps, (w, h))
|
|
61
|
+
|
|
62
|
+
print(f"🔴 Recording started → {self.file_path}")
|
|
63
|
+
|
|
64
|
+
# ----------------------------
|
|
65
|
+
# WRITE FRAME
|
|
66
|
+
# ----------------------------
|
|
67
|
+
def write(self, frame):
|
|
68
|
+
if not self.is_recording or self.is_paused:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# -------------------------
|
|
72
|
+
# Convert BGR → RGB (IMPORTANT)
|
|
73
|
+
# -------------------------
|
|
74
|
+
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
75
|
+
|
|
76
|
+
# MP4 mode
|
|
77
|
+
if self.output_format == "mp4" and self.writer:
|
|
78
|
+
self.writer.write(frame) # MP4 expects BGR (OpenCV standard)
|
|
79
|
+
|
|
80
|
+
# GIF mode
|
|
81
|
+
elif self.output_format == "gif":
|
|
82
|
+
self.frames.append(rgb_frame)
|
|
83
|
+
|
|
84
|
+
# ----------------------------
|
|
85
|
+
# TIMER
|
|
86
|
+
# ----------------------------
|
|
87
|
+
def get_elapsed_time(self):
|
|
88
|
+
if self.start_time:
|
|
89
|
+
return round(time.time() - self.start_time, 2)
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
# ----------------------------
|
|
93
|
+
# PAUSE / RESUME
|
|
94
|
+
# ----------------------------
|
|
95
|
+
def pause(self):
|
|
96
|
+
self.is_paused = True
|
|
97
|
+
print("⏸ Recording paused")
|
|
98
|
+
|
|
99
|
+
def resume(self):
|
|
100
|
+
self.is_paused = False
|
|
101
|
+
print("▶ Recording resumed")
|
|
102
|
+
|
|
103
|
+
# ----------------------------
|
|
104
|
+
# STOP RECORDING
|
|
105
|
+
# ----------------------------
|
|
106
|
+
def stop(self):
|
|
107
|
+
self.is_recording = False
|
|
108
|
+
|
|
109
|
+
if self.writer:
|
|
110
|
+
self.writer.release()
|
|
111
|
+
print(f"✅ MP4 saved → {self.file_path}")
|
|
112
|
+
|
|
113
|
+
if self.output_format == "gif":
|
|
114
|
+
if not self.frames or len(self.frames) == 0:
|
|
115
|
+
print("⚠️ No frames recorded. Skipping GIF export.")
|
|
116
|
+
return
|
|
117
|
+
safe_fps = self.fps if self.fps and self.fps > 0 else 10
|
|
118
|
+
imageio.mimsave(self.file_path, self.frames, fps=safe_fps)
|
|
119
|
+
print(f"✅ GIF saved → {self.file_path}")
|
|
120
|
+
|
|
121
|
+
# ----------------------------
|
|
122
|
+
# AUDIO SYNC (HOOK)
|
|
123
|
+
# ----------------------------
|
|
124
|
+
def attach_audio(self, audio_path: str):
|
|
125
|
+
"""
|
|
126
|
+
Placeholder for ffmpeg-based audio sync.
|
|
127
|
+
"""
|
|
128
|
+
print(f"🎙 Audio sync not implemented yet: {audio_path}")
|