akafesu commited on
Commit
46c64d5
·
0 Parent(s):

Auto deploy Embeddings API

Browse files
.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
3
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
4
+ *.h5 filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official lightweight Python image
2
+ FROM python:3.10-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ build-essential \
10
+ python3-dev \
11
+ libglib2.0-0 \
12
+ libsm6 \
13
+ libxext6 \
14
+ libxrender-dev \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # Copy requirements and install
18
+ COPY requirements.txt .
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Copy app code
22
+ COPY . .
23
+
24
+ # Expose port
25
+ EXPOSE 8000
26
+
27
+ # Start FastAPI server
28
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Fourwalls Embedding API
3
+ emoji: 🏠
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ ---
__init__.py ADDED
File without changes
__pycache__/__init__.cpython-312.pyc ADDED
Binary file (160 Bytes). View file
 
__pycache__/__init__.cpython-313.pyc ADDED
Binary file (160 Bytes). View file
 
__pycache__/main.cpython-312.pyc ADDED
Binary file (5.79 kB). View file
 
__pycache__/main.cpython-313.pyc ADDED
Binary file (5.79 kB). View file
 
__pycache__/models.cpython-312.pyc ADDED
Binary file (806 Bytes). View file
 
__pycache__/models.cpython-313.pyc ADDED
Binary file (792 Bytes). View file
 
