diff --git a/services/imajin-identity/service/src/api/routes/identities.py b/services/imajin-identity/service/src/api/routes/identities.py index 7ee2003d..d052bae5 100644 --- a/services/imajin-identity/service/src/api/routes/identities.py +++ b/services/imajin-identity/service/src/api/routes/identities.py @@ -139,6 +139,7 @@ async def create_identity( image_count=identity.image_count, created_at=identity.created_at, updated_at=identity.updated_at, + gender=identity.gender, ) except ValueError as e: @@ -166,6 +167,7 @@ async def list_identities(request: Request) -> IdentityListResponse: image_count=identity.image_count, created_at=identity.created_at, updated_at=identity.updated_at, + gender=identity.gender, ) for identity in identities ], @@ -199,6 +201,7 @@ async def get_identity(identity_id: str, request: Request) -> IdentityResponse: source_paths=identity.source_paths, created_at=identity.created_at, updated_at=identity.updated_at, + gender=identity.gender, ) @@ -306,6 +309,7 @@ async def update_identity( image_count=identity.image_count, created_at=identity.created_at, updated_at=identity.updated_at, + gender=identity.gender, ) except Exception as e: @@ -659,6 +663,66 @@ async def get_identity_photos(identity_id: str, request: Request) -> dict: return {"identity_id": identity_id, "photos": photos} +@router.get("/{identity_id}/photos/{photo_id}/source") +async def get_identity_photo_source(identity_id: str, photo_id: str, request: Request) -> dict: + """Get the full-resolution source image for a specific identity photo. + + The photo_id is the 8-char MD5 hash of the source path (same as returned + by the /photos endpoint). Returns the full image as base64 so the frontend + can use it as input for background repaint or other tools. + + Args: + identity_id: Identity ID (lowercase, underscores) + photo_id: 8-char photo ID from /photos endpoint + + Returns: + Dict with source_base64, width, height, format + """ + import base64 + import hashlib + import io + + from PIL import Image + + store = get_store(request) + + identity = await store.get(identity_id) + if identity is None: + raise HTTPException(status_code=404, detail=f"Identity '{identity_id}' not found") + + # Find the source_path whose MD5 prefix matches photo_id + source_path_str = None + for path_str in identity.source_paths: + if hashlib.md5(path_str.encode()).hexdigest()[:8] == photo_id: + source_path_str = path_str + break + + if source_path_str is None: + raise HTTPException(status_code=404, detail=f"Photo '{photo_id}' not found in identity '{identity_id}'") + + source_path = Path(source_path_str) + if not source_path.exists(): + raise HTTPException(status_code=404, detail=f"Source file no longer exists: {source_path_str}") + + img = Image.open(source_path) + if img.mode not in ("RGB", "RGBA"): + img = img.convert("RGB") + + img_format = img.format or "JPEG" + buf = io.BytesIO() + save_format = "JPEG" if img_format.upper() in ("JPEG", "JPG") else "PNG" + img.save(buf, format=save_format) + source_base64 = base64.b64encode(buf.getvalue()).decode("utf-8") + + return { + "photo_id": photo_id, + "source_base64": source_base64, + "width": img.width, + "height": img.height, + "format": save_format.lower(), + } + + @router.post("/search-in-namespace", response_model=NamespacedSearchResponse) async def search_in_namespace( request_body: NamespacedSearchRequest,