Nightfury16 commited on
Commit
9be6c71
·
0 Parent(s):

Initial commit

Browse files
.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