main.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request
2
+ from fastapi.responses import JSONResponse
3
+ import io
4
+ import numpy as np
5
+ from PIL import Image
6
+ import tensorflow as tf
7
+ import uvicorn
8
+ from supabase_client import supabase
9
+ from models import create_models
10
+
11
+ # Constants
12
+ IMG_SIZE = 224
13
+
14
+ # Load models
15
+ base_model = tf.keras.applications.MobileNetV2(
16
+ input_shape=(IMG_SIZE, IMG_SIZE, 3),
17
+ include_top=False,
18
+ weights="imagenet"
19
+ )
20
+ base_model.trainable = False
21
+
22
+ # Create the base and top models
23
+ base_model, top_model = create_models(IMG_SIZE)
24
+
25
+
26
+ app = FastAPI()
27
+
28
+ def preprocess_image(image_bytes):
29
+ """Preprocess the uploaded image for MobileNetV2"""
30
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
31
+ image = image.resize((IMG_SIZE, IMG_SIZE))
32
+ array = np.array(image).astype("float32") / 255.0
33
+ return np.expand_dims(array, axis=0)
34
+
35
+ @app.get("/")
36
+ def read_root():
37
+ return {"status": "ok"}
38
+
39
+ # Helper to download image from Supabase Storage using supabase-py
40
+ def download_image_from_supabase(bucket_id, file_path):
41
+ resp = supabase.storage.from_(bucket_id).download(file_path)
42
+ if not resp:
43
+ raise Exception(f"Failed to download image from {bucket_id}/{file_path}")
44
+ return resp
45
+
46
+ # Helper to insert record into property_images table using supabase-py
47
+ def insert_property_image(property_id, aspect, embedding, confidence, image_url):
48
+ try:
49
+ data = {
50
+ "property_id": property_id,
51
+ "aspect": aspect,
52
+ "embedding": embedding,
53
+ "confidence": confidence,
54
+ "url": image_url
55
+ }
56
+ resp = supabase.table("property_images").insert(data).execute()
57
+ return resp.data
58
+ except Exception as e:
59
+ raise Exception(f"Failed to insert property image: {str(e)}")
60
+
61
+
62
+ @app.post("/on-upload")
63
+ async def embed_image(request: Request):
64
+ """ Endpoint to embed images after upload. Triggered by Supabase Storage webhook.
65
+ """
66
+ try:
67
+ data = await request.json()
68
+ print("Received webhook data:", data)
69
+ record = data.get("record")
70
+ if not record:
71
+ return JSONResponse(status_code=400, content={"error": "Missing record in webhook data"})
72
+
73
+ bucket_id = record.get("bucket_id")
74
+ file_path = record.get("name")
75
+ if not bucket_id or not file_path:
76
+ return JSONResponse(status_code=400, content={"error": "Missing bucket_id or file_path."})
77
+
78
+ # Only process if the image is a property image
79
+ if not bucket_id == "property-images":
80
+ print(f"Skipping non-property image: {file_path}")
81
+ return JSONResponse({"status": "skipped", "reason": "not a property image"})
82
+
83
+ # Download image from Supabase Storage
84
+ image_bytes = download_image_from_supabase(bucket_id, file_path)
85
+ print(f"Downloaded image: {file_path} ({len(image_bytes)} bytes)")
86
+
87
+ # Preprocess and embed
88
+ img_tensor = preprocess_image(image_bytes)
89
+ feature_map = base_model(img_tensor, training=False)
90
+ prediction = top_model(feature_map, training=False)
91
+ pooled_embedding = tf.keras.layers.GlobalAveragePooling2D()(feature_map)
92
+ embedding_vector = pooled_embedding.numpy()[0].tolist()
93
+ aspect = "exterior" if prediction.numpy()[0][0] > 0.5 else "interior"
94
+ confidence = float(prediction.numpy()[0][0])
95
+
96
+ # Extract property_id from file_path (after 'property_image/' and before the next slash)
97
+ property_id = file_path.split("/")[0]
98
+
99
+ # Get public URL for the image
100
+ public_url = supabase.storage.from_(bucket_id).get_public_url(file_path)
101
+
102
+ confidence = 1 - confidence if aspect == "interior" else confidence
103
+
104
+ # Insert into property_images table
105
+ insert_property_image(property_id, aspect, embedding_vector, confidence, public_url)
106
+
107
+ return JSONResponse({"status": "ok"})
108
+ except Exception as e:
109
+ print("Error in /embed:", str(e))
110
+ return JSONResponse(status_code=500, content={"error": str(e)})
111
+
112
+ @app.post('/on-delete')
113
+ async def on_delete(request: Request):
114
+ """ Endpoint to handle image deletions. Triggered by Supabase Storage webhook.
115
+ """
116
+ try:
117
+ data = await request.json()
118
+ print("Received delete webhook data:", data)
119
+ record = data.get("record")
120
+ if not record:
121
+ return JSONResponse(status_code=400, content={"error": "Missing record in webhook data"})
122
+
123
+ bucket_id = record.get("bucket_id")
124
+ file_path = record.get("name")
125
+ if not bucket_id or not file_path:
126
+ return JSONResponse(status_code=400, content={"error": "Missing bucket_id or file_path."})
127
+
128
+ # Only process if the image is a property image
129
+ if not bucket_id == "property-images":
130
+ print(f"Skipping non-property image: {file_path}")
131
+ return JSONResponse({"status": "skipped", "reason": "not a property image"})
132
+
133
+ # Create image url
134
+ public_url = supabase.storage.from_(bucket_id).get_public_url(file_path)
135
+
136
+ # Delete entry with image url from property_images table
137
+ supabase.table("property_images").delete().eq("url", public_url).execute()
138
+
139
+ return JSONResponse({"status": "ok"})
140
+ except Exception as e:
141
+ print("Error in /on-delete:", str(e))
142
+ return JSONResponse(status_code=500, content={"error": str(e)})
143
+
144
+ @app.get("/health")
145
+ def health_check():
146
+ return {"status": "ok"}
147
+
148
+
149
+ if __name__ == "__main__":
150
+ uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
model/top_classifier_head.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0c5102343e6c8023c2f44ce6ddc559d29d50799a627acfa1d636def3ac939c7e
3
+ size 676344
models.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+
3
+
4
+ def create_models(IMG_SIZE):
5
+ """Create and return the base and top models"""
6
+ base_model = tf.keras.applications.MobileNetV2(
7
+ input_shape=(IMG_SIZE, IMG_SIZE, 3),
8
+ include_top=False,
9
+ weights="imagenet"
10
+ )
11
+ base_model.trainable = False
12
+
13
+ top_model = tf.keras.models.load_model("model/top_classifier_head.h5")
14
+
15
+ return base_model, top_model
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ pillow
4
+ tensorflow
5
+ numpy
6
+ supabase
7
+
8
+
9
+
sample-images/exterior-front-and-yard.jpg ADDED

Git LFS Details

  • SHA256: cd37265453f20b39920827dbd8a249c68ca0b3fbd4428a906a823fd0f8777dc5
  • Pointer size: 130 Bytes
  • Size of remote file: 60.4 kB
sample-images/gate.jpg ADDED

Git LFS Details

  • SHA256: 1cc8912949753deb40c013ca148c70ac1d0bb5b1fcda132c747d2bb425d3232f
  • Pointer size: 130 Bytes
  • Size of remote file: 58.2 kB
sample-images/kitchen.jpg ADDED

Git LFS Details

  • SHA256: 2595ccead218df31a1a44bae7cf88bfb7f0f40d51273148fd60c5d4f2b3d434e
  • Pointer size: 130 Bytes
  • Size of remote file: 43.1 kB
sample-images/living-room.jpg ADDED

Git LFS Details

  • SHA256: fd2d67c1a1b6deeb1b5c713a7e8987bfd84420bdabd8d6f81b03f0fc52c5b049
  • Pointer size: 130 Bytes
  • Size of remote file: 25.6 kB
sample-images/yard.jpg ADDED

Git LFS Details

  • SHA256: 71685d1efa10508cf56577e4dc6cc101ce8b924407edad4d5e5315a4566f107c
  • Pointer size: 130 Bytes
  • Size of remote file: 46.6 kB
services.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "EMBEDDINGS": "https://akafesu-fourwalls-embeddings-api.hf.space",
3
+ "RECOMMENDATIONS": "https://akafesu-fourwalls-recommendations-api.hf.space",
4
+ "CHAT": "https://akafesu-fourwalls-chat-api.hf.space",
5
+ "SUPABASE": "https://shhagbawphxfyehkqobv.supabase.co",
6
+ "MIGRATIONS": "https://akafesu-fourwalls-migrations-api.hf.space"
7
+ }
supabase_client.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from supabase import create_client
2
+ import os
3
+
4
+
5
+ SUPABASE_URL = os.getenv('SUPABASE_URL')
6
+ SUPABASE_SERVICE_ROLE_KEY = os.getenv('SUPABASE_SERVICE_ROLE_KEY')
7
+
8
+ supabase = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
tests/__init__.py ADDED
File without changes
tests/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (166 Bytes). View file
 
tests/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (166 Bytes). View file
 
tests/__pycache__/test_main.cpython-312-pytest-7.4.4.pyc ADDED
Binary file (3.95 kB). View file
 
tests/__pycache__/test_main.cpython-313-pytest-8.4.1.pyc ADDED
Binary file (4.19 kB). View file
 
tests/__pycache__/test_models.cpython-312-pytest-7.4.4.pyc ADDED
Binary file (1.16 kB). View file
 
tests/__pycache__/test_models.cpython-313-pytest-8.4.1.pyc ADDED
Binary file (1.21 kB). View file
 
tests/test_main.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from embeddings.main import preprocess_image, download_image_from_supabase, insert_property_image, health_check
2
+ import pytest
3
+
4
+ # Test preprocess_image
5
+
6
+ def test_preprocess_image():
7
+ # Provide dummy image bytes (simulate a small PNG header)
8
+ image_bytes = b'\x89PNG\r\n\x1a\n' + b'0' * 100
9
+ try:
10
+ result = preprocess_image(image_bytes)
11
+ assert result is not None
12
+ except Exception:
13
+ # Acceptable if function raises for invalid image
14
+ assert True
15
+
16
+ # Test download_image_from_supabase (mocked)
17
+ def test_download_image_from_supabase(monkeypatch):
18
+ def mock_download(bucket_id, file_path):
19
+ return b"fake_image_bytes"
20
+ monkeypatch.setattr('embeddings.main.download_image_from_supabase', mock_download)
21
+ result = download_image_from_supabase('bucket', 'file.png')
22
+ assert result == b"fake_image_bytes"
23
+
24
+ # Test insert_property_image (mocked DB)
25
+ def test_insert_property_image():
26
+ # This function likely inserts into DB, so just check it runs
27
+ try:
28
+ insert_property_image(1, 'exterior', [0.1, 0.2], 0.99, 'http://example.com/img.png')
29
+ assert True
30
+ except Exception:
31
+ assert True
32
+
33
+ # Test health_check
34
+ def test_health_check():
35
+ result = health_check()
36
+ assert result is not None
tests/test_models.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from embeddings.models import create_models
2
+
3
+ def test_create_models():
4
+ IMG_SIZE = (224, 224)
5
+ model = create_models(IMG_SIZE)
6
+ assert model is not None