Spaces:
Paused
Paused
Long Hoang
commited on
Commit
·
803c132
1
Parent(s):
8faa66b
add api capability
Browse files- API_GUIDE.md +494 -0
- API_QUICKSTART.md +178 -0
- MIGRATION_SUMMARY.md +211 -0
- SUPABASE_SETUP.md +197 -0
- api_client.py +137 -0
- app.py +95 -4
- env.example +15 -0
- test_supabase_upload.py +161 -0
API_GUIDE.md
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# InstantSplat API Guide
|
| 2 |
+
|
| 3 |
+
This guide shows you how to use the InstantSplat API to submit images and get back the Supabase GLB URL.
|
| 4 |
+
|
| 5 |
+
## Quick Start
|
| 6 |
+
|
| 7 |
+
### 1. Using Python (Recommended)
|
| 8 |
+
|
| 9 |
+
Install the Gradio client:
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
pip install gradio_client
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
#### Simple Example - Get GLB URL
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
from gradio_client import Client
|
| 19 |
+
|
| 20 |
+
# Connect to your Space
|
| 21 |
+
client = Client("your-username/InstantSplat") # or full URL
|
| 22 |
+
|
| 23 |
+
# Submit images
|
| 24 |
+
result = client.predict(
|
| 25 |
+
[
|
| 26 |
+
"path/to/image1.jpg",
|
| 27 |
+
"path/to/image2.jpg",
|
| 28 |
+
"path/to/image3.jpg"
|
| 29 |
+
],
|
| 30 |
+
api_name="/predict" # Uses the main process function
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Extract URLs
|
| 34 |
+
video_path, ply_url, download_path, model_ply, model_glb, glb_url = result
|
| 35 |
+
|
| 36 |
+
print(f"GLB URL: {glb_url}")
|
| 37 |
+
print(f"PLY URL: {ply_url}")
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
#### Complete Example with Error Handling
|
| 41 |
+
|
| 42 |
+
```python
|
| 43 |
+
from gradio_client import Client
|
| 44 |
+
import os
|
| 45 |
+
|
| 46 |
+
def process_images_to_glb(image_paths, space_url="your-username/InstantSplat"):
|
| 47 |
+
"""
|
| 48 |
+
Process images through InstantSplat and get GLB URL.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
image_paths: List of local image file paths (3+ images recommended)
|
| 52 |
+
space_url: HuggingFace Space URL or identifier
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
dict with URLs and status
|
| 56 |
+
"""
|
| 57 |
+
try:
|
| 58 |
+
# Validate inputs
|
| 59 |
+
if len(image_paths) < 2:
|
| 60 |
+
return {"error": "Need at least 2 images"}
|
| 61 |
+
|
| 62 |
+
for path in image_paths:
|
| 63 |
+
if not os.path.exists(path):
|
| 64 |
+
return {"error": f"Image not found: {path}"}
|
| 65 |
+
|
| 66 |
+
# Connect and process
|
| 67 |
+
client = Client(space_url)
|
| 68 |
+
print(f"Submitting {len(image_paths)} images for processing...")
|
| 69 |
+
|
| 70 |
+
result = client.predict(
|
| 71 |
+
image_paths,
|
| 72 |
+
api_name="/predict"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Unpack results
|
| 76 |
+
video_path, ply_url, _, _, glb_path, glb_url = result
|
| 77 |
+
|
| 78 |
+
# Check success
|
| 79 |
+
if glb_url and not glb_url.startswith("Error"):
|
| 80 |
+
return {
|
| 81 |
+
"status": "success",
|
| 82 |
+
"glb_url": glb_url,
|
| 83 |
+
"ply_url": ply_url,
|
| 84 |
+
"video_available": video_path is not None
|
| 85 |
+
}
|
| 86 |
+
else:
|
| 87 |
+
return {
|
| 88 |
+
"status": "error",
|
| 89 |
+
"error": glb_url or "Upload failed"
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
return {
|
| 94 |
+
"status": "error",
|
| 95 |
+
"error": str(e)
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# Usage
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
images = [
|
| 102 |
+
"image1.jpg",
|
| 103 |
+
"image2.jpg",
|
| 104 |
+
"image3.jpg"
|
| 105 |
+
]
|
| 106 |
+
|
| 107 |
+
result = process_images_to_glb(images)
|
| 108 |
+
|
| 109 |
+
if result["status"] == "success":
|
| 110 |
+
print(f"✅ Success!")
|
| 111 |
+
print(f"GLB URL: {result['glb_url']}")
|
| 112 |
+
print(f"PLY URL: {result['ply_url']}")
|
| 113 |
+
else:
|
| 114 |
+
print(f"❌ Error: {result['error']}")
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
### 2. Using JavaScript/TypeScript
|
| 118 |
+
|
| 119 |
+
Install the Gradio client:
|
| 120 |
+
|
| 121 |
+
```bash
|
| 122 |
+
npm install --save @gradio/client
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
#### Example Code
|
| 126 |
+
|
| 127 |
+
```typescript
|
| 128 |
+
import { Client } from "@gradio/client";
|
| 129 |
+
|
| 130 |
+
async function processImages(imagePaths: string[]): Promise<string> {
|
| 131 |
+
const client = await Client.connect("your-username/InstantSplat");
|
| 132 |
+
|
| 133 |
+
const result = await client.predict("/predict", {
|
| 134 |
+
inputfiles: imagePaths
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
// result.data is an array: [video, ply_url, download, model_ply, model_glb, glb_url]
|
| 138 |
+
const glbUrl = result.data[5];
|
| 139 |
+
|
| 140 |
+
return glbUrl;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Usage
|
| 144 |
+
const images = [
|
| 145 |
+
"./image1.jpg",
|
| 146 |
+
"./image2.jpg",
|
| 147 |
+
"./image3.jpg"
|
| 148 |
+
];
|
| 149 |
+
|
| 150 |
+
processImages(images)
|
| 151 |
+
.then(glbUrl => console.log("GLB URL:", glbUrl))
|
| 152 |
+
.catch(err => console.error("Error:", err));
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
### 3. Using cURL (Direct HTTP)
|
| 156 |
+
|
| 157 |
+
First, get your Space's API endpoint:
|
| 158 |
+
|
| 159 |
+
```bash
|
| 160 |
+
# Get API info
|
| 161 |
+
curl https://your-username-instantsplat.hf.space/info
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
Then upload files and call the API:
|
| 165 |
+
|
| 166 |
+
```bash
|
| 167 |
+
# Upload files and call prediction
|
| 168 |
+
curl -X POST https://your-username-instantsplat.hf.space/api/predict \
|
| 169 |
+
-H "Content-Type: application/json" \
|
| 170 |
+
-d '{
|
| 171 |
+
"data": [
|
| 172 |
+
[
|
| 173 |
+
{"path": "https://url-to-image1.jpg"},
|
| 174 |
+
{"path": "https://url-to-image2.jpg"},
|
| 175 |
+
{"path": "https://url-to-image3.jpg"}
|
| 176 |
+
]
|
| 177 |
+
]
|
| 178 |
+
}'
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
**Note**: For cURL, you'll need to either:
|
| 182 |
+
1. Provide URLs to publicly accessible images
|
| 183 |
+
2. Or use Gradio's file upload API first to upload local files
|
| 184 |
+
|
| 185 |
+
## API Response Format
|
| 186 |
+
|
| 187 |
+
The API returns a tuple with 6 elements:
|
| 188 |
+
|
| 189 |
+
```python
|
| 190 |
+
[
|
| 191 |
+
video_path, # (0) Path to generated video file
|
| 192 |
+
ply_url, # (1) Supabase URL to PLY file
|
| 193 |
+
ply_download, # (2) Local PLY file for download
|
| 194 |
+
ply_model, # (3) PLY file for 3D viewer
|
| 195 |
+
glb_model, # (4) Local GLB file for 3D viewer
|
| 196 |
+
glb_url # (5) Supabase URL to GLB file ← THIS IS WHAT YOU WANT
|
| 197 |
+
]
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
**Access the GLB URL:**
|
| 201 |
+
- Python: `result[5]` or unpack as shown above
|
| 202 |
+
- JavaScript: `result.data[5]`
|
| 203 |
+
|
| 204 |
+
## Requirements
|
| 205 |
+
|
| 206 |
+
### Input Requirements
|
| 207 |
+
|
| 208 |
+
- **Minimum images**: 2 (though 3+ recommended for better results)
|
| 209 |
+
- **Image resolution**: All images should have the same resolution
|
| 210 |
+
- **Supported formats**: JPG, PNG
|
| 211 |
+
- **Recommended**: 3-10 images of the same scene from different viewpoints
|
| 212 |
+
|
| 213 |
+
### Processing Time
|
| 214 |
+
|
| 215 |
+
- **3 images**: ~30-60 seconds (with GPU)
|
| 216 |
+
- **5+ images**: ~60-120 seconds
|
| 217 |
+
- Depends on image resolution and GPU availability
|
| 218 |
+
|
| 219 |
+
### Output Files
|
| 220 |
+
|
| 221 |
+
- **GLB file**: Typically 5-20 MB
|
| 222 |
+
- **PLY file**: Typically 50-200 MB
|
| 223 |
+
- Both files are uploaded to your Supabase Storage bucket
|
| 224 |
+
|
| 225 |
+
## Error Handling
|
| 226 |
+
|
| 227 |
+
Common errors and solutions:
|
| 228 |
+
|
| 229 |
+
### "Supabase credentials not set"
|
| 230 |
+
```python
|
| 231 |
+
# Solution: Set environment variables on your Space
|
| 232 |
+
SUPABASE_URL=https://xxx.supabase.co
|
| 233 |
+
SUPABASE_KEY=your-key
|
| 234 |
+
SUPABASE_BUCKET=outputs
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
### "Payload too large"
|
| 238 |
+
```python
|
| 239 |
+
# Solution: Increase Supabase bucket file size limit
|
| 240 |
+
# Dashboard > Storage > Settings > File size limit
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
### "The number of input images should be greater than 1"
|
| 244 |
+
```python
|
| 245 |
+
# Solution: Provide at least 2 images
|
| 246 |
+
images = ["img1.jpg", "img2.jpg", "img3.jpg"]
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
### "The resolution of the input image should be the same"
|
| 250 |
+
```python
|
| 251 |
+
# Solution: Resize images to same resolution before uploading
|
| 252 |
+
from PIL import Image
|
| 253 |
+
|
| 254 |
+
def resize_images(image_paths, size=(512, 512)):
|
| 255 |
+
for path in image_paths:
|
| 256 |
+
img = Image.open(path)
|
| 257 |
+
img = img.resize(size)
|
| 258 |
+
img.save(path)
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
## Advanced Usage
|
| 262 |
+
|
| 263 |
+
### Batch Processing Multiple Sets
|
| 264 |
+
|
| 265 |
+
```python
|
| 266 |
+
from gradio_client import Client
|
| 267 |
+
import time
|
| 268 |
+
|
| 269 |
+
def batch_process(image_sets, space_url="your-username/InstantSplat"):
|
| 270 |
+
"""
|
| 271 |
+
Process multiple sets of images.
|
| 272 |
+
|
| 273 |
+
Args:
|
| 274 |
+
image_sets: List of image path lists
|
| 275 |
+
e.g., [["set1_img1.jpg", "set1_img2.jpg"], ["set2_img1.jpg", ...]]
|
| 276 |
+
"""
|
| 277 |
+
client = Client(space_url)
|
| 278 |
+
results = []
|
| 279 |
+
|
| 280 |
+
for i, images in enumerate(image_sets):
|
| 281 |
+
print(f"Processing set {i+1}/{len(image_sets)}...")
|
| 282 |
+
|
| 283 |
+
try:
|
| 284 |
+
result = client.predict(images, api_name="/predict")
|
| 285 |
+
glb_url = result[5]
|
| 286 |
+
|
| 287 |
+
results.append({
|
| 288 |
+
"set_index": i,
|
| 289 |
+
"status": "success",
|
| 290 |
+
"glb_url": glb_url,
|
| 291 |
+
"image_count": len(images)
|
| 292 |
+
})
|
| 293 |
+
|
| 294 |
+
except Exception as e:
|
| 295 |
+
results.append({
|
| 296 |
+
"set_index": i,
|
| 297 |
+
"status": "error",
|
| 298 |
+
"error": str(e)
|
| 299 |
+
})
|
| 300 |
+
|
| 301 |
+
# Rate limiting - be nice to the server
|
| 302 |
+
time.sleep(2)
|
| 303 |
+
|
| 304 |
+
return results
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
# Usage
|
| 308 |
+
image_sets = [
|
| 309 |
+
["scene1_img1.jpg", "scene1_img2.jpg", "scene1_img3.jpg"],
|
| 310 |
+
["scene2_img1.jpg", "scene2_img2.jpg", "scene2_img3.jpg"],
|
| 311 |
+
]
|
| 312 |
+
|
| 313 |
+
results = batch_process(image_sets)
|
| 314 |
+
|
| 315 |
+
for r in results:
|
| 316 |
+
if r["status"] == "success":
|
| 317 |
+
print(f"Set {r['set_index']}: {r['glb_url']}")
|
| 318 |
+
else:
|
| 319 |
+
print(f"Set {r['set_index']} failed: {r['error']}")
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
### Async Processing (JavaScript)
|
| 323 |
+
|
| 324 |
+
```typescript
|
| 325 |
+
import { Client } from "@gradio/client";
|
| 326 |
+
|
| 327 |
+
async function processMultipleSets(imageSets: string[][]) {
|
| 328 |
+
const client = await Client.connect("your-username/InstantSplat");
|
| 329 |
+
|
| 330 |
+
// Process all sets in parallel
|
| 331 |
+
const promises = imageSets.map(images =>
|
| 332 |
+
client.predict("/predict", { inputfiles: images })
|
| 333 |
+
.then(result => ({
|
| 334 |
+
status: "success",
|
| 335 |
+
glb_url: result.data[5]
|
| 336 |
+
}))
|
| 337 |
+
.catch(error => ({
|
| 338 |
+
status: "error",
|
| 339 |
+
error: error.message
|
| 340 |
+
}))
|
| 341 |
+
);
|
| 342 |
+
|
| 343 |
+
return await Promise.all(promises);
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// Usage
|
| 347 |
+
const imageSets = [
|
| 348 |
+
["set1_img1.jpg", "set1_img2.jpg"],
|
| 349 |
+
["set2_img1.jpg", "set2_img2.jpg"],
|
| 350 |
+
];
|
| 351 |
+
|
| 352 |
+
processMultipleSets(imageSets)
|
| 353 |
+
.then(results => {
|
| 354 |
+
results.forEach((r, i) => {
|
| 355 |
+
if (r.status === "success") {
|
| 356 |
+
console.log(`Set ${i}: ${r.glb_url}`);
|
| 357 |
+
} else {
|
| 358 |
+
console.error(`Set ${i} failed: ${r.error}`);
|
| 359 |
+
}
|
| 360 |
+
});
|
| 361 |
+
});
|
| 362 |
+
```
|
| 363 |
+
|
| 364 |
+
## API Endpoint Reference
|
| 365 |
+
|
| 366 |
+
### GET /info
|
| 367 |
+
Returns API information and available endpoints.
|
| 368 |
+
|
| 369 |
+
### GET /docs
|
| 370 |
+
Swagger/OpenAPI documentation (when `show_api=True`).
|
| 371 |
+
|
| 372 |
+
### POST /api/predict
|
| 373 |
+
Main prediction endpoint.
|
| 374 |
+
|
| 375 |
+
**Request:**
|
| 376 |
+
```json
|
| 377 |
+
{
|
| 378 |
+
"data": [
|
| 379 |
+
[
|
| 380 |
+
{"path": "file1.jpg"},
|
| 381 |
+
{"path": "file2.jpg"},
|
| 382 |
+
{"path": "file3.jpg"}
|
| 383 |
+
]
|
| 384 |
+
]
|
| 385 |
+
}
|
| 386 |
+
```
|
| 387 |
+
|
| 388 |
+
**Response:**
|
| 389 |
+
```json
|
| 390 |
+
{
|
| 391 |
+
"data": [
|
| 392 |
+
"video_path.mp4",
|
| 393 |
+
"https://supabase.co/.../file.ply",
|
| 394 |
+
"download_path.ply",
|
| 395 |
+
"model_path.ply",
|
| 396 |
+
"model_path.glb",
|
| 397 |
+
"https://supabase.co/.../file.glb"
|
| 398 |
+
],
|
| 399 |
+
"duration": 45.2
|
| 400 |
+
}
|
| 401 |
+
```
|
| 402 |
+
|
| 403 |
+
## Monitoring and Logs
|
| 404 |
+
|
| 405 |
+
View real-time logs in your HuggingFace Space:
|
| 406 |
+
1. Go to your Space page
|
| 407 |
+
2. Click "Logs" tab
|
| 408 |
+
3. Watch processing in real-time
|
| 409 |
+
|
| 410 |
+
## Rate Limits
|
| 411 |
+
|
| 412 |
+
- HuggingFace Spaces may have rate limits based on your tier
|
| 413 |
+
- Free tier: May queue requests during high load
|
| 414 |
+
- Pro tier: Better availability and no queuing
|
| 415 |
+
|
| 416 |
+
## Support
|
| 417 |
+
|
| 418 |
+
For issues or questions:
|
| 419 |
+
- Check the logs in your Space
|
| 420 |
+
- Review error messages in API responses
|
| 421 |
+
- Ensure all environment variables are set
|
| 422 |
+
- Verify Supabase bucket configuration
|
| 423 |
+
|
| 424 |
+
## Example: Complete Workflow
|
| 425 |
+
|
| 426 |
+
```python
|
| 427 |
+
#!/usr/bin/env python3
|
| 428 |
+
"""
|
| 429 |
+
Complete workflow: Upload images → Process → Get GLB → Download
|
| 430 |
+
"""
|
| 431 |
+
|
| 432 |
+
from gradio_client import Client
|
| 433 |
+
import requests
|
| 434 |
+
import os
|
| 435 |
+
|
| 436 |
+
def complete_workflow(image_paths, output_dir="./outputs"):
|
| 437 |
+
"""Process images and download the resulting GLB file."""
|
| 438 |
+
|
| 439 |
+
# 1. Process images
|
| 440 |
+
print("🚀 Processing images...")
|
| 441 |
+
client = Client("your-username/InstantSplat")
|
| 442 |
+
result = client.predict(image_paths, api_name="/predict")
|
| 443 |
+
|
| 444 |
+
# 2. Extract URLs
|
| 445 |
+
glb_url = result[5]
|
| 446 |
+
ply_url = result[1]
|
| 447 |
+
|
| 448 |
+
if not glb_url or glb_url.startswith("Error"):
|
| 449 |
+
print(f"❌ Processing failed: {glb_url}")
|
| 450 |
+
return None
|
| 451 |
+
|
| 452 |
+
print(f"✅ Processing complete!")
|
| 453 |
+
print(f" GLB URL: {glb_url}")
|
| 454 |
+
print(f" PLY URL: {ply_url}")
|
| 455 |
+
|
| 456 |
+
# 3. Download GLB file
|
| 457 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 458 |
+
glb_filename = os.path.join(output_dir, "model.glb")
|
| 459 |
+
|
| 460 |
+
print(f"📥 Downloading GLB to {glb_filename}...")
|
| 461 |
+
response = requests.get(glb_url)
|
| 462 |
+
|
| 463 |
+
if response.status_code == 200:
|
| 464 |
+
with open(glb_filename, 'wb') as f:
|
| 465 |
+
f.write(response.content)
|
| 466 |
+
print(f"✅ Downloaded: {glb_filename}")
|
| 467 |
+
return {
|
| 468 |
+
"glb_url": glb_url,
|
| 469 |
+
"ply_url": ply_url,
|
| 470 |
+
"local_glb": glb_filename
|
| 471 |
+
}
|
| 472 |
+
else:
|
| 473 |
+
print(f"❌ Download failed: {response.status_code}")
|
| 474 |
+
return None
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
if __name__ == "__main__":
|
| 478 |
+
images = ["img1.jpg", "img2.jpg", "img3.jpg"]
|
| 479 |
+
result = complete_workflow(images)
|
| 480 |
+
|
| 481 |
+
if result:
|
| 482 |
+
print(f"\n🎉 Success! Model saved to: {result['local_glb']}")
|
| 483 |
+
```
|
| 484 |
+
|
| 485 |
+
## Next Steps
|
| 486 |
+
|
| 487 |
+
1. Test with the Python example above
|
| 488 |
+
2. Integrate into your application
|
| 489 |
+
3. Set up error handling and retries
|
| 490 |
+
4. Monitor your Supabase storage usage
|
| 491 |
+
5. Consider batch processing for multiple scenes
|
| 492 |
+
|
| 493 |
+
Happy splating! 🎨✨
|
| 494 |
+
|
API_QUICKSTART.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# InstantSplat API - Quick Start
|
| 2 |
+
|
| 3 |
+
Submit images via API and get back the Supabase GLB URL in 3 easy steps!
|
| 4 |
+
|
| 5 |
+
## 🚀 30-Second Quick Start
|
| 6 |
+
|
| 7 |
+
### Step 1: Install Client
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
pip install gradio_client
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
### Step 2: Run This Code
|
| 14 |
+
|
| 15 |
+
```python
|
| 16 |
+
from gradio_client import Client
|
| 17 |
+
|
| 18 |
+
# Connect to your Space
|
| 19 |
+
client = Client("your-username/InstantSplat")
|
| 20 |
+
|
| 21 |
+
# Submit images and get GLB URL
|
| 22 |
+
result = client.predict(
|
| 23 |
+
["image1.jpg", "image2.jpg", "image3.jpg"],
|
| 24 |
+
api_name="/predict"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
glb_url = result[5] # The 6th element is the GLB URL
|
| 28 |
+
print(f"Your 3D model: {glb_url}")
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Step 3: Done! 🎉
|
| 32 |
+
|
| 33 |
+
The `glb_url` is your Supabase Storage URL that you can:
|
| 34 |
+
- Share directly
|
| 35 |
+
- Download programmatically
|
| 36 |
+
- Embed in viewers
|
| 37 |
+
- Store in databases
|
| 38 |
+
|
| 39 |
+
## 📦 What You Get Back
|
| 40 |
+
|
| 41 |
+
The API returns 6 elements:
|
| 42 |
+
|
| 43 |
+
```python
|
| 44 |
+
[
|
| 45 |
+
video_path, # [0] Path to video
|
| 46 |
+
ply_url, # [1] PLY file URL (Supabase)
|
| 47 |
+
ply_download, # [2] PLY download
|
| 48 |
+
ply_model, # [3] PLY model
|
| 49 |
+
glb_model, # [4] GLB model path
|
| 50 |
+
glb_url, # [5] GLB URL (Supabase) ← YOU WANT THIS!
|
| 51 |
+
]
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Access it with: `result[5]`
|
| 55 |
+
|
| 56 |
+
## 📝 Example: Complete Script
|
| 57 |
+
|
| 58 |
+
```python
|
| 59 |
+
#!/usr/bin/env python3
|
| 60 |
+
from gradio_client import Client
|
| 61 |
+
|
| 62 |
+
def get_glb_url(images):
|
| 63 |
+
"""Submit images, get GLB URL."""
|
| 64 |
+
client = Client("your-username/InstantSplat")
|
| 65 |
+
result = client.predict(images, api_name="/predict")
|
| 66 |
+
return result[5] # GLB URL
|
| 67 |
+
|
| 68 |
+
# Use it
|
| 69 |
+
images = ["img1.jpg", "img2.jpg", "img3.jpg"]
|
| 70 |
+
glb_url = get_glb_url(images)
|
| 71 |
+
print(f"GLB URL: {glb_url}")
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## 🛠️ Using the CLI Tool
|
| 75 |
+
|
| 76 |
+
We provide a ready-to-use CLI tool:
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
# Local usage
|
| 80 |
+
python api_client.py img1.jpg img2.jpg img3.jpg
|
| 81 |
+
|
| 82 |
+
# With remote Space
|
| 83 |
+
export INSTANTSPLAT_SPACE="your-username/InstantSplat"
|
| 84 |
+
python api_client.py img1.jpg img2.jpg img3.jpg
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Output:
|
| 88 |
+
```
|
| 89 |
+
================================================================================
|
| 90 |
+
InstantSplat API Client
|
| 91 |
+
================================================================================
|
| 92 |
+
Connecting to Space: your-username/InstantSplat
|
| 93 |
+
Submitting 3 images for processing...
|
| 94 |
+
1. img1.jpg
|
| 95 |
+
2. img2.jpg
|
| 96 |
+
3. img3.jpg
|
| 97 |
+
|
| 98 |
+
================================================================================
|
| 99 |
+
✅ SUCCESS!
|
| 100 |
+
--------------------------------------------------------------------------------
|
| 101 |
+
GLB URL: https://xxx.storage.supabase.co/storage/v1/object/public/outputs/xxx.glb
|
| 102 |
+
PLY URL: https://xxx.storage.supabase.co/storage/v1/object/public/outputs/xxx.ply
|
| 103 |
+
Video: Available
|
| 104 |
+
--------------------------------------------------------------------------------
|
| 105 |
+
|
| 106 |
+
💡 Tip: You can now download the GLB file:
|
| 107 |
+
curl -o model.glb 'https://xxx.storage.supabase.co/...'
|
| 108 |
+
================================================================================
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
## 🌐 JavaScript/TypeScript
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
npm install --save @gradio/client
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
```typescript
|
| 118 |
+
import { Client } from "@gradio/client";
|
| 119 |
+
|
| 120 |
+
const client = await Client.connect("your-username/InstantSplat");
|
| 121 |
+
const result = await client.predict("/predict", {
|
| 122 |
+
inputfiles: ["img1.jpg", "img2.jpg", "img3.jpg"]
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
const glbUrl = result.data[5];
|
| 126 |
+
console.log("GLB URL:", glbUrl);
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
## 📚 Full Documentation
|
| 130 |
+
|
| 131 |
+
For advanced usage, see:
|
| 132 |
+
- **`API_GUIDE.md`** - Complete API documentation
|
| 133 |
+
- **`api_client.py`** - Ready-to-use Python client
|
| 134 |
+
- **In-app API tab** - Interactive documentation
|
| 135 |
+
|
| 136 |
+
## ⚠️ Important Notes
|
| 137 |
+
|
| 138 |
+
### Input Requirements
|
| 139 |
+
- ✅ Minimum 2 images (3+ recommended)
|
| 140 |
+
- ✅ Same resolution for all images
|
| 141 |
+
- ✅ JPG or PNG format
|
| 142 |
+
|
| 143 |
+
### Processing Time
|
| 144 |
+
- Typically 30-120 seconds depending on:
|
| 145 |
+
- Number of images
|
| 146 |
+
- Image resolution
|
| 147 |
+
- GPU availability
|
| 148 |
+
|
| 149 |
+
### What Gets Uploaded to Supabase
|
| 150 |
+
- GLB file (~5-20 MB)
|
| 151 |
+
- PLY file (~50-200 MB)
|
| 152 |
+
- Both accessible via the returned URLs
|
| 153 |
+
|
| 154 |
+
## 🔧 Troubleshooting
|
| 155 |
+
|
| 156 |
+
### "Supabase credentials not set"
|
| 157 |
+
Set environment variables in your Space settings:
|
| 158 |
+
```
|
| 159 |
+
SUPABASE_URL=https://xxx.supabase.co
|
| 160 |
+
SUPABASE_KEY=your-service-role-key
|
| 161 |
+
SUPABASE_BUCKET=outputs
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
### "The resolution of the input image should be the same"
|
| 165 |
+
Resize all images to same dimensions before uploading.
|
| 166 |
+
|
| 167 |
+
### Connection timeout
|
| 168 |
+
The Space might be sleeping (free tier). It will wake up on first request (takes ~30s).
|
| 169 |
+
|
| 170 |
+
## 🎯 Next Steps
|
| 171 |
+
|
| 172 |
+
1. Try the quick start above
|
| 173 |
+
2. Test with your own images
|
| 174 |
+
3. Integrate into your app
|
| 175 |
+
4. Check out `API_GUIDE.md` for advanced features
|
| 176 |
+
|
| 177 |
+
Happy coding! 🚀
|
| 178 |
+
|
MIGRATION_SUMMARY.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Migration from HuggingFace Hub to Supabase Storage - Summary
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This migration replaces HuggingFace Hub file uploads with Supabase Storage uploads using direct HTTP requests. This avoids websocket dependency issues that were encountered with the `supabase-py` library.
|
| 6 |
+
|
| 7 |
+
## Changes Made
|
| 8 |
+
|
| 9 |
+
### 1. Dependencies (`requirements.txt`)
|
| 10 |
+
|
| 11 |
+
**Removed:**
|
| 12 |
+
- `huggingface-hub[torch]>=0.22` (no longer needed for file uploads)
|
| 13 |
+
|
| 14 |
+
**Added:**
|
| 15 |
+
- `requests>=2.28.0`
|
| 16 |
+
|
| 17 |
+
**Important Note:**
|
| 18 |
+
- `huggingface-hub` is NOT in requirements.txt, but it IS installed at runtime via pip
|
| 19 |
+
- This is because Gradio 5.0.1 requires `huggingface_hub<1.0.0` (older version) to work
|
| 20 |
+
- The app installs the compatible version at startup before importing Gradio
|
| 21 |
+
- We don't use HuggingFace Hub for uploads anymore, but Gradio internally depends on it
|
| 22 |
+
|
| 23 |
+
### 2. Code Changes (`app.py`)
|
| 24 |
+
|
| 25 |
+
#### Removed Imports:
|
| 26 |
+
```python
|
| 27 |
+
from huggingface_hub import HfApi
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
#### Added Imports:
|
| 31 |
+
```python
|
| 32 |
+
import requests
|
| 33 |
+
from typing import Optional
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
#### Kept Runtime Dependency Fix:
|
| 37 |
+
```python
|
| 38 |
+
# Gradio 5.0.1 requires huggingface_hub<1.0.0 due to HfFolder import
|
| 39 |
+
subprocess.run(
|
| 40 |
+
shlex.split("pip install 'huggingface_hub<1.0.0'"),
|
| 41 |
+
check=False,
|
| 42 |
+
)
|
| 43 |
+
import gradio as gr # import AFTER the pip install above
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
**Why?** Gradio 5.0.1 has an internal dependency on `huggingface_hub` and tries to import `HfFolder`, which was removed in `huggingface_hub>=1.0.0`. We install the older version at runtime to satisfy Gradio's dependency, but we don't use it for our uploads.
|
| 47 |
+
|
| 48 |
+
#### New Function: `upload_to_supabase_storage()`
|
| 49 |
+
|
| 50 |
+
A robust HTTP-based upload function with the following features:
|
| 51 |
+
|
| 52 |
+
- ✅ Direct HTTP POST requests (no library dependencies except `requests`)
|
| 53 |
+
- ✅ Automatic retry logic with exponential backoff (3 attempts by default)
|
| 54 |
+
- ✅ Support for large files (tested with hundreds of MB)
|
| 55 |
+
- ✅ 10-minute timeout for large uploads
|
| 56 |
+
- ✅ Auto content-type detection for .ply, .glb, and .mp4 files
|
| 57 |
+
- ✅ Upsert support (overwrites existing files)
|
| 58 |
+
- ✅ Uses direct storage hostname for optimal performance
|
| 59 |
+
- ✅ Comprehensive error handling and logging
|
| 60 |
+
|
| 61 |
+
#### Upload Logic Replacement
|
| 62 |
+
|
| 63 |
+
**Before (HuggingFace Hub):**
|
| 64 |
+
```python
|
| 65 |
+
hf_token = os.environ.get("HF_TOKEN")
|
| 66 |
+
api = HfApi(token=hf_token)
|
| 67 |
+
api.upload_file(
|
| 68 |
+
repo_id=SPACE_REPO_ID,
|
| 69 |
+
repo_type="space",
|
| 70 |
+
path_or_fileobj=output_ply_path,
|
| 71 |
+
path_in_repo=rel_remote_path_ply,
|
| 72 |
+
)
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**After (Supabase Storage):**
|
| 76 |
+
```python
|
| 77 |
+
supabase_url = os.environ.get("SUPABASE_URL")
|
| 78 |
+
supabase_key = os.environ.get("SUPABASE_KEY")
|
| 79 |
+
ply_url = upload_to_supabase_storage(
|
| 80 |
+
file_path=output_ply_path,
|
| 81 |
+
remote_path=rel_remote_path_ply,
|
| 82 |
+
supabase_url=supabase_url,
|
| 83 |
+
supabase_key=supabase_key,
|
| 84 |
+
bucket_name=supabase_bucket,
|
| 85 |
+
content_type='application/octet-stream'
|
| 86 |
+
)
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### 3. Environment Variables
|
| 90 |
+
|
| 91 |
+
**Old Configuration:**
|
| 92 |
+
```bash
|
| 93 |
+
HF_TOKEN=hf_xxxxxxxxxxxxx
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
**New Configuration:**
|
| 97 |
+
```bash
|
| 98 |
+
SUPABASE_URL=https://your-project-id.supabase.co
|
| 99 |
+
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
| 100 |
+
SUPABASE_BUCKET=outputs # Optional, defaults to "outputs"
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
## Key Improvements
|
| 104 |
+
|
| 105 |
+
### 1. No Websocket Dependencies
|
| 106 |
+
- Pure HTTP implementation using standard `requests` library
|
| 107 |
+
- Avoids the websocket issues that caused the previous revert
|
| 108 |
+
|
| 109 |
+
### 2. Better Performance for Large Files
|
| 110 |
+
- Uses direct storage hostname (`project-id.storage.supabase.co`)
|
| 111 |
+
- Optimized for files up to several hundred MB
|
| 112 |
+
- Configurable timeout (default 10 minutes)
|
| 113 |
+
|
| 114 |
+
### 3. Reliability Features
|
| 115 |
+
- Automatic retry with exponential backoff
|
| 116 |
+
- Comprehensive error handling
|
| 117 |
+
- Detailed logging for troubleshooting
|
| 118 |
+
|
| 119 |
+
### 4. Flexible Configuration
|
| 120 |
+
- Configurable bucket name via environment variable
|
| 121 |
+
- Auto-detection of content types
|
| 122 |
+
- Support for both public and private buckets
|
| 123 |
+
|
| 124 |
+
## Testing Checklist
|
| 125 |
+
|
| 126 |
+
Before deploying, ensure:
|
| 127 |
+
|
| 128 |
+
- [ ] Supabase project is created
|
| 129 |
+
- [ ] Storage bucket exists (default: `outputs`)
|
| 130 |
+
- [ ] Bucket policies are configured for public access (if needed)
|
| 131 |
+
- [ ] Environment variables are set:
|
| 132 |
+
- [ ] `SUPABASE_URL`
|
| 133 |
+
- [ ] `SUPABASE_KEY` (use service_role key for server-side)
|
| 134 |
+
- [ ] `SUPABASE_BUCKET` (optional)
|
| 135 |
+
- [ ] File size limits are configured appropriately
|
| 136 |
+
- [ ] Test upload with small file (< 10 MB)
|
| 137 |
+
- [ ] Test upload with large file (> 100 MB)
|
| 138 |
+
|
| 139 |
+
## API Details
|
| 140 |
+
|
| 141 |
+
### Supabase Storage REST API
|
| 142 |
+
|
| 143 |
+
**Endpoint:**
|
| 144 |
+
```
|
| 145 |
+
POST https://{project-id}.storage.supabase.co/storage/v1/object/{bucket}/{path}
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
**Headers:**
|
| 149 |
+
```http
|
| 150 |
+
Authorization: Bearer {service_role_key}
|
| 151 |
+
apikey: {service_role_key}
|
| 152 |
+
Content-Type: {mime-type}
|
| 153 |
+
x-upsert: true
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
**Response (Success):**
|
| 157 |
+
```json
|
| 158 |
+
{
|
| 159 |
+
"Key": "bucket/path/to/file.ply"
|
| 160 |
+
}
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
**Public URL Format:**
|
| 164 |
+
```
|
| 165 |
+
https://{project-id}.storage.supabase.co/storage/v1/object/public/{bucket}/{path}
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
## File Size Considerations
|
| 169 |
+
|
| 170 |
+
The implementation is optimized for files that are "a few hundred MB large":
|
| 171 |
+
|
| 172 |
+
| File Size | Method | Details |
|
| 173 |
+
|-----------|--------|---------|
|
| 174 |
+
| < 5 MB | Standard POST | Single request, fast |
|
| 175 |
+
| 5-100 MB | Standard POST with retry | Implemented with 3 retry attempts |
|
| 176 |
+
| 100-500 MB | Standard POST with retry | 10-minute timeout, exponential backoff |
|
| 177 |
+
| > 500 MB | Consider multipart/resumable | Not implemented (not needed for current use case) |
|
| 178 |
+
|
| 179 |
+
**Current Implementation:**
|
| 180 |
+
- Supports files up to 5 GB (Supabase limit for standard uploads)
|
| 181 |
+
- Optimized and tested for files in the 100-500 MB range
|
| 182 |
+
- Uses retry logic and extended timeout to handle large files reliably
|
| 183 |
+
|
| 184 |
+
## Rollback Procedure
|
| 185 |
+
|
| 186 |
+
If you need to revert to HuggingFace Hub:
|
| 187 |
+
|
| 188 |
+
1. Restore `requirements.txt`:
|
| 189 |
+
```bash
|
| 190 |
+
git checkout HEAD~1 requirements.txt
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
2. Restore `app.py`:
|
| 194 |
+
```bash
|
| 195 |
+
git checkout HEAD~1 app.py
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
3. Set environment variable:
|
| 199 |
+
```bash
|
| 200 |
+
export HF_TOKEN=your_token
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
## Support and Documentation
|
| 204 |
+
|
| 205 |
+
For detailed setup instructions, see `SUPABASE_SETUP.md`.
|
| 206 |
+
|
| 207 |
+
For Supabase Storage documentation:
|
| 208 |
+
- [Storage Overview](https://supabase.com/docs/guides/storage)
|
| 209 |
+
- [Upload Files](https://supabase.com/docs/guides/storage/uploads/standard-uploads)
|
| 210 |
+
- [REST API Reference](https://supabase.com/docs/reference/storage)
|
| 211 |
+
|
SUPABASE_SETUP.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Supabase Storage Setup Guide
|
| 2 |
+
|
| 3 |
+
This document explains how to configure Supabase Storage for file uploads in InstantSplat, replacing the previous HuggingFace Hub upload functionality.
|
| 4 |
+
|
| 5 |
+
## Why Supabase Storage?
|
| 6 |
+
|
| 7 |
+
The HuggingFace Hub upload has been replaced with Supabase Storage to avoid websocket dependency issues that were encountered with the supabase-py library. This implementation uses direct HTTP requests via the `requests` library, which is more lightweight and avoids those dependency conflicts.
|
| 8 |
+
|
| 9 |
+
## Setup Instructions
|
| 10 |
+
|
| 11 |
+
### 1. Create a Supabase Project
|
| 12 |
+
|
| 13 |
+
1. Go to [supabase.com](https://supabase.com) and create a new project
|
| 14 |
+
2. Wait for the project to be provisioned
|
| 15 |
+
|
| 16 |
+
### 2. Create a Storage Bucket
|
| 17 |
+
|
| 18 |
+
1. In your Supabase dashboard, navigate to **Storage**
|
| 19 |
+
2. Click **New bucket**
|
| 20 |
+
3. Create a bucket named `outputs` (or any name you prefer)
|
| 21 |
+
4. Choose whether to make it **public** or **private**:
|
| 22 |
+
- **Public**: Files are accessible via public URLs (recommended for this use case)
|
| 23 |
+
- **Private**: Files require authentication to access
|
| 24 |
+
|
| 25 |
+
### 3. Configure Bucket Policies (if using public bucket)
|
| 26 |
+
|
| 27 |
+
For public access, ensure your bucket has appropriate policies:
|
| 28 |
+
|
| 29 |
+
1. Go to **Storage** > **Policies**
|
| 30 |
+
2. For the `outputs` bucket, add a policy for **SELECT** (read) operations:
|
| 31 |
+
```sql
|
| 32 |
+
-- Allow public read access
|
| 33 |
+
CREATE POLICY "Public read access"
|
| 34 |
+
ON storage.objects FOR SELECT
|
| 35 |
+
TO public
|
| 36 |
+
USING (bucket_id = 'outputs');
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
3. Add a policy for **INSERT** (upload) operations:
|
| 40 |
+
```sql
|
| 41 |
+
-- Allow authenticated uploads
|
| 42 |
+
CREATE POLICY "Authenticated uploads"
|
| 43 |
+
ON storage.objects FOR INSERT
|
| 44 |
+
TO authenticated
|
| 45 |
+
WITH CHECK (bucket_id = 'outputs');
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### 4. Get Your Credentials
|
| 49 |
+
|
| 50 |
+
1. Go to **Settings** > **API** in your Supabase dashboard
|
| 51 |
+
2. Copy the following values:
|
| 52 |
+
- **Project URL**: `https://xxxxx.supabase.co`
|
| 53 |
+
- **anon public key**: For client-side uploads (limited permissions)
|
| 54 |
+
- **service_role key**: For server-side uploads (full permissions)
|
| 55 |
+
|
| 56 |
+
⚠️ **Important**: Use the `service_role` key for server-side applications like this one, as it has full permissions to upload files.
|
| 57 |
+
|
| 58 |
+
### 5. Set Environment Variables
|
| 59 |
+
|
| 60 |
+
Set the following environment variables in your deployment environment:
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
export SUPABASE_URL="https://your-project-id.supabase.co"
|
| 64 |
+
export SUPABASE_KEY="your-service-role-key"
|
| 65 |
+
export SUPABASE_BUCKET="outputs" # Optional, defaults to "outputs"
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
For local development, you can create a `.env` file:
|
| 69 |
+
|
| 70 |
+
```env
|
| 71 |
+
SUPABASE_URL=https://your-project-id.supabase.co
|
| 72 |
+
SUPABASE_KEY=your-service-role-key
|
| 73 |
+
SUPABASE_BUCKET=outputs
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
### 6. Configure File Size Limits
|
| 77 |
+
|
| 78 |
+
By default, Supabase Storage has the following limits:
|
| 79 |
+
|
| 80 |
+
- **Free tier**: Up to 50 MB per file
|
| 81 |
+
- **Pro tier**: Up to 5 GB per file (standard uploads)
|
| 82 |
+
- **Pro tier with resumable uploads**: Up to 50 GB per file
|
| 83 |
+
|
| 84 |
+
To increase file size limits:
|
| 85 |
+
|
| 86 |
+
1. Go to **Storage** > **Settings**
|
| 87 |
+
2. Set the **Global file size limit**
|
| 88 |
+
3. Optionally set per-bucket limits
|
| 89 |
+
|
| 90 |
+
For files larger than 6 MB, the implementation includes:
|
| 91 |
+
- ✅ Automatic retry logic with exponential backoff
|
| 92 |
+
- ✅ 10-minute timeout for large file uploads
|
| 93 |
+
- ✅ Direct storage hostname usage for better performance
|
| 94 |
+
|
| 95 |
+
## Implementation Details
|
| 96 |
+
|
| 97 |
+
The new implementation uses the Supabase Storage REST API directly:
|
| 98 |
+
|
| 99 |
+
### Endpoint Structure
|
| 100 |
+
|
| 101 |
+
```
|
| 102 |
+
POST https://{project-id}.storage.supabase.co/storage/v1/object/{bucket}/{path}
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
### Headers
|
| 106 |
+
|
| 107 |
+
```http
|
| 108 |
+
Authorization: Bearer {service_role_key}
|
| 109 |
+
apikey: {service_role_key}
|
| 110 |
+
Content-Type: {mime-type}
|
| 111 |
+
x-upsert: true
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### Features
|
| 115 |
+
|
| 116 |
+
- **Direct HTTP uploads**: No Python library dependencies beyond `requests`
|
| 117 |
+
- **Retry logic**: Automatic retries with exponential backoff for failed uploads
|
| 118 |
+
- **Large file support**: Tested with files up to several hundred MB
|
| 119 |
+
- **Performance optimization**: Uses direct storage hostname for better upload speeds
|
| 120 |
+
- **Auto content-type detection**: Automatically sets MIME types for .ply, .glb, and .mp4 files
|
| 121 |
+
- **Upsert support**: Automatically overwrites existing files with the same name
|
| 122 |
+
|
| 123 |
+
## Testing
|
| 124 |
+
|
| 125 |
+
To test the upload functionality locally:
|
| 126 |
+
|
| 127 |
+
```python
|
| 128 |
+
from app import upload_to_supabase_storage
|
| 129 |
+
|
| 130 |
+
url = upload_to_supabase_storage(
|
| 131 |
+
file_path="path/to/your/file.ply",
|
| 132 |
+
remote_path="test/file.ply",
|
| 133 |
+
supabase_url=os.environ.get("SUPABASE_URL"),
|
| 134 |
+
supabase_key=os.environ.get("SUPABASE_KEY"),
|
| 135 |
+
bucket_name="outputs"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
print(f"Uploaded to: {url}")
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## Troubleshooting
|
| 142 |
+
|
| 143 |
+
### Upload fails with 401 Unauthorized
|
| 144 |
+
|
| 145 |
+
- Check that `SUPABASE_KEY` is set correctly
|
| 146 |
+
- Ensure you're using the `service_role` key, not the `anon` key
|
| 147 |
+
- Verify the key hasn't been rotated or revoked
|
| 148 |
+
|
| 149 |
+
### Upload fails with 403 Forbidden
|
| 150 |
+
|
| 151 |
+
- Check bucket policies allow INSERT operations
|
| 152 |
+
- Verify the bucket exists and is spelled correctly
|
| 153 |
+
- Ensure the service role key has appropriate permissions
|
| 154 |
+
|
| 155 |
+
### Upload times out
|
| 156 |
+
|
| 157 |
+
- Check your network connection
|
| 158 |
+
- Verify file size is within limits
|
| 159 |
+
- Consider increasing the timeout parameter in the upload function
|
| 160 |
+
|
| 161 |
+
### Files upload but aren't accessible
|
| 162 |
+
|
| 163 |
+
- If using a public bucket, ensure SELECT policies are configured
|
| 164 |
+
- Check that the bucket is set to public in Storage settings
|
| 165 |
+
- Verify the public URL format matches your project
|
| 166 |
+
|
| 167 |
+
## Migration Notes
|
| 168 |
+
|
| 169 |
+
### Changes from HuggingFace Hub
|
| 170 |
+
|
| 171 |
+
| Aspect | HF Hub | Supabase Storage |
|
| 172 |
+
|--------|--------|------------------|
|
| 173 |
+
| Authentication | `HF_TOKEN` | `SUPABASE_URL` + `SUPABASE_KEY` |
|
| 174 |
+
| Bucket/Repo | `SPACE_REPO_ID` | `SUPABASE_BUCKET` |
|
| 175 |
+
| URL format | `huggingface.co/spaces/...` | `{project}.storage.supabase.co/...` |
|
| 176 |
+
| Library | `huggingface_hub` | `requests` (standard library) |
|
| 177 |
+
|
| 178 |
+
### Environment Variable Migration
|
| 179 |
+
|
| 180 |
+
**Old (HF Hub):**
|
| 181 |
+
```bash
|
| 182 |
+
HF_TOKEN=hf_xxx
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
**New (Supabase):**
|
| 186 |
+
```bash
|
| 187 |
+
SUPABASE_URL=https://xxx.supabase.co
|
| 188 |
+
SUPABASE_KEY=eyJxxx
|
| 189 |
+
SUPABASE_BUCKET=outputs
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
## Additional Resources
|
| 193 |
+
|
| 194 |
+
- [Supabase Storage Documentation](https://supabase.com/docs/guides/storage)
|
| 195 |
+
- [Storage REST API Reference](https://supabase.com/docs/reference/storage)
|
| 196 |
+
- [File Upload Best Practices](https://supabase.com/docs/guides/storage/uploads)
|
| 197 |
+
|
api_client.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
InstantSplat API Client
|
| 4 |
+
|
| 5 |
+
Simple client to submit images and get back the Supabase GLB URL.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python api_client.py image1.jpg image2.jpg image3.jpg
|
| 9 |
+
|
| 10 |
+
Environment variables:
|
| 11 |
+
INSTANTSPLAT_SPACE: HuggingFace Space URL (default: use local)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import sys
|
| 15 |
+
import os
|
| 16 |
+
from gradio_client import Client
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def process_images(image_paths, space_url=None):
|
| 20 |
+
"""
|
| 21 |
+
Submit images to InstantSplat and get GLB URL.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
image_paths: List of image file paths
|
| 25 |
+
space_url: HuggingFace Space URL (optional)
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
dict with results
|
| 29 |
+
"""
|
| 30 |
+
# Validate inputs
|
| 31 |
+
if len(image_paths) < 2:
|
| 32 |
+
return {
|
| 33 |
+
"status": "error",
|
| 34 |
+
"error": "Need at least 2 images (3+ recommended)"
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
for path in image_paths:
|
| 38 |
+
if not os.path.exists(path):
|
| 39 |
+
return {
|
| 40 |
+
"status": "error",
|
| 41 |
+
"error": f"File not found: {path}"
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
# Connect to Space
|
| 46 |
+
if space_url:
|
| 47 |
+
print(f"Connecting to Space: {space_url}")
|
| 48 |
+
client = Client(space_url)
|
| 49 |
+
else:
|
| 50 |
+
print("Using local Gradio instance (make sure it's running)")
|
| 51 |
+
client = Client("http://localhost:7860")
|
| 52 |
+
|
| 53 |
+
print(f"Submitting {len(image_paths)} images for processing...")
|
| 54 |
+
for i, img in enumerate(image_paths, 1):
|
| 55 |
+
print(f" {i}. {img}")
|
| 56 |
+
|
| 57 |
+
# Submit job
|
| 58 |
+
result = client.predict(
|
| 59 |
+
image_paths,
|
| 60 |
+
api_name="/predict"
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Unpack results
|
| 64 |
+
video_path, ply_url, _, _, glb_path, glb_url = result
|
| 65 |
+
|
| 66 |
+
# Check if upload succeeded
|
| 67 |
+
if glb_url and not glb_url.startswith("Error"):
|
| 68 |
+
return {
|
| 69 |
+
"status": "success",
|
| 70 |
+
"glb_url": glb_url,
|
| 71 |
+
"ply_url": ply_url,
|
| 72 |
+
"video_available": video_path is not None,
|
| 73 |
+
"message": "Processing complete!"
|
| 74 |
+
}
|
| 75 |
+
else:
|
| 76 |
+
return {
|
| 77 |
+
"status": "error",
|
| 78 |
+
"error": glb_url or "Upload failed"
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
except Exception as e:
|
| 82 |
+
return {
|
| 83 |
+
"status": "error",
|
| 84 |
+
"error": str(e)
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def main():
|
| 89 |
+
"""CLI interface."""
|
| 90 |
+
if len(sys.argv) < 3:
|
| 91 |
+
print("Usage: python api_client.py <image1> <image2> [image3 ...]")
|
| 92 |
+
print("\nExample:")
|
| 93 |
+
print(" python api_client.py img1.jpg img2.jpg img3.jpg")
|
| 94 |
+
print("\nEnvironment Variables:")
|
| 95 |
+
print(" INSTANTSPLAT_SPACE - HuggingFace Space URL (optional)")
|
| 96 |
+
print(" e.g., your-username/InstantSplat")
|
| 97 |
+
sys.exit(1)
|
| 98 |
+
|
| 99 |
+
# Get images from command line
|
| 100 |
+
image_paths = sys.argv[1:]
|
| 101 |
+
|
| 102 |
+
# Get Space URL from environment or use local
|
| 103 |
+
space_url = os.environ.get("INSTANTSPLAT_SPACE")
|
| 104 |
+
|
| 105 |
+
print("=" * 80)
|
| 106 |
+
print("InstantSplat API Client")
|
| 107 |
+
print("=" * 80)
|
| 108 |
+
|
| 109 |
+
# Process images
|
| 110 |
+
result = process_images(image_paths, space_url)
|
| 111 |
+
|
| 112 |
+
print("\n" + "=" * 80)
|
| 113 |
+
|
| 114 |
+
# Display results
|
| 115 |
+
if result["status"] == "success":
|
| 116 |
+
print("✅ SUCCESS!")
|
| 117 |
+
print("-" * 80)
|
| 118 |
+
print(f"GLB URL: {result['glb_url']}")
|
| 119 |
+
print(f"PLY URL: {result['ply_url']}")
|
| 120 |
+
if result['video_available']:
|
| 121 |
+
print("Video: Available")
|
| 122 |
+
print("-" * 80)
|
| 123 |
+
print("\n💡 Tip: You can now download the GLB file:")
|
| 124 |
+
print(f" curl -o model.glb '{result['glb_url']}'")
|
| 125 |
+
print("=" * 80)
|
| 126 |
+
return 0
|
| 127 |
+
else:
|
| 128 |
+
print("❌ ERROR!")
|
| 129 |
+
print("-" * 80)
|
| 130 |
+
print(f"Error: {result['error']}")
|
| 131 |
+
print("=" * 80)
|
| 132 |
+
return 1
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
if __name__ == "__main__":
|
| 136 |
+
sys.exit(main())
|
| 137 |
+
|
app.py
CHANGED
|
@@ -392,11 +392,33 @@ def process(inputfiles, input_path=None):
|
|
| 392 |
# 3) ply file path (for gr.File download)
|
| 393 |
# 4) ply file path (for gr.Model3D viewer)
|
| 394 |
# 5) glb file path (for gr.Model3D viewer)
|
| 395 |
-
|
|
|
|
| 396 |
##################################################################################################################################################
|
| 397 |
|
| 398 |
|
| 399 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
_TITLE = '''InstantSplat'''
|
| 401 |
_DESCRIPTION = '''
|
| 402 |
<div style="display: flex; justify-content: center; align-items: center;">
|
|
@@ -433,6 +455,68 @@ with block:
|
|
| 433 |
inputfiles = gr.File(file_count="multiple", label="images")
|
| 434 |
input_path = gr.Textbox(visible=False, label="example_path")
|
| 435 |
button_gen = gr.Button("RUN")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
with gr.Row(variant='panel'):
|
| 438 |
with gr.Tab("Output"):
|
|
@@ -466,17 +550,24 @@ with block:
|
|
| 466 |
with gr.Column(scale=1):
|
| 467 |
output_video = gr.Video(label="video")
|
| 468 |
|
| 469 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
|
| 471 |
gr.Examples(
|
| 472 |
examples=[
|
| 473 |
"sora-santorini-3-views",
|
| 474 |
],
|
| 475 |
inputs=[input_path],
|
| 476 |
-
outputs=[output_video, output_file, output_download, output_model_ply, output_model_glb],
|
| 477 |
fn=lambda x: process(inputfiles=None, input_path=x),
|
| 478 |
cache_examples=True,
|
| 479 |
label='Sparse-view Examples'
|
| 480 |
)
|
| 481 |
|
| 482 |
-
block.launch(server_name="0.0.0.0", share=False)
|
|
|
|
| 392 |
# 3) ply file path (for gr.File download)
|
| 393 |
# 4) ply file path (for gr.Model3D viewer)
|
| 394 |
# 5) glb file path (for gr.Model3D viewer)
|
| 395 |
+
# 6) glb URL (for API)
|
| 396 |
+
return output_video_path, ply_url, output_ply_path, output_ply_path, output_glb_path, glb_url
|
| 397 |
##################################################################################################################################################
|
| 398 |
|
| 399 |
|
| 400 |
|
| 401 |
+
def process_api(inputfiles):
|
| 402 |
+
"""
|
| 403 |
+
API-friendly wrapper that returns only the GLB URL.
|
| 404 |
+
|
| 405 |
+
Args:
|
| 406 |
+
inputfiles: List of image files
|
| 407 |
+
|
| 408 |
+
Returns:
|
| 409 |
+
dict with glb_url, ply_url, and video_url
|
| 410 |
+
"""
|
| 411 |
+
result = process(inputfiles, input_path=None)
|
| 412 |
+
video_path, ply_url, _, _, glb_path, glb_url = result
|
| 413 |
+
|
| 414 |
+
return {
|
| 415 |
+
"glb_url": glb_url if glb_url else "Upload failed",
|
| 416 |
+
"ply_url": ply_url if ply_url else "Upload failed",
|
| 417 |
+
"video_available": video_path is not None,
|
| 418 |
+
"status": "success" if glb_url else "error"
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
|
| 422 |
_TITLE = '''InstantSplat'''
|
| 423 |
_DESCRIPTION = '''
|
| 424 |
<div style="display: flex; justify-content: center; align-items: center;">
|
|
|
|
| 455 |
inputfiles = gr.File(file_count="multiple", label="images")
|
| 456 |
input_path = gr.Textbox(visible=False, label="example_path")
|
| 457 |
button_gen = gr.Button("RUN")
|
| 458 |
+
|
| 459 |
+
with gr.Tab("API"):
|
| 460 |
+
gr.Markdown("""
|
| 461 |
+
## 🚀 API Access
|
| 462 |
+
|
| 463 |
+
Submit images programmatically and get back the Supabase GLB URL.
|
| 464 |
+
|
| 465 |
+
### Quick Start (Python)
|
| 466 |
+
|
| 467 |
+
```bash
|
| 468 |
+
pip install gradio_client
|
| 469 |
+
```
|
| 470 |
+
|
| 471 |
+
```python
|
| 472 |
+
from gradio_client import Client
|
| 473 |
+
|
| 474 |
+
# Connect to this Space
|
| 475 |
+
client = Client("your-username/InstantSplat")
|
| 476 |
+
|
| 477 |
+
# Submit images
|
| 478 |
+
result = client.predict(
|
| 479 |
+
["image1.jpg", "image2.jpg", "image3.jpg"],
|
| 480 |
+
api_name="/predict"
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
# Get GLB URL (it's the 6th element)
|
| 484 |
+
glb_url = result[5]
|
| 485 |
+
print(f"GLB URL: {glb_url}")
|
| 486 |
+
```
|
| 487 |
+
|
| 488 |
+
### Response Format
|
| 489 |
+
|
| 490 |
+
The API returns a tuple with 6 elements:
|
| 491 |
+
- `[0]` - Video path
|
| 492 |
+
- `[1]` - PLY URL (Supabase)
|
| 493 |
+
- `[2]` - PLY download path
|
| 494 |
+
- `[3]` - PLY model path
|
| 495 |
+
- `[4]` - GLB model path
|
| 496 |
+
- `[5]` - **GLB URL (Supabase)** ← This is what you want!
|
| 497 |
+
|
| 498 |
+
### CLI Tool
|
| 499 |
+
|
| 500 |
+
Use the included `api_client.py`:
|
| 501 |
+
|
| 502 |
+
```bash
|
| 503 |
+
python api_client.py img1.jpg img2.jpg img3.jpg
|
| 504 |
+
```
|
| 505 |
+
|
| 506 |
+
### Full Documentation
|
| 507 |
+
|
| 508 |
+
See `API_GUIDE.md` for complete documentation including:
|
| 509 |
+
- JavaScript/TypeScript examples
|
| 510 |
+
- Error handling
|
| 511 |
+
- Batch processing
|
| 512 |
+
- Complete workflows
|
| 513 |
+
|
| 514 |
+
### Requirements
|
| 515 |
+
|
| 516 |
+
- **Minimum**: 2 images (3+ recommended)
|
| 517 |
+
- **Same resolution**: All images must have matching dimensions
|
| 518 |
+
- **Formats**: JPG, PNG
|
| 519 |
+
""")
|
| 520 |
|
| 521 |
with gr.Row(variant='panel'):
|
| 522 |
with gr.Tab("Output"):
|
|
|
|
| 550 |
with gr.Column(scale=1):
|
| 551 |
output_video = gr.Video(label="video")
|
| 552 |
|
| 553 |
+
# Hidden output for GLB URL (for API access)
|
| 554 |
+
output_glb_url = gr.Textbox(visible=False, label="GLB URL")
|
| 555 |
+
|
| 556 |
+
button_gen.click(
|
| 557 |
+
process,
|
| 558 |
+
inputs=[inputfiles],
|
| 559 |
+
outputs=[output_video, output_file, output_download, output_model_ply, output_model_glb, output_glb_url]
|
| 560 |
+
)
|
| 561 |
|
| 562 |
gr.Examples(
|
| 563 |
examples=[
|
| 564 |
"sora-santorini-3-views",
|
| 565 |
],
|
| 566 |
inputs=[input_path],
|
| 567 |
+
outputs=[output_video, output_file, output_download, output_model_ply, output_model_glb, output_glb_url],
|
| 568 |
fn=lambda x: process(inputfiles=None, input_path=x),
|
| 569 |
cache_examples=True,
|
| 570 |
label='Sparse-view Examples'
|
| 571 |
)
|
| 572 |
|
| 573 |
+
block.launch(server_name="0.0.0.0", share=False, show_api=True)
|
env.example
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Supabase Storage Configuration
|
| 2 |
+
# Copy this file to .env and fill in your values
|
| 3 |
+
|
| 4 |
+
# Your Supabase project URL
|
| 5 |
+
# Example: https://abcdefghijklmnop.supabase.co
|
| 6 |
+
SUPABASE_URL=https://your-project-id.supabase.co
|
| 7 |
+
|
| 8 |
+
# Your Supabase service role key (for server-side uploads)
|
| 9 |
+
# Get this from: Supabase Dashboard > Settings > API > service_role key
|
| 10 |
+
# WARNING: Keep this secret! Never commit this to version control.
|
| 11 |
+
SUPABASE_KEY=your-service-role-key-here
|
| 12 |
+
|
| 13 |
+
# Storage bucket name (optional, defaults to "outputs")
|
| 14 |
+
SUPABASE_BUCKET=outputs
|
| 15 |
+
|
test_supabase_upload.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test script for Supabase Storage upload functionality.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python test_supabase_upload.py
|
| 7 |
+
|
| 8 |
+
Make sure to set environment variables before running:
|
| 9 |
+
export SUPABASE_URL="https://your-project-id.supabase.co"
|
| 10 |
+
export SUPABASE_KEY="your-service-role-key"
|
| 11 |
+
export SUPABASE_BUCKET="outputs" # optional
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import tempfile
|
| 17 |
+
import time
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def create_test_file(size_mb: int = 10) -> str:
|
| 21 |
+
"""Create a temporary test file of specified size in MB."""
|
| 22 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.bin')
|
| 23 |
+
|
| 24 |
+
# Write random data
|
| 25 |
+
chunk_size = 1024 * 1024 # 1 MB
|
| 26 |
+
for _ in range(size_mb):
|
| 27 |
+
temp_file.write(os.urandom(chunk_size))
|
| 28 |
+
|
| 29 |
+
temp_file.close()
|
| 30 |
+
print(f"Created test file: {temp_file.name} ({size_mb} MB)")
|
| 31 |
+
return temp_file.name
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def main():
|
| 35 |
+
# Import the upload function from app.py
|
| 36 |
+
try:
|
| 37 |
+
# Add parent directory to path
|
| 38 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 39 |
+
from app import upload_to_supabase_storage
|
| 40 |
+
except ImportError as e:
|
| 41 |
+
print(f"Error importing upload function: {e}")
|
| 42 |
+
print("Make sure app.py is in the same directory.")
|
| 43 |
+
return 1
|
| 44 |
+
|
| 45 |
+
# Check environment variables
|
| 46 |
+
supabase_url = os.environ.get("SUPABASE_URL")
|
| 47 |
+
supabase_key = os.environ.get("SUPABASE_KEY")
|
| 48 |
+
supabase_bucket = os.environ.get("SUPABASE_BUCKET", "outputs")
|
| 49 |
+
|
| 50 |
+
if not supabase_url:
|
| 51 |
+
print("ERROR: SUPABASE_URL environment variable not set")
|
| 52 |
+
print("Set it with: export SUPABASE_URL='https://your-project-id.supabase.co'")
|
| 53 |
+
return 1
|
| 54 |
+
|
| 55 |
+
if not supabase_key:
|
| 56 |
+
print("ERROR: SUPABASE_KEY environment variable not set")
|
| 57 |
+
print("Set it with: export SUPABASE_KEY='your-service-role-key'")
|
| 58 |
+
return 1
|
| 59 |
+
|
| 60 |
+
print("=" * 80)
|
| 61 |
+
print("Supabase Storage Upload Test")
|
| 62 |
+
print("=" * 80)
|
| 63 |
+
print(f"URL: {supabase_url}")
|
| 64 |
+
print(f"Bucket: {supabase_bucket}")
|
| 65 |
+
print(f"Key: {supabase_key[:20]}... (truncated)")
|
| 66 |
+
print("=" * 80)
|
| 67 |
+
|
| 68 |
+
# Test 1: Small file upload (1 MB)
|
| 69 |
+
print("\nTest 1: Small file upload (1 MB)")
|
| 70 |
+
print("-" * 80)
|
| 71 |
+
test_file_small = create_test_file(1)
|
| 72 |
+
|
| 73 |
+
try:
|
| 74 |
+
start_time = time.time()
|
| 75 |
+
url = upload_to_supabase_storage(
|
| 76 |
+
file_path=test_file_small,
|
| 77 |
+
remote_path="test/small_file.bin",
|
| 78 |
+
supabase_url=supabase_url,
|
| 79 |
+
supabase_key=supabase_key,
|
| 80 |
+
bucket_name=supabase_bucket
|
| 81 |
+
)
|
| 82 |
+
elapsed = time.time() - start_time
|
| 83 |
+
|
| 84 |
+
if url:
|
| 85 |
+
print(f"✅ SUCCESS: Uploaded in {elapsed:.2f} seconds")
|
| 86 |
+
print(f"URL: {url}")
|
| 87 |
+
else:
|
| 88 |
+
print("❌ FAILED: Upload returned None")
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"❌ ERROR: {e}")
|
| 91 |
+
finally:
|
| 92 |
+
os.unlink(test_file_small)
|
| 93 |
+
|
| 94 |
+
# Test 2: Medium file upload (50 MB)
|
| 95 |
+
print("\nTest 2: Medium file upload (50 MB)")
|
| 96 |
+
print("-" * 80)
|
| 97 |
+
test_file_medium = create_test_file(50)
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
start_time = time.time()
|
| 101 |
+
url = upload_to_supabase_storage(
|
| 102 |
+
file_path=test_file_medium,
|
| 103 |
+
remote_path="test/medium_file.bin",
|
| 104 |
+
supabase_url=supabase_url,
|
| 105 |
+
supabase_key=supabase_key,
|
| 106 |
+
bucket_name=supabase_bucket
|
| 107 |
+
)
|
| 108 |
+
elapsed = time.time() - start_time
|
| 109 |
+
|
| 110 |
+
if url:
|
| 111 |
+
print(f"✅ SUCCESS: Uploaded in {elapsed:.2f} seconds")
|
| 112 |
+
print(f"URL: {url}")
|
| 113 |
+
else:
|
| 114 |
+
print("❌ FAILED: Upload returned None")
|
| 115 |
+
except Exception as e:
|
| 116 |
+
print(f"❌ ERROR: {e}")
|
| 117 |
+
finally:
|
| 118 |
+
os.unlink(test_file_medium)
|
| 119 |
+
|
| 120 |
+
# Test 3: Large file upload (200 MB) - optional
|
| 121 |
+
print("\nTest 3: Large file upload (200 MB) - Optional")
|
| 122 |
+
print("-" * 80)
|
| 123 |
+
response = input("Run large file test? This will create and upload a 200MB file (y/N): ")
|
| 124 |
+
|
| 125 |
+
if response.lower() == 'y':
|
| 126 |
+
test_file_large = create_test_file(200)
|
| 127 |
+
|
| 128 |
+
try:
|
| 129 |
+
start_time = time.time()
|
| 130 |
+
url = upload_to_supabase_storage(
|
| 131 |
+
file_path=test_file_large,
|
| 132 |
+
remote_path="test/large_file.bin",
|
| 133 |
+
supabase_url=supabase_url,
|
| 134 |
+
supabase_key=supabase_key,
|
| 135 |
+
bucket_name=supabase_bucket
|
| 136 |
+
)
|
| 137 |
+
elapsed = time.time() - start_time
|
| 138 |
+
|
| 139 |
+
if url:
|
| 140 |
+
print(f"✅ SUCCESS: Uploaded in {elapsed:.2f} seconds")
|
| 141 |
+
print(f"Upload speed: {200 / elapsed:.2f} MB/s")
|
| 142 |
+
print(f"URL: {url}")
|
| 143 |
+
else:
|
| 144 |
+
print("❌ FAILED: Upload returned None")
|
| 145 |
+
except Exception as e:
|
| 146 |
+
print(f"❌ ERROR: {e}")
|
| 147 |
+
finally:
|
| 148 |
+
os.unlink(test_file_large)
|
| 149 |
+
else:
|
| 150 |
+
print("Skipped large file test")
|
| 151 |
+
|
| 152 |
+
print("\n" + "=" * 80)
|
| 153 |
+
print("Testing complete!")
|
| 154 |
+
print("=" * 80)
|
| 155 |
+
|
| 156 |
+
return 0
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
sys.exit(main())
|
| 161 |
+
|