swap-cli 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {swap_cli-0.1.2 → swap_cli-0.1.3}/PKG-INFO +1 -1
- {swap_cli-0.1.2 → swap_cli-0.1.3}/pyproject.toml +1 -1
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/display.py +14 -48
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/gui.py +2 -134
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/runtime.py +0 -16
- swap_cli-0.1.3/src/swap_cli/version.py +1 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_watermark.py +0 -61
- swap_cli-0.1.2/src/swap_cli/version.py +0 -1
- {swap_cli-0.1.2 → swap_cli-0.1.3}/.github/workflows/ci.yml +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/.github/workflows/release.yml +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/.gitignore +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/CHANGELOG.md +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/LICENSE.md +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/README.md +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/docs/RELEASING.md +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/scripts/mirror_voices.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/__init__.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/__main__.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/assets/watermark_default.png +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/camera.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/cli.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/config.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/devices.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/license.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/rvc_catalog.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_engines/__init__.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_engines/rvc_converter.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_engines/rvc_engine.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_library.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_ops.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_prereq.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_router.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_track.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voices/__init__.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/watermark.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_config.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_cuda_torch.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_devices.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_engine_wiring.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_engines.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_fairseq_patch.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_runtime_timeout.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_rvc.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_rvc_catalog.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_settings_modal.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_silent_threshold.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_sola.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_virtual_camera.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_voice_prereq.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_voice_router.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tools/build_library.py +0 -0
- {swap_cli-0.1.2 → swap_cli-0.1.3}/tools/personas.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swap-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Real-time deepfake on your desktop. Bring your own Decart API key.
|
|
5
5
|
Project-URL: Homepage, https://github.com/BlAcQW/swap-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/BlAcQW/swap-cli
|
|
@@ -39,16 +39,11 @@ class Display:
|
|
|
39
39
|
on_quit: callable = lambda: None, # type: ignore[assignment]
|
|
40
40
|
virtual_camera: bool = False,
|
|
41
41
|
watermark: WatermarkRemover | None = None,
|
|
42
|
-
show_window: bool = True,
|
|
43
42
|
) -> None:
|
|
44
43
|
self._track = track
|
|
45
44
|
self._record_path = record_path
|
|
46
45
|
self._on_quit = on_quit
|
|
47
46
|
self._virtual_camera = virtual_camera
|
|
48
|
-
# Show the cv2 preview window. False in GUI mode: the session runs on a
|
|
49
|
-
# worker thread, and macOS forbids OpenCV HighGUI off the main thread, so
|
|
50
|
-
# the GUI renders frames itself (tkinter, main thread) via latest_frame().
|
|
51
|
-
self._show_window = show_window
|
|
52
47
|
# Sprint 15: optional per-frame watermark remover. None = no-op.
|
|
53
48
|
# Injected as a configured instance so _loop stays thin and the
|
|
54
49
|
# remover is unit-testable in isolation.
|
|
@@ -79,8 +74,7 @@ class Display:
|
|
|
79
74
|
with suppress(Exception):
|
|
80
75
|
self._vcam.close()
|
|
81
76
|
self._vcam = None
|
|
82
|
-
|
|
83
|
-
cv2.destroyAllWindows()
|
|
77
|
+
cv2.destroyAllWindows()
|
|
84
78
|
|
|
85
79
|
def snapshot(self, dest: Path) -> bool:
|
|
86
80
|
"""Save the most recent rendered frame as JPEG. Returns success."""
|
|
@@ -89,19 +83,9 @@ class Display:
|
|
|
89
83
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
90
84
|
return cv2.imwrite(str(dest), self._latest_bgr)
|
|
91
85
|
|
|
92
|
-
def latest_frame(self) -> np.ndarray | None:
|
|
93
|
-
"""Most recent processed (cleaned) frame — for the in-app GUI preview.
|
|
94
|
-
The render loop rebinds this each frame, so the read is atomic."""
|
|
95
|
-
return self._latest_bgr
|
|
96
|
-
|
|
97
|
-
def latest_raw_frame(self) -> np.ndarray | None:
|
|
98
|
-
"""Most recent RAW (pre-removal) frame — for GUI watermark capture."""
|
|
99
|
-
return self._latest_raw_bgr
|
|
100
|
-
|
|
101
86
|
async def _loop(self) -> None:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
cv2.resizeWindow(WINDOW_TITLE, 960, 540)
|
|
87
|
+
cv2.namedWindow(WINDOW_TITLE, cv2.WINDOW_NORMAL)
|
|
88
|
+
cv2.resizeWindow(WINDOW_TITLE, 960, 540)
|
|
105
89
|
first_frame = True
|
|
106
90
|
try:
|
|
107
91
|
while not self._stopped.is_set():
|
|
@@ -117,6 +101,7 @@ class Display:
|
|
|
117
101
|
self._maybe_init_writer(bgr.shape, fps_guess=20)
|
|
118
102
|
if self._writer is not None:
|
|
119
103
|
self._writer.write(bgr)
|
|
104
|
+
cv2.imshow(WINDOW_TITLE, bgr)
|
|
120
105
|
# Sprint 14k: also push the frame to the OBS Virtual Camera
|
|
121
106
|
# driver so Zoom/Meet/Discord pick it up as a real camera.
|
|
122
107
|
# pyvirtualcam expects RGB.
|
|
@@ -130,15 +115,6 @@ class Display:
|
|
|
130
115
|
except Exception as err: # noqa: BLE001
|
|
131
116
|
print(f"[display] vcam send error: {err}", flush=True)
|
|
132
117
|
# Don't tear the driver down on a single bad frame.
|
|
133
|
-
if not self._show_window:
|
|
134
|
-
# GUI renders the preview itself (tkinter, main thread) and
|
|
135
|
-
# drives capture/stop — no cv2 window/keys here. Yield so a
|
|
136
|
-
# non-vcam session doesn't spin the loop hot.
|
|
137
|
-
if not self._virtual_camera:
|
|
138
|
-
await asyncio.sleep(0)
|
|
139
|
-
first_frame = False
|
|
140
|
-
continue
|
|
141
|
-
cv2.imshow(WINDOW_TITLE, bgr)
|
|
142
118
|
if first_frame:
|
|
143
119
|
# Flash topmost so the cv2 window pops above the tk GUI on
|
|
144
120
|
# Windows. We don't want it pinned forever — just one beat.
|
|
@@ -166,20 +142,11 @@ class Display:
|
|
|
166
142
|
self._on_quit()
|
|
167
143
|
self._stopped.set()
|
|
168
144
|
finally:
|
|
169
|
-
|
|
170
|
-
cv2.destroyAllWindows()
|
|
171
|
-
|
|
172
|
-
def capture_watermark(self, roi: tuple[int, int, int, int]) -> None:
|
|
173
|
-
"""Public entry for the GUI: capture the badge template from a caller-
|
|
174
|
-
supplied ROI (the GUI's tk drag-selector), instead of cv2.selectROI."""
|
|
175
|
-
self._capture_watermark_template(roi=roi)
|
|
145
|
+
cv2.destroyAllWindows()
|
|
176
146
|
|
|
177
|
-
def _capture_watermark_template(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
"""Select the watermark on the current frame and save it as the template
|
|
181
|
-
PNG (Sprint 15). `roi` is supplied by the GUI's tk selector; when None
|
|
182
|
-
(CLI / cv2-window mode) we fall back to cv2.selectROI on the W key.
|
|
147
|
+
def _capture_watermark_template(self) -> None:
|
|
148
|
+
"""Drag-select the watermark on the current frame and save it as the
|
|
149
|
+
template PNG (Sprint 15). Bound to the `w` key in the preview window.
|
|
183
150
|
|
|
184
151
|
Captures from the RAW (pre-removal) frame so it works even while
|
|
185
152
|
removal is on but not matching. Persists the path AND the frame width
|
|
@@ -196,13 +163,12 @@ class Display:
|
|
|
196
163
|
try:
|
|
197
164
|
from . import config as _config
|
|
198
165
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
cv2.destroyWindow("select watermark — ENTER to save, C to cancel")
|
|
166
|
+
roi = cv2.selectROI(
|
|
167
|
+
"select watermark — ENTER to save, C to cancel",
|
|
168
|
+
source,
|
|
169
|
+
showCrosshair=False,
|
|
170
|
+
)
|
|
171
|
+
cv2.destroyWindow("select watermark — ENTER to save, C to cancel")
|
|
206
172
|
x, y, w, h = (int(v) for v in roi)
|
|
207
173
|
if w <= 0 or h <= 0:
|
|
208
174
|
print("[display] watermark capture cancelled.", flush=True)
|
|
@@ -11,10 +11,9 @@ import asyncio
|
|
|
11
11
|
import sys
|
|
12
12
|
import threading
|
|
13
13
|
import tkinter as tk
|
|
14
|
-
from contextlib import suppress
|
|
15
14
|
from pathlib import Path
|
|
16
15
|
from tkinter import filedialog
|
|
17
|
-
from typing import TYPE_CHECKING,
|
|
16
|
+
from typing import TYPE_CHECKING, Callable
|
|
18
17
|
|
|
19
18
|
import customtkinter as ctk
|
|
20
19
|
from PIL import Image
|
|
@@ -117,14 +116,6 @@ class SwapGUI(ctk.CTk):
|
|
|
117
116
|
# Set by runtime.run_session via the on_runtime_ready callback. Calling
|
|
118
117
|
# this from the tk main thread cleanly winds the session down.
|
|
119
118
|
self._stop_session: Callable[[], None] | None = None
|
|
120
|
-
# In-app live preview (macOS-safe: rendered on the tk main thread instead
|
|
121
|
-
# of an OpenCV window). _display is the live Display, handed over via
|
|
122
|
-
# on_display_ready; the rest is the preview-window/render-loop state.
|
|
123
|
-
self._display: Any = None
|
|
124
|
-
self._preview_win: ctk.CTkToplevel | None = None
|
|
125
|
-
self._preview_label: ctk.CTkLabel | None = None
|
|
126
|
-
self._preview_image: ctk.CTkImage | None = None
|
|
127
|
-
self._preview_after_id: str | None = None
|
|
128
119
|
# Voice-only test state (no Decart). Independent of _session_thread.
|
|
129
120
|
self._voice_test_thread: threading.Thread | None = None
|
|
130
121
|
self._voice_test_loop: asyncio.AbstractEventLoop | None = None
|
|
@@ -480,7 +471,7 @@ class SwapGUI(ctk.CTk):
|
|
|
480
471
|
# Footer (pinned)
|
|
481
472
|
ctk.CTkLabel(
|
|
482
473
|
bottom,
|
|
483
|
-
text="swap-cli —
|
|
474
|
+
text="swap-cli — Press Q in the preview window to stop",
|
|
484
475
|
font=ctk.CTkFont(size=10),
|
|
485
476
|
text_color="#6b7280",
|
|
486
477
|
).pack(pady=(8, 0))
|
|
@@ -669,11 +660,6 @@ class SwapGUI(ctk.CTk):
|
|
|
669
660
|
def _capture_stop(stop_fn: Callable[[], None]) -> None:
|
|
670
661
|
self._stop_session = stop_fn
|
|
671
662
|
|
|
672
|
-
def _on_display_ready(disp: Any) -> None:
|
|
673
|
-
# Called from the worker thread; just store the ref. The main-thread
|
|
674
|
-
# render loop (started below) picks it up to draw frames.
|
|
675
|
-
self._display = disp
|
|
676
|
-
|
|
677
663
|
# Voice opts: only set when toggle is on AND a library/user voice is
|
|
678
664
|
# picked. We resolve the mic + virtual cable here using
|
|
679
665
|
# voice_router's auto-detect so the user doesn't have to pick from
|
|
@@ -708,8 +694,6 @@ class SwapGUI(ctk.CTk):
|
|
|
708
694
|
record=record_path,
|
|
709
695
|
on_status_change=_emit_status,
|
|
710
696
|
on_runtime_ready=_capture_stop,
|
|
711
|
-
show_preview_window=False, # render in-app (macOS-safe), not a cv2 window
|
|
712
|
-
on_display_ready=_on_display_ready,
|
|
713
697
|
reference_voice=voice_id,
|
|
714
698
|
microphone_device=mic_device,
|
|
715
699
|
voice_output_device=out_device,
|
|
@@ -771,9 +755,6 @@ class SwapGUI(ctk.CTk):
|
|
|
771
755
|
self._session_thread = threading.Thread(target=worker, daemon=True)
|
|
772
756
|
self._session_thread.start()
|
|
773
757
|
|
|
774
|
-
# Open the in-app live preview (tk main thread) and start polling frames.
|
|
775
|
-
self._open_preview()
|
|
776
|
-
|
|
777
758
|
# Best-effort license check (non-blocking).
|
|
778
759
|
threading.Thread(target=self._check_license_async, daemon=True).start()
|
|
779
760
|
|
|
@@ -830,116 +811,6 @@ class SwapGUI(ctk.CTk):
|
|
|
830
811
|
self._stop_btn.configure(state="disabled")
|
|
831
812
|
self._stop_session = None
|
|
832
813
|
|
|
833
|
-
# ── In-app live preview (macOS-safe: tk main thread, not a cv2 window) ──
|
|
834
|
-
|
|
835
|
-
def _open_preview(self) -> None:
|
|
836
|
-
"""Open the live preview window and start polling frames on the main
|
|
837
|
-
thread. Replaces the cv2 window, which can't run off the main thread on
|
|
838
|
-
macOS (the session runs on a worker thread)."""
|
|
839
|
-
self._close_preview()
|
|
840
|
-
win = ctk.CTkToplevel(self)
|
|
841
|
-
win.title("swap — Lucy 2 live")
|
|
842
|
-
win.geometry("900x600")
|
|
843
|
-
self._preview_win = win
|
|
844
|
-
self._preview_label = ctk.CTkLabel(win, text="Connecting…")
|
|
845
|
-
self._preview_label.pack(fill="both", expand=True, padx=8, pady=8)
|
|
846
|
-
bar = ctk.CTkFrame(win, fg_color="transparent")
|
|
847
|
-
bar.pack(fill="x", padx=8, pady=(0, 8))
|
|
848
|
-
ctk.CTkButton(
|
|
849
|
-
bar, text="Capture watermark", command=self._on_capture_watermark
|
|
850
|
-
).pack(side="left")
|
|
851
|
-
ctk.CTkButton(
|
|
852
|
-
bar, text="Stop", fg_color="#b91c1c", hover_color="#991b1b",
|
|
853
|
-
command=self._on_stop,
|
|
854
|
-
).pack(side="right")
|
|
855
|
-
# Closing the preview window ends the session (mirrors the old Q-to-quit).
|
|
856
|
-
win.protocol("WM_DELETE_WINDOW", self._on_stop)
|
|
857
|
-
self._preview_after_id = self.after(100, self._render_preview)
|
|
858
|
-
|
|
859
|
-
def _render_preview(self) -> None:
|
|
860
|
-
win = self._preview_win
|
|
861
|
-
if win is None or not win.winfo_exists():
|
|
862
|
-
return
|
|
863
|
-
disp = self._display
|
|
864
|
-
frame = disp.latest_frame() if disp is not None else None
|
|
865
|
-
if frame is not None and self._preview_label is not None:
|
|
866
|
-
try:
|
|
867
|
-
img = Image.fromarray(frame[:, :, ::-1].copy()) # BGR→RGB
|
|
868
|
-
w, h = img.size
|
|
869
|
-
scale = min(884 / w, 500 / h, 1.0)
|
|
870
|
-
size = (max(1, int(w * scale)), max(1, int(h * scale)))
|
|
871
|
-
self._preview_image = ctk.CTkImage(light_image=img, dark_image=img, size=size)
|
|
872
|
-
self._preview_label.configure(image=self._preview_image, text="")
|
|
873
|
-
except Exception: # preview is best-effort; never crash the GUI
|
|
874
|
-
pass
|
|
875
|
-
# ~20 fps preview; the virtual camera still runs full-rate on the worker.
|
|
876
|
-
self._preview_after_id = self.after(50, self._render_preview)
|
|
877
|
-
|
|
878
|
-
def _close_preview(self) -> None:
|
|
879
|
-
if self._preview_after_id is not None:
|
|
880
|
-
with suppress(Exception):
|
|
881
|
-
self.after_cancel(self._preview_after_id)
|
|
882
|
-
self._preview_after_id = None
|
|
883
|
-
if self._preview_win is not None:
|
|
884
|
-
with suppress(Exception):
|
|
885
|
-
self._preview_win.destroy()
|
|
886
|
-
self._preview_win = None
|
|
887
|
-
self._preview_label = None
|
|
888
|
-
self._preview_image = None
|
|
889
|
-
self._display = None
|
|
890
|
-
|
|
891
|
-
def _on_capture_watermark(self) -> None:
|
|
892
|
-
"""Grab the current RAW frame and let the user box the badge (tk drag
|
|
893
|
-
selector) — the cross-platform replacement for the cv2 W-key capture."""
|
|
894
|
-
disp = self._display
|
|
895
|
-
frame = disp.latest_raw_frame() if disp is not None else None
|
|
896
|
-
if frame is None:
|
|
897
|
-
self._status_var.set("No live frame yet — wait for the preview.")
|
|
898
|
-
return
|
|
899
|
-
self._select_roi_and_capture(frame)
|
|
900
|
-
|
|
901
|
-
def _select_roi_and_capture(self, frame: Any) -> None:
|
|
902
|
-
from PIL import ImageTk
|
|
903
|
-
|
|
904
|
-
h, w = frame.shape[:2]
|
|
905
|
-
scale = min(900 / w, 600 / h, 1.0)
|
|
906
|
-
dw, dh = max(1, int(w * scale)), max(1, int(h * scale))
|
|
907
|
-
photo = ImageTk.PhotoImage(Image.fromarray(frame[:, :, ::-1].copy()).resize((dw, dh)))
|
|
908
|
-
top = ctk.CTkToplevel(self)
|
|
909
|
-
top.title("Drag a box around the badge, release to capture")
|
|
910
|
-
canvas = tk.Canvas(top, width=dw, height=dh, highlightthickness=0, cursor="crosshair")
|
|
911
|
-
canvas.pack()
|
|
912
|
-
canvas.create_image(0, 0, anchor="nw", image=photo)
|
|
913
|
-
canvas._photo = photo # keep a ref so it isn't GC'd
|
|
914
|
-
st: dict[str, Any] = {"x0": 0, "y0": 0, "rect": None}
|
|
915
|
-
|
|
916
|
-
def on_press(e: Any) -> None:
|
|
917
|
-
st["x0"], st["y0"] = e.x, e.y
|
|
918
|
-
if st["rect"] is not None:
|
|
919
|
-
canvas.delete(st["rect"])
|
|
920
|
-
st["rect"] = canvas.create_rectangle(e.x, e.y, e.x, e.y, outline="#22d3ee", width=2)
|
|
921
|
-
|
|
922
|
-
def on_drag(e: Any) -> None:
|
|
923
|
-
if st["rect"] is not None:
|
|
924
|
-
canvas.coords(st["rect"], st["x0"], st["y0"], e.x, e.y)
|
|
925
|
-
|
|
926
|
-
def on_release(e: Any) -> None:
|
|
927
|
-
rx, ry = int(min(st["x0"], e.x) / scale), int(min(st["y0"], e.y) / scale)
|
|
928
|
-
rw, rh = int(abs(e.x - st["x0"]) / scale), int(abs(e.y - st["y0"]) / scale)
|
|
929
|
-
with suppress(Exception):
|
|
930
|
-
top.destroy()
|
|
931
|
-
if rw > 5 and rh > 5 and self._display is not None:
|
|
932
|
-
try:
|
|
933
|
-
self._display.capture_watermark((rx, ry, rw, rh))
|
|
934
|
-
self._status_var.set("Watermark captured — removal updated.")
|
|
935
|
-
except Exception as err:
|
|
936
|
-
self._status_var.set(f"Capture failed: {err}")
|
|
937
|
-
|
|
938
|
-
canvas.bind("<Button-1>", on_press)
|
|
939
|
-
canvas.bind("<B1-Motion>", on_drag)
|
|
940
|
-
canvas.bind("<ButtonRelease-1>", on_release)
|
|
941
|
-
top.grab_set()
|
|
942
|
-
|
|
943
814
|
def _on_enable_voice(self) -> None:
|
|
944
815
|
"""Open the Enable Voice modal: prereq check + guided install."""
|
|
945
816
|
modal = _EnableVoiceModal(self)
|
|
@@ -1136,9 +1007,6 @@ class SwapGUI(ctk.CTk):
|
|
|
1136
1007
|
return label_to_id.get(self._voice_var.get())
|
|
1137
1008
|
|
|
1138
1009
|
def _set_running(self, running: bool) -> None:
|
|
1139
|
-
if not running:
|
|
1140
|
-
# Session ended (stop / drop / error) → tear down the live preview.
|
|
1141
|
-
self._close_preview()
|
|
1142
1010
|
self._live_btn.configure(state="disabled" if running else "normal")
|
|
1143
1011
|
self._stop_btn.configure(state="normal" if running else "disabled")
|
|
1144
1012
|
self._select_face_btn.configure(state="disabled" if running else "normal")
|
|
@@ -66,14 +66,6 @@ class RunOptions:
|
|
|
66
66
|
# the session is set up. Calling that function from any thread cleanly
|
|
67
67
|
# winds down the live session.
|
|
68
68
|
on_runtime_ready: Callable[[Callable[[], None]], None] | None = None
|
|
69
|
-
# Show the cv2 preview window. False in GUI mode: the GUI renders the preview
|
|
70
|
-
# itself (tkinter, main thread) because macOS forbids OpenCV windows off the
|
|
71
|
-
# main thread (the session runs on a worker thread).
|
|
72
|
-
show_preview_window: bool = True
|
|
73
|
-
# Optional callback handed the live Display once created, so the GUI can poll
|
|
74
|
-
# frames (display.latest_frame()) and drive watermark capture. Called from the
|
|
75
|
-
# worker thread; the GUI marshals to the tk main thread.
|
|
76
|
-
on_display_ready: Callable[[Any], None] | None = None
|
|
77
69
|
# Voice cloning (optional, sprint 13). All None = video-only, current behavior.
|
|
78
70
|
# When all three are set, run_session will spin a parallel voice task in 13b.
|
|
79
71
|
reference_voice: str | None = None # voice id (library) or path to WAV/MP3
|
|
@@ -170,8 +162,6 @@ async def run_session(opts: RunOptions) -> None:
|
|
|
170
162
|
display_box=display_box,
|
|
171
163
|
virtual_camera=opts.virtual_camera,
|
|
172
164
|
watermark=_build_watermark_remover(opts),
|
|
173
|
-
show_window=opts.show_preview_window,
|
|
174
|
-
on_display_ready=opts.on_display_ready,
|
|
175
165
|
),
|
|
176
166
|
initial_state=ModelState(
|
|
177
167
|
prompt=Prompt(text=opts.prompt, enhance=True),
|
|
@@ -376,8 +366,6 @@ def _on_remote_stream(
|
|
|
376
366
|
display_box: list[Display | None],
|
|
377
367
|
virtual_camera: bool = False,
|
|
378
368
|
watermark: Any = None,
|
|
379
|
-
show_window: bool = True,
|
|
380
|
-
on_display_ready: Callable[[Any], None] | None = None,
|
|
381
369
|
) -> None:
|
|
382
370
|
record_path = record if record is not None else None
|
|
383
371
|
if record is not None and not record.is_absolute():
|
|
@@ -391,13 +379,9 @@ def _on_remote_stream(
|
|
|
391
379
|
on_quit=quit_event.set,
|
|
392
380
|
virtual_camera=virtual_camera,
|
|
393
381
|
watermark=watermark,
|
|
394
|
-
show_window=show_window,
|
|
395
382
|
)
|
|
396
383
|
disp.start()
|
|
397
384
|
display_box[0] = disp
|
|
398
|
-
if on_display_ready is not None:
|
|
399
|
-
with suppress(Exception):
|
|
400
|
-
on_display_ready(disp)
|
|
401
385
|
|
|
402
386
|
|
|
403
387
|
def _build_set_input(prompt: str, reference: str) -> Any:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.3"
|
|
@@ -853,66 +853,5 @@ def test_bundled_template_uses_its_own_ref_width() -> None:
|
|
|
853
853
|
assert rem._params.template_ref_width == BUNDLED_TEMPLATE_REF_WIDTH
|
|
854
854
|
|
|
855
855
|
|
|
856
|
-
def test_display_windowless_skips_cv2_window(monkeypatch) -> None:
|
|
857
|
-
"""GUI mode (show_window=False): the loop opens NO cv2 window (macOS can't
|
|
858
|
-
run HighGUI off the main thread) and exposes frames via latest_frame()."""
|
|
859
|
-
import asyncio
|
|
860
|
-
from unittest.mock import MagicMock
|
|
861
|
-
|
|
862
|
-
from swap_cli import display as disp_mod
|
|
863
|
-
from swap_cli.display import Display
|
|
864
|
-
|
|
865
|
-
called = {"w": False}
|
|
866
|
-
for fn in ("namedWindow", "resizeWindow", "imshow", "waitKey",
|
|
867
|
-
"destroyAllWindows", "setWindowProperty"):
|
|
868
|
-
monkeypatch.setattr(disp_mod.cv2, fn,
|
|
869
|
-
lambda *a, **k: called.__setitem__("w", True))
|
|
870
|
-
|
|
871
|
-
arr = np.zeros((12, 16, 3), np.uint8)
|
|
872
|
-
arr[4:8, 4:12] = 200
|
|
873
|
-
disp = Display(track=MagicMock(), show_window=False)
|
|
874
|
-
calls = {"n": 0}
|
|
875
|
-
|
|
876
|
-
async def _recv():
|
|
877
|
-
calls["n"] += 1
|
|
878
|
-
if calls["n"] >= 2:
|
|
879
|
-
disp._stopped.set()
|
|
880
|
-
frame = MagicMock()
|
|
881
|
-
frame.to_ndarray = lambda format: arr # to_ndarray(format=...) keyword
|
|
882
|
-
return frame
|
|
883
|
-
|
|
884
|
-
disp._track.recv = _recv
|
|
885
|
-
asyncio.run(disp._loop()) # must not raise
|
|
886
|
-
assert called["w"] is False # no cv2 window touched
|
|
887
|
-
assert disp.latest_frame() is not None
|
|
888
|
-
assert disp.latest_raw_frame() is not None
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
def test_capture_watermark_with_roi_writes_template(tmp_path: Path, monkeypatch) -> None:
|
|
892
|
-
"""GUI capture path: capture_watermark(roi) crops the RAW frame, writes the
|
|
893
|
-
template, and updates config — no cv2 window needed (replaces the W key)."""
|
|
894
|
-
from unittest.mock import MagicMock
|
|
895
|
-
|
|
896
|
-
from swap_cli import config as cfgmod
|
|
897
|
-
from swap_cli import display as disp_mod
|
|
898
|
-
from swap_cli.display import Display
|
|
899
|
-
|
|
900
|
-
monkeypatch.setattr(cfgmod, "config_path", lambda: tmp_path / "config.toml")
|
|
901
|
-
dest = tmp_path / "wm.png"
|
|
902
|
-
monkeypatch.setattr(disp_mod, "default_watermark_template_path", lambda: dest)
|
|
903
|
-
|
|
904
|
-
frame = _textured_frame()
|
|
905
|
-
cv2.putText(frame, "AI GENERATED", (310, 84), cv2.FONT_HERSHEY_SIMPLEX, 0.6,
|
|
906
|
-
(255, 255, 255), 2, cv2.LINE_AA)
|
|
907
|
-
disp = Display(track=MagicMock(), show_window=False)
|
|
908
|
-
disp._latest_raw_bgr = frame
|
|
909
|
-
disp.capture_watermark((300, 60, 180, 40))
|
|
910
|
-
|
|
911
|
-
assert dest.is_file()
|
|
912
|
-
cfg = cfgmod.load()
|
|
913
|
-
assert cfg.watermark_template == str(dest)
|
|
914
|
-
assert cfg.watermark_template_width == frame.shape[1]
|
|
915
|
-
|
|
916
|
-
|
|
917
856
|
if __name__ == "__main__":
|
|
918
857
|
sys.exit(pytest.main([__file__, "-v"]))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|