Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions api/routes/compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
CompressionStartResponse,
CompressionStats,
CompressionStatus,
ForceRecompressPreviewRequest,
ForceRecompressPreviewResponse,
ForceRecompressRequest,
ForceRecompressResponse,
)
from core.compression import CompressionProgress, compression_service
from core.config import config
Expand Down Expand Up @@ -119,3 +123,79 @@ async def get_compression_stats():
original_size_bytes=stats["original_size_bytes"],
estimated_savings_bytes=estimated_savings,
)


@router.post("/force-recompress/preview", response_model=ForceRecompressPreviewResponse)
async def preview_force_recompress(request: ForceRecompressPreviewRequest):
"""
Preview force recompression impact.

Shows how many screenshots would be affected by force recompression,
including how many are already compressed (which will lose quality).
"""
counts = db.get_force_recompressible_count(request.older_than_days)

warning = ""
if counts["already_compressed"] > 0:
warning = (
f"{counts['already_compressed']} screenshots are already compressed. "
"Re-compressing them will further reduce quality and cannot be undone."
)

return ForceRecompressPreviewResponse(
total_count=counts["total"],
already_compressed_count=counts["already_compressed"],
not_compressed_count=counts["not_compressed"],
warning=warning,
)


@router.post("/force-recompress", response_model=ForceRecompressResponse)
async def start_force_recompress(request: ForceRecompressRequest):
"""
Start force recompression of all screenshots older than specified days.

Unlike normal compression, this will re-compress already-compressed screenshots.
This is destructive and cannot be undone.

Requires confirm=true to proceed.
"""
if not request.confirm:
return ForceRecompressResponse(
success=False,
message="Must set confirm=true to proceed with force recompression",
affected_count=0,
)

if compression_service.is_running:
return ForceRecompressResponse(
success=False,
message="Compression is already running",
affected_count=0,
)

counts = db.get_force_recompressible_count(request.older_than_days)
if counts["total"] == 0:
return ForceRecompressResponse(
success=True,
message=f"No screenshots older than {request.older_than_days} days found",
affected_count=0,
)

started = compression_service.start_force_recompress(
older_than_days=request.older_than_days,
quality=request.quality,
)

if not started:
return ForceRecompressResponse(
success=False,
message="Failed to start - compression may already be running",
affected_count=0,
)

return ForceRecompressResponse(
success=True,
message=f"Force recompression started for {counts['total']} screenshots",
affected_count=counts["total"],
)
44 changes: 40 additions & 4 deletions api/routes/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,45 @@ async def search_screenshots(request: SearchRequest):
detail="No synced screenshots. Run sync first to generate embeddings.",
)

# Validate: need either query or image
if not request.query and request.image is None:
raise HTTPException(
status_code=400,
detail="Either query or image must be provided.",
)

# Generate embeddings based on search mode
image_embedding = None
text_embedding = None

# When image param is provided, use it for embedding
if request.image is not None:
if isinstance(request.image, int):
# Screenshot ID — use stored embedding
image_embedding = db.get_embedding(request.image)
if image_embedding is None:
raise HTTPException(
status_code=400,
detail="Screenshot has no embedding. Run sync first.",
)
else:
# Base64 image data — generate embedding from bytes
import base64

try:
image_bytes = base64.b64decode(request.image)
except Exception as e:
raise HTTPException(status_code=400, detail="Invalid base64 image data") from e
from core.embeddings import get_image_embedding_from_bytes

try:
image_embedding = get_image_embedding_from_bytes(image_bytes)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Failed to process image: {e}") from e

try:
# CLIP image embedding (for image and auto modes)
if request.search_mode in (SearchMode.AUTO, SearchMode.IMAGE):
if request.image is None and request.search_mode in (SearchMode.AUTO, SearchMode.IMAGE):
# CLIP image embedding (for image and auto modes)
if request.safe_mode:
image_embedding = get_safe_search_embedding(
text=request.query,
Expand All @@ -79,8 +111,8 @@ async def search_screenshots(request: SearchRequest):
else:
image_embedding = get_text_embedding(request.query)

# BGE text embedding (for text_semantic and auto modes)
if request.search_mode in (SearchMode.AUTO, SearchMode.TEXT_SEMANTIC):
# BGE text embedding (for text_semantic and auto modes) - skip when using image query
if request.image is None and request.search_mode in (SearchMode.AUTO, SearchMode.TEXT_SEMANTIC):
# Lazy import to avoid loading text model if not needed
from core.text_embeddings import text_embedding_service

Expand Down Expand Up @@ -167,6 +199,10 @@ async def search_screenshots(request: SearchRequest):
break
results = filtered_results

# Filter out the source screenshot when searching by ID
if isinstance(request.image, int):
results = [r for r in results if r["id"] != request.image]

# Convert to response
search_results = [
SearchResult(
Expand Down
36 changes: 35 additions & 1 deletion api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,37 @@ class CompressionStartResponse(BaseModel):
compressible_count: int


class ForceRecompressPreviewRequest(BaseModel):
"""Request to preview force recompression"""

older_than_days: int = Field(ge=30, le=365)


class ForceRecompressPreviewResponse(BaseModel):
"""Preview of force recompression impact"""

total_count: int
already_compressed_count: int
not_compressed_count: int
warning: str


class ForceRecompressRequest(BaseModel):
"""Request to start force recompression"""

older_than_days: int = Field(ge=30, le=365)
quality: int | None = Field(None, ge=50, le=90)
confirm: bool = False


class ForceRecompressResponse(BaseModel):
"""Response after starting force recompression"""

success: bool
message: str
affected_count: int


class SyncStartRequest(BaseModel):
"""Request to start sync"""

Expand Down Expand Up @@ -177,7 +208,10 @@ class SearchMode(str, Enum):
class SearchRequest(BaseModel):
"""Search request with mode selection"""

query: str = Field(..., min_length=1, max_length=500)
query: str = Field(default="", max_length=500)
image: int | str | None = Field(
default=None, description="Screenshot ID (int) or base64 image data (string) to use as image query"
)
limit: int = Field(default=20, ge=1, le=100)
search_mode: SearchMode = Field(
default=SearchMode.AUTO,
Expand Down
Loading