diff --git a/api/routes/status.py b/api/routes/status.py index 7abd905..cefe31a 100644 --- a/api/routes/status.py +++ b/api/routes/status.py @@ -100,6 +100,7 @@ async def get_config(): threshold=config.capture.threshold, save_threshold=config.capture.save_threshold, quality=config.capture.quality, + max_time_without_save=config.capture.max_time_without_save, ), compression=CompressionConfig( enabled=config.compression.enabled, @@ -147,6 +148,10 @@ async def update_config(request: ConfigUpdateRequest): config.capture.quality = request.capture_quality updated.append(f"capture_quality={request.capture_quality}") + if request.capture_max_time_without_save is not None: + config.capture.max_time_without_save = request.capture_max_time_without_save + updated.append(f"capture_max_time_without_save={request.capture_max_time_without_save}") + if request.compression_enabled is not None: config.compression.enabled = request.compression_enabled updated.append(f"compression_enabled={request.compression_enabled}") diff --git a/api/schemas.py b/api/schemas.py index 961ddc0..d1f5fbe 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -465,6 +465,7 @@ class CaptureConfig(BaseModel): threshold: float save_threshold: float quality: int + max_time_without_save: float class CompressionConfig(BaseModel): @@ -494,6 +495,7 @@ class ConfigUpdateRequest(BaseModel): capture_interval: float | None = Field(None, ge=0.5, le=60.0) capture_threshold: float | None = Field(None, ge=0.5, le=0.99) capture_quality: int | None = Field(None, ge=50, le=100) + capture_max_time_without_save: float | None = Field(None, ge=0, le=120) compression_enabled: bool | None = None compression_after_days: int | None = Field(None, ge=7, le=365) compression_quality: int | None = Field(None, ge=50, le=90) diff --git a/core/capture.py b/core/capture.py index 485facf..0a0b774 100644 --- a/core/capture.py +++ b/core/capture.py @@ -26,6 +26,7 @@ def __init__(self): self._thread: threading.Thread | None = None self._previous_screenshot: np.ndarray | None = None self._on_capture: Callable[[str], None] | None = None + self._last_save_time: float = 0.0 # Track last save for force-save logic @property def is_running(self) -> bool: @@ -107,6 +108,7 @@ def _capture_loop(self): """Main capture loop""" # Initialize with first screenshot self._previous_screenshot = self._image_to_array(self._grab_screen()) + self._last_save_time = time.time() # Initialize timer while self._running: try: @@ -129,6 +131,7 @@ def _capture_loop(self): # Wait for screen to stabilize stable_checks = 3 + is_stable = True while stable_checks > 0 and self._running: stable_checks -= 1 time.sleep(config.capture.interval * 0.3) @@ -138,12 +141,26 @@ def _capture_loop(self): stability = self._calculate_ssim(self._previous_screenshot, new_array) if stability <= config.capture.save_threshold: - # Still changing, reset + # Still changing + is_stable = False break - else: - # Screen is stable, save screenshot + + # Check if we should force save due to time threshold + time_since_last = time.time() - self._last_save_time + should_force_save = ( + not is_stable + and config.capture.max_time_without_save > 0 + and time_since_last >= config.capture.max_time_without_save + ) + + if is_stable or should_force_save: + if should_force_save: + print(f"Force saving after {time_since_last:.1f}s of chaotic changes") + + # Save screenshot filepath = self._save_screenshot(current_image) print(f"Screenshot saved: {filepath}") + self._last_save_time = time.time() # Reset timer if self._on_capture: self._on_capture(filepath) @@ -159,6 +176,7 @@ def capture_now(self) -> str: image = self._grab_screen() filepath = self._save_screenshot(image) self._previous_screenshot = self._image_to_array(image) + self._last_save_time = time.time() # Reset timer return filepath diff --git a/core/config.py b/core/config.py index a7a1d13..c76fdbe 100644 --- a/core/config.py +++ b/core/config.py @@ -128,6 +128,7 @@ class CaptureSettings: threshold: float = 0.9 # SSIM threshold for change detection save_threshold: float = 0.6 # SSIM threshold for saving quality: int = 95 # JPEG quality (1-100) + max_time_without_save: float = 40.0 # Force save after this many seconds of chaotic changes # Preset modes MODES = { @@ -240,6 +241,7 @@ def save(self): "threshold": self.capture.threshold, "save_threshold": self.capture.save_threshold, "quality": self.capture.quality, + "max_time_without_save": self.capture.max_time_without_save, }, "compression": { "enabled": self.compression.enabled, @@ -291,6 +293,9 @@ def load(self): self.capture.threshold = cap.get("threshold", self.capture.threshold) self.capture.save_threshold = cap.get("save_threshold", self.capture.save_threshold) self.capture.quality = cap.get("quality", self.capture.quality) + self.capture.max_time_without_save = cap.get( + "max_time_without_save", self.capture.max_time_without_save + ) # Load compression settings if "compression" in data: diff --git a/web/src/app/settings/page.tsx b/web/src/app/settings/page.tsx index ce37ede..ea769b3 100644 --- a/web/src/app/settings/page.tsx +++ b/web/src/app/settings/page.tsx @@ -278,6 +278,15 @@ export default function SettingsPage() { onChange={(val) => handleUpdate({ capture_quality: val })} /> + + handleUpdate({ capture_max_time_without_save: val })} + /> + {/* Sync */} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 385b4c6..8073dd2 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -64,6 +64,7 @@ export async function updateConfig(updates: Partial<{ capture_threshold: number; capture_save_threshold: number; capture_quality: number; + capture_max_time_without_save: number; compression_enabled: boolean; compression_after_days: number; compression_quality: number; diff --git a/web/src/types/index.ts b/web/src/types/index.ts index ae2983a..5cccc72 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -72,6 +72,7 @@ export interface CaptureConfig { threshold: number; save_threshold: number; quality: number; + max_time_without_save: number; } export interface CompressionConfig {