Spaces:
Sleeping
Sleeping
Commit
·
9be6c71
0
Parent(s):
Initial commit
Browse files- .env +0 -0
- Dockerfile +22 -0
- app/__init__.py +0 -0
- app/api/__init__.py +0 -0
- app/api/endpoints/__init__.py +0 -0
- app/api/endpoints/staging.py +35 -0
- app/core/__init__.py +0 -0
- app/core/config.py +14 -0
- app/main.py +14 -0
- app/models/__init__.py +0 -0
- app/models/clip_model.py +60 -0
- app/schemas/__init__.py +0 -0
- app/schemas/staging.py +28 -0
- requirements.txt +9 -0
.env
ADDED
|
File without changes
|
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN pip install huggingface-hub
|
| 6 |
+
|
| 7 |
+
RUN huggingface-hub download \
|
| 8 |
+
apple/MobileCLIP2-S0 \
|
| 9 |
+
--local-dir /app/model \
|
| 10 |
+
--local-dir-use-symlinks False \
|
| 11 |
+
--include "*.pt"
|
| 12 |
+
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
|
| 15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
COPY ./app /app/app
|
| 18 |
+
|
| 19 |
+
EXPOSE 8000
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
app/__init__.py
ADDED
|
File without changes
|
app/api/__init__.py
ADDED
|
File without changes
|
app/api/endpoints/__init__.py
ADDED
|
File without changes
|
app/api/endpoints/staging.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, File, UploadFile, Depends, HTTPException
|
| 2 |
+
from PIL import Image
|
| 3 |
+
import io
|
| 4 |
+
from app.models.clip_model import staging_ranker
|
| 5 |
+
from app.schemas.staging import StagingRequest, StagingResponse
|
| 6 |
+
|
| 7 |
+
router = APIRouter()
|
| 8 |
+
|
| 9 |
+
@router.post("/rank_image", response_model=StagingResponse)
|
| 10 |
+
async def rank_image_for_staging(
|
| 11 |
+
prompts: StagingRequest = Depends(),
|
| 12 |
+
file: UploadFile = File(...)
|
| 13 |
+
):
|
| 14 |
+
"""
|
| 15 |
+
Accepts an image upload and optional JSON prompts to compute a stageability score.
|
| 16 |
+
|
| 17 |
+
- **file**: The image file to be analyzed.
|
| 18 |
+
- **prompts**: A JSON object with `prompt_good`, `prompt_bad`, and optional `prompt_aesthetic`.
|
| 19 |
+
"""
|
| 20 |
+
if not file.content_type.startswith("image/"):
|
| 21 |
+
raise HTTPException(status_code=400, detail="File provided is not an image.")
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
contents = await file.read()
|
| 25 |
+
image = Image.open(io.BytesIO(contents))
|
| 26 |
+
except Exception:
|
| 27 |
+
raise HTTPException(status_code=500, detail="Could not process the uploaded image.")
|
| 28 |
+
|
| 29 |
+
score = staging_ranker.compute_score(image, prompts)
|
| 30 |
+
|
| 31 |
+
return StagingResponse(
|
| 32 |
+
filename=file.filename,
|
| 33 |
+
stageability_score=score,
|
| 34 |
+
details="Score calculated based on the provided prompts."
|
| 35 |
+
)
|
app/core/__init__.py
ADDED
|
File without changes
|
app/core/config.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseSettings
|
| 2 |
+
|
| 3 |
+
class Settings(BaseSettings):
|
| 4 |
+
"""
|
| 5 |
+
Configuration settings for the application.
|
| 6 |
+
The model path is defined here, pointing to where the Dockerfile will download it.
|
| 7 |
+
"""
|
| 8 |
+
MODEL_PATH: str = "/app/model/mobileclip2_s0.pt"
|
| 9 |
+
MODEL_NAME: str = "MobileCLIP2-S0"
|
| 10 |
+
|
| 11 |
+
class Config:
|
| 12 |
+
env_file = ".env"
|
| 13 |
+
|
| 14 |
+
settings = Settings()
|
app/main.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from app.api.endpoints import staging
|
| 3 |
+
|
| 4 |
+
app = FastAPI(
|
| 5 |
+
title="Virtual Staging Ranker API",
|
| 6 |
+
description="An API to rank images based on their suitability for virtual staging using MobileCLIP2.",
|
| 7 |
+
version="1.0.0"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
app.include_router(staging.router, prefix="/api/v1", tags=["Staging"])
|
| 11 |
+
|
| 12 |
+
@app.get("/")
|
| 13 |
+
def read_root():
|
| 14 |
+
return {"message": "Welcome to the Staging Ranker API. Visit /docs for more info."}
|
app/models/__init__.py
ADDED
|
File without changes
|
app/models/clip_model.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import open_clip
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from mobileclip.modules.common.mobileone import reparameterize_model
|
| 5 |
+
from app.core.config import settings
|
| 6 |
+
from app.schemas.staging import StagingRequest
|
| 7 |
+
|
| 8 |
+
class StagingRanker:
|
| 9 |
+
_instance = None
|
| 10 |
+
|
| 11 |
+
def __new__(cls):
|
| 12 |
+
if cls._instance is None:
|
| 13 |
+
cls._instance = super(StagingRanker, cls).__new__(cls)
|
| 14 |
+
cls._instance.load_model()
|
| 15 |
+
return cls._instance
|
| 16 |
+
|
| 17 |
+
def load_model(self):
|
| 18 |
+
"""
|
| 19 |
+
Loads the MobileCLIP2 model and tokenizer into memory.
|
| 20 |
+
This is a time-consuming operation and should only be done once.
|
| 21 |
+
"""
|
| 22 |
+
print("Loading MobileCLIP2 model...")
|
| 23 |
+
self.model, _, self.preprocess = open_clip.create_model_and_transforms(
|
| 24 |
+
settings.MODEL_NAME,
|
| 25 |
+
pretrained=settings.MODEL_PATH
|
| 26 |
+
)
|
| 27 |
+
self.tokenizer = open_clip.get_tokenizer(settings.MODEL_NAME)
|
| 28 |
+
self.model.eval()
|
| 29 |
+
self.model = reparameterize_model(self.model)
|
| 30 |
+
print("Model loaded successfully.")
|
| 31 |
+
|
| 32 |
+
def compute_score(self, image: Image.Image, prompts: StagingRequest) -> float:
|
| 33 |
+
"""
|
| 34 |
+
Computes the differential stageability score for a given image and prompts.
|
| 35 |
+
"""
|
| 36 |
+
image_tensor = self.preprocess(image.convert("RGB")).unsqueeze(0)
|
| 37 |
+
|
| 38 |
+
text_prompts = [prompts.prompt_good, prompts.prompt_bad]
|
| 39 |
+
if prompts.prompt_aesthetic:
|
| 40 |
+
text_prompts.append(prompts.prompt_aesthetic)
|
| 41 |
+
|
| 42 |
+
text_tokens = self.tokenizer(text_prompts)
|
| 43 |
+
|
| 44 |
+
with torch.no_grad():
|
| 45 |
+
image_features = self.model.encode_image(image_tensor)
|
| 46 |
+
text_features = self.model.encode_text(text_tokens)
|
| 47 |
+
|
| 48 |
+
image_features /= image_features.norm(dim=-1, keepdim=True)
|
| 49 |
+
text_features /= text_features.norm(dim=-1, keepdim=True)
|
| 50 |
+
|
| 51 |
+
sims = (image_features @ text_features.T)[0]
|
| 52 |
+
|
| 53 |
+
score = (sims[0] - sims[1]).item()
|
| 54 |
+
|
| 55 |
+
if prompts.prompt_aesthetic:
|
| 56 |
+
score += sims[2].item()
|
| 57 |
+
|
| 58 |
+
return score
|
| 59 |
+
|
| 60 |
+
staging_ranker = StagingRanker()
|
app/schemas/__init__.py
ADDED
|
File without changes
|
app/schemas/staging.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
class StagingRequest(BaseModel):
|
| 5 |
+
"""
|
| 6 |
+
Pydantic model for user-provided prompts.
|
| 7 |
+
Users can override the default prompts to tune the scoring logic.
|
| 8 |
+
"""
|
| 9 |
+
prompt_good: str = Field(
|
| 10 |
+
"an empty room ideal for virtual staging: large visible floor space, clear walls and corners, windows visible and not blocked, no doorway in the middle, evenly lit with natural light, aesthetically pleasing",
|
| 11 |
+
description="A descriptive prompt for what makes a room suitable for staging."
|
| 12 |
+
)
|
| 13 |
+
prompt_bad: str = Field(
|
| 14 |
+
"a room that is hard to stage: narrow, cluttered, windows blocked, poor lighting, doorway or obstacles in the center, little open space",
|
| 15 |
+
description="A descriptive prompt for what makes a room unsuitable for staging."
|
| 16 |
+
)
|
| 17 |
+
prompt_aesthetic: Optional[str] = Field(
|
| 18 |
+
None,
|
| 19 |
+
description="An optional plus-prompt for aesthetic qualities like 'modern fireplace' or 'hardwood floors'."
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
class StagingResponse(BaseModel):
|
| 23 |
+
"""
|
| 24 |
+
Pydantic model for the API response.
|
| 25 |
+
"""
|
| 26 |
+
filename: str
|
| 27 |
+
stageability_score: float = Field(..., description="The calculated differential score (good - bad).")
|
| 28 |
+
details: str
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
pydantic
|
| 4 |
+
python-multipart
|
| 5 |
+
torch
|
| 6 |
+
Pillow
|
| 7 |
+
|
| 8 |
+
git+https://github.com/mlfoundations/open_clip.git
|
| 9 |
+
git+https://github.com/apple/ml-mobileclip
|