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.
Files changed (52) hide show
  1. {swap_cli-0.1.2 → swap_cli-0.1.3}/PKG-INFO +1 -1
  2. {swap_cli-0.1.2 → swap_cli-0.1.3}/pyproject.toml +1 -1
  3. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/display.py +14 -48
  4. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/gui.py +2 -134
  5. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/runtime.py +0 -16
  6. swap_cli-0.1.3/src/swap_cli/version.py +1 -0
  7. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_watermark.py +0 -61
  8. swap_cli-0.1.2/src/swap_cli/version.py +0 -1
  9. {swap_cli-0.1.2 → swap_cli-0.1.3}/.github/workflows/ci.yml +0 -0
  10. {swap_cli-0.1.2 → swap_cli-0.1.3}/.github/workflows/release.yml +0 -0
  11. {swap_cli-0.1.2 → swap_cli-0.1.3}/.gitignore +0 -0
  12. {swap_cli-0.1.2 → swap_cli-0.1.3}/CHANGELOG.md +0 -0
  13. {swap_cli-0.1.2 → swap_cli-0.1.3}/LICENSE.md +0 -0
  14. {swap_cli-0.1.2 → swap_cli-0.1.3}/README.md +0 -0
  15. {swap_cli-0.1.2 → swap_cli-0.1.3}/docs/RELEASING.md +0 -0
  16. {swap_cli-0.1.2 → swap_cli-0.1.3}/scripts/mirror_voices.py +0 -0
  17. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/__init__.py +0 -0
  18. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/__main__.py +0 -0
  19. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/assets/watermark_default.png +0 -0
  20. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/camera.py +0 -0
  21. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/cli.py +0 -0
  22. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/config.py +0 -0
  23. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/devices.py +0 -0
  24. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/license.py +0 -0
  25. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/rvc_catalog.py +0 -0
  26. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_engines/__init__.py +0 -0
  27. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_engines/rvc_converter.py +0 -0
  28. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_engines/rvc_engine.py +0 -0
  29. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_library.py +0 -0
  30. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_ops.py +0 -0
  31. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_prereq.py +0 -0
  32. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_router.py +0 -0
  33. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voice_track.py +0 -0
  34. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/voices/__init__.py +0 -0
  35. {swap_cli-0.1.2 → swap_cli-0.1.3}/src/swap_cli/watermark.py +0 -0
  36. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_config.py +0 -0
  37. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_cuda_torch.py +0 -0
  38. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_devices.py +0 -0
  39. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_engine_wiring.py +0 -0
  40. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_engines.py +0 -0
  41. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_fairseq_patch.py +0 -0
  42. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_runtime_timeout.py +0 -0
  43. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_rvc.py +0 -0
  44. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_rvc_catalog.py +0 -0
  45. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_settings_modal.py +0 -0
  46. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_silent_threshold.py +0 -0
  47. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_sola.py +0 -0
  48. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_virtual_camera.py +0 -0
  49. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_voice_prereq.py +0 -0
  50. {swap_cli-0.1.2 → swap_cli-0.1.3}/tests/test_voice_router.py +0 -0
  51. {swap_cli-0.1.2 → swap_cli-0.1.3}/tools/build_library.py +0 -0
  52. {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.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "swap-cli"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Real-time deepfake on your desktop. Bring your own Decart API key."
5
5
  authors = [{ name = "BlAcQW", email = "enochhenyo@gmail.com" }]
6
6
  readme = "README.md"
@@ -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
- if self._show_window:
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
- if self._show_window:
103
- cv2.namedWindow(WINDOW_TITLE, cv2.WINDOW_NORMAL)
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
- if self._show_window:
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
- self, roi: tuple[int, int, int, int] | None = None
179
- ) -> None:
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
- if roi is None:
200
- roi = cv2.selectROI(
201
- "select watermark — ENTER to save, C to cancel",
202
- source,
203
- showCrosshair=False,
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, Any, Callable
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 — click Stop (or close the preview) to end the session",
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