Spaces:
Paused
Paused
Long Hoang
commited on
Commit
·
1575924
1
Parent(s):
803c132
Use Supabase for outputs and add Python/TS API clients
Browse files- API_TYPESCRIPT.md +494 -0
- README_TS.md +164 -0
- TYPESCRIPT_QUICKSTART.md +97 -0
- api_client.py +6 -7
- api_client.ts +302 -0
- app.py +233 -198
- example_api_usage.ts +259 -0
API_TYPESCRIPT.md
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# InstantSplat TypeScript API Client
|
| 2 |
+
|
| 3 |
+
Complete TypeScript/JavaScript client for InstantSplat API with full type safety.
|
| 4 |
+
|
| 5 |
+
## 🚀 Quick Start
|
| 6 |
+
|
| 7 |
+
### Installation
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
npm install @gradio/client @types/node ts-node tsx
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
### Basic Usage
|
| 14 |
+
|
| 15 |
+
```typescript
|
| 16 |
+
import { processImages } from "./api_client";
|
| 17 |
+
|
| 18 |
+
const result = await processImages(
|
| 19 |
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
| 20 |
+
"your-username/InstantSplat"
|
| 21 |
+
);
|
| 22 |
+
|
| 23 |
+
if (result.status === "success") {
|
| 24 |
+
console.log("GLB URL:", result.glb_url);
|
| 25 |
+
console.log("PLY URL:", result.ply_url);
|
| 26 |
+
}
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## 📦 Available Functions
|
| 30 |
+
|
| 31 |
+
### `processImages(imagePaths, spaceUrl?)`
|
| 32 |
+
|
| 33 |
+
Submit images and get back URLs.
|
| 34 |
+
|
| 35 |
+
```typescript
|
| 36 |
+
async function processImages(
|
| 37 |
+
imagePaths: string[],
|
| 38 |
+
spaceUrl?: string
|
| 39 |
+
): Promise<ProcessResult>
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
**Parameters:**
|
| 43 |
+
- `imagePaths`: Array of local image file paths
|
| 44 |
+
- `spaceUrl`: (Optional) HuggingFace Space URL or env var `INSTANTSPLAT_SPACE`
|
| 45 |
+
|
| 46 |
+
**Returns:**
|
| 47 |
+
```typescript
|
| 48 |
+
interface ProcessResult {
|
| 49 |
+
status: "success" | "error";
|
| 50 |
+
glb_url?: string;
|
| 51 |
+
ply_url?: string;
|
| 52 |
+
video_available?: boolean;
|
| 53 |
+
error?: string;
|
| 54 |
+
message?: string;
|
| 55 |
+
}
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
**Example:**
|
| 59 |
+
```typescript
|
| 60 |
+
const result = await processImages(
|
| 61 |
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
| 62 |
+
"username/InstantSplat"
|
| 63 |
+
);
|
| 64 |
+
|
| 65 |
+
console.log(result.glb_url); // Supabase URL to GLB file
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### `completeWorkflow(imagePaths, outputDir, spaceUrl?)`
|
| 69 |
+
|
| 70 |
+
Process images and download the GLB file.
|
| 71 |
+
|
| 72 |
+
```typescript
|
| 73 |
+
async function completeWorkflow(
|
| 74 |
+
imagePaths: string[],
|
| 75 |
+
outputDir?: string,
|
| 76 |
+
spaceUrl?: string
|
| 77 |
+
): Promise<string | null>
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
**Parameters:**
|
| 81 |
+
- `imagePaths`: Array of image paths
|
| 82 |
+
- `outputDir`: Local directory for downloaded file (default: `./outputs`)
|
| 83 |
+
- `spaceUrl`: (Optional) Space URL
|
| 84 |
+
|
| 85 |
+
**Returns:** Local path to downloaded GLB file, or `null` on error
|
| 86 |
+
|
| 87 |
+
**Example:**
|
| 88 |
+
```typescript
|
| 89 |
+
const localPath = await completeWorkflow(
|
| 90 |
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
| 91 |
+
"./my_models"
|
| 92 |
+
);
|
| 93 |
+
|
| 94 |
+
console.log("Model saved to:", localPath);
|
| 95 |
+
// Output: Model saved to: ./my_models/model.glb
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
### `downloadFile(url, outputPath)`
|
| 99 |
+
|
| 100 |
+
Download a file from URL.
|
| 101 |
+
|
| 102 |
+
```typescript
|
| 103 |
+
async function downloadFile(
|
| 104 |
+
url: string,
|
| 105 |
+
outputPath: string
|
| 106 |
+
): Promise<void>
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
## 🛠️ CLI Usage
|
| 110 |
+
|
| 111 |
+
### Using npx
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
npx tsx api_client.ts img1.jpg img2.jpg img3.jpg
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
### Using npm script
|
| 118 |
+
|
| 119 |
+
```bash
|
| 120 |
+
npm run api img1.jpg img2.jpg img3.jpg
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### With environment variable
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
export INSTANTSPLAT_SPACE="username/InstantSplat"
|
| 127 |
+
npx tsx api_client.ts img1.jpg img2.jpg img3.jpg
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
## 📝 Examples
|
| 131 |
+
|
| 132 |
+
### Example 1: Simple Usage
|
| 133 |
+
|
| 134 |
+
```typescript
|
| 135 |
+
import { processImages } from "./api_client";
|
| 136 |
+
|
| 137 |
+
async function main() {
|
| 138 |
+
const result = await processImages(
|
| 139 |
+
["image1.jpg", "image2.jpg", "image3.jpg"],
|
| 140 |
+
"username/InstantSplat"
|
| 141 |
+
);
|
| 142 |
+
|
| 143 |
+
if (result.status === "success") {
|
| 144 |
+
console.log("✅ GLB URL:", result.glb_url);
|
| 145 |
+
console.log("✅ PLY URL:", result.ply_url);
|
| 146 |
+
} else {
|
| 147 |
+
console.error("❌ Error:", result.error);
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
main();
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### Example 2: With Error Handling
|
| 155 |
+
|
| 156 |
+
```typescript
|
| 157 |
+
import { processImages } from "./api_client";
|
| 158 |
+
|
| 159 |
+
async function processWithRetry(
|
| 160 |
+
images: string[],
|
| 161 |
+
maxRetries = 3
|
| 162 |
+
): Promise<ProcessResult> {
|
| 163 |
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
| 164 |
+
const result = await processImages(images);
|
| 165 |
+
|
| 166 |
+
if (result.status === "success") {
|
| 167 |
+
return result;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
if (attempt < maxRetries) {
|
| 171 |
+
const delay = Math.pow(2, attempt) * 1000;
|
| 172 |
+
await new Promise(resolve => setTimeout(resolve, delay));
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
throw new Error("All retries failed");
|
| 177 |
+
}
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
### Example 3: Batch Processing
|
| 181 |
+
|
| 182 |
+
```typescript
|
| 183 |
+
import { processImages } from "./api_client";
|
| 184 |
+
|
| 185 |
+
async function batchProcess(imageSets: string[][]) {
|
| 186 |
+
const results = [];
|
| 187 |
+
|
| 188 |
+
for (let i = 0; i < imageSets.length; i++) {
|
| 189 |
+
console.log(`Processing set ${i + 1}/${imageSets.length}...`);
|
| 190 |
+
|
| 191 |
+
const result = await processImages(imageSets[i]);
|
| 192 |
+
results.push(result);
|
| 193 |
+
|
| 194 |
+
// Rate limiting
|
| 195 |
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
return results;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
const sets = [
|
| 202 |
+
["scene1_img1.jpg", "scene1_img2.jpg"],
|
| 203 |
+
["scene2_img1.jpg", "scene2_img2.jpg"],
|
| 204 |
+
];
|
| 205 |
+
|
| 206 |
+
const results = await batchProcess(sets);
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
### Example 4: Download and Use
|
| 210 |
+
|
| 211 |
+
```typescript
|
| 212 |
+
import { completeWorkflow } from "./api_client";
|
| 213 |
+
|
| 214 |
+
async function processAndDownload() {
|
| 215 |
+
const modelPath = await completeWorkflow(
|
| 216 |
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
| 217 |
+
"./models"
|
| 218 |
+
);
|
| 219 |
+
|
| 220 |
+
if (modelPath) {
|
| 221 |
+
console.log(`Model ready at: ${modelPath}`);
|
| 222 |
+
// Use the model file...
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
## 🌐 Integration Examples
|
| 228 |
+
|
| 229 |
+
### Express.js API
|
| 230 |
+
|
| 231 |
+
```typescript
|
| 232 |
+
import express from "express";
|
| 233 |
+
import { processImages } from "./api_client";
|
| 234 |
+
|
| 235 |
+
const app = express();
|
| 236 |
+
app.use(express.json());
|
| 237 |
+
|
| 238 |
+
app.post("/api/process", async (req, res) => {
|
| 239 |
+
const { images } = req.body;
|
| 240 |
+
|
| 241 |
+
const result = await processImages(
|
| 242 |
+
images,
|
| 243 |
+
process.env.INSTANTSPLAT_SPACE
|
| 244 |
+
);
|
| 245 |
+
|
| 246 |
+
if (result.status === "success") {
|
| 247 |
+
res.json({
|
| 248 |
+
success: true,
|
| 249 |
+
glb_url: result.glb_url,
|
| 250 |
+
ply_url: result.ply_url
|
| 251 |
+
});
|
| 252 |
+
} else {
|
| 253 |
+
res.status(500).json({
|
| 254 |
+
success: false,
|
| 255 |
+
error: result.error
|
| 256 |
+
});
|
| 257 |
+
}
|
| 258 |
+
});
|
| 259 |
+
|
| 260 |
+
app.listen(3000);
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
### Next.js API Route
|
| 264 |
+
|
| 265 |
+
```typescript
|
| 266 |
+
// pages/api/process.ts
|
| 267 |
+
import type { NextApiRequest, NextApiResponse } from "next";
|
| 268 |
+
import { processImages } from "../../api_client";
|
| 269 |
+
|
| 270 |
+
export default async function handler(
|
| 271 |
+
req: NextApiRequest,
|
| 272 |
+
res: NextApiResponse
|
| 273 |
+
) {
|
| 274 |
+
if (req.method !== "POST") {
|
| 275 |
+
return res.status(405).json({ error: "Method not allowed" });
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
const { images } = req.body;
|
| 279 |
+
|
| 280 |
+
const result = await processImages(
|
| 281 |
+
images,
|
| 282 |
+
process.env.INSTANTSPLAT_SPACE
|
| 283 |
+
);
|
| 284 |
+
|
| 285 |
+
if (result.status === "success") {
|
| 286 |
+
res.status(200).json(result);
|
| 287 |
+
} else {
|
| 288 |
+
res.status(500).json(result);
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
### React Hook
|
| 294 |
+
|
| 295 |
+
```typescript
|
| 296 |
+
import { useState } from "react";
|
| 297 |
+
import { processImages } from "./api_client";
|
| 298 |
+
|
| 299 |
+
function useInstantSplat() {
|
| 300 |
+
const [loading, setLoading] = useState(false);
|
| 301 |
+
const [result, setResult] = useState<ProcessResult | null>(null);
|
| 302 |
+
|
| 303 |
+
const process = async (images: string[]) => {
|
| 304 |
+
setLoading(true);
|
| 305 |
+
try {
|
| 306 |
+
const res = await processImages(images);
|
| 307 |
+
setResult(res);
|
| 308 |
+
return res;
|
| 309 |
+
} finally {
|
| 310 |
+
setLoading(false);
|
| 311 |
+
}
|
| 312 |
+
};
|
| 313 |
+
|
| 314 |
+
return { process, loading, result };
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// Usage in component:
|
| 318 |
+
function MyComponent() {
|
| 319 |
+
const { process, loading, result } = useInstantSplat();
|
| 320 |
+
|
| 321 |
+
const handleSubmit = () => {
|
| 322 |
+
process(["img1.jpg", "img2.jpg", "img3.jpg"]);
|
| 323 |
+
};
|
| 324 |
+
|
| 325 |
+
return (
|
| 326 |
+
<div>
|
| 327 |
+
{loading && <p>Processing...</p>}
|
| 328 |
+
{result?.glb_url && <a href={result.glb_url}>Download Model</a>}
|
| 329 |
+
</div>
|
| 330 |
+
);
|
| 331 |
+
}
|
| 332 |
+
```
|
| 333 |
+
|
| 334 |
+
### Vue.js Composable
|
| 335 |
+
|
| 336 |
+
```typescript
|
| 337 |
+
import { ref } from "vue";
|
| 338 |
+
import { processImages } from "./api_client";
|
| 339 |
+
|
| 340 |
+
export function useInstantSplat() {
|
| 341 |
+
const loading = ref(false);
|
| 342 |
+
const result = ref(null);
|
| 343 |
+
|
| 344 |
+
const process = async (images: string[]) => {
|
| 345 |
+
loading.value = true;
|
| 346 |
+
try {
|
| 347 |
+
result.value = await processImages(images);
|
| 348 |
+
} finally {
|
| 349 |
+
loading.value = false;
|
| 350 |
+
}
|
| 351 |
+
};
|
| 352 |
+
|
| 353 |
+
return { process, loading, result };
|
| 354 |
+
}
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
## 📚 Type Definitions
|
| 358 |
+
|
| 359 |
+
```typescript
|
| 360 |
+
interface ProcessResult {
|
| 361 |
+
status: "success" | "error";
|
| 362 |
+
glb_url?: string; // Supabase URL to GLB file
|
| 363 |
+
ply_url?: string; // Supabase URL to PLY file
|
| 364 |
+
video_available?: boolean; // Whether video was generated
|
| 365 |
+
error?: string; // Error message if status is "error"
|
| 366 |
+
message?: string; // Success message
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
interface InstantSplatResponse {
|
| 370 |
+
video_path: string; // [0] Path to video
|
| 371 |
+
ply_url: string; // [1] PLY URL
|
| 372 |
+
ply_download: string; // [2] PLY download
|
| 373 |
+
ply_model: string; // [3] PLY model
|
| 374 |
+
glb_model: string; // [4] GLB model path
|
| 375 |
+
glb_url: string; // [5] GLB URL (Supabase)
|
| 376 |
+
}
|
| 377 |
+
```
|
| 378 |
+
|
| 379 |
+
## ⚙️ Configuration
|
| 380 |
+
|
| 381 |
+
### Environment Variables
|
| 382 |
+
|
| 383 |
+
```bash
|
| 384 |
+
# .env
|
| 385 |
+
INSTANTSPLAT_SPACE=username/InstantSplat
|
| 386 |
+
```
|
| 387 |
+
|
| 388 |
+
### package.json Scripts
|
| 389 |
+
|
| 390 |
+
Add to your `package.json`:
|
| 391 |
+
|
| 392 |
+
```json
|
| 393 |
+
{
|
| 394 |
+
"scripts": {
|
| 395 |
+
"api": "npx tsx api_client.ts",
|
| 396 |
+
"process": "npx tsx example_api_usage.ts"
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
## 🔧 Troubleshooting
|
| 402 |
+
|
| 403 |
+
### "Cannot find module '@gradio/client'"
|
| 404 |
+
|
| 405 |
+
```bash
|
| 406 |
+
npm install @gradio/client
|
| 407 |
+
```
|
| 408 |
+
|
| 409 |
+
### "Cannot find name 'require'"
|
| 410 |
+
|
| 411 |
+
Add to your `tsconfig.json`:
|
| 412 |
+
|
| 413 |
+
```json
|
| 414 |
+
{
|
| 415 |
+
"compilerOptions": {
|
| 416 |
+
"esModuleInterop": true,
|
| 417 |
+
"allowSyntheticDefaultImports": true
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
```
|
| 421 |
+
|
| 422 |
+
### TypeScript errors
|
| 423 |
+
|
| 424 |
+
Make sure you have the types installed:
|
| 425 |
+
|
| 426 |
+
```bash
|
| 427 |
+
npm install --save-dev @types/node
|
| 428 |
+
```
|
| 429 |
+
|
| 430 |
+
### Import errors
|
| 431 |
+
|
| 432 |
+
If using ES modules, change imports:
|
| 433 |
+
|
| 434 |
+
```typescript
|
| 435 |
+
// CommonJS
|
| 436 |
+
const { processImages } = require("./api_client");
|
| 437 |
+
|
| 438 |
+
// ES Modules
|
| 439 |
+
import { processImages } from "./api_client.js";
|
| 440 |
+
```
|
| 441 |
+
|
| 442 |
+
## 📦 Building for Production
|
| 443 |
+
|
| 444 |
+
### Compile TypeScript
|
| 445 |
+
|
| 446 |
+
```bash
|
| 447 |
+
npx tsc api_client.ts
|
| 448 |
+
```
|
| 449 |
+
|
| 450 |
+
### Bundle with esbuild
|
| 451 |
+
|
| 452 |
+
```bash
|
| 453 |
+
npx esbuild api_client.ts --bundle --platform=node --outfile=dist/api_client.js
|
| 454 |
+
```
|
| 455 |
+
|
| 456 |
+
### Use in production
|
| 457 |
+
|
| 458 |
+
```typescript
|
| 459 |
+
// Import the compiled version
|
| 460 |
+
import { processImages } from "./dist/api_client.js";
|
| 461 |
+
```
|
| 462 |
+
|
| 463 |
+
## 🎯 Best Practices
|
| 464 |
+
|
| 465 |
+
1. **Rate Limiting**: Add delays between batch requests
|
| 466 |
+
2. **Error Handling**: Always wrap in try-catch
|
| 467 |
+
3. **Retries**: Implement exponential backoff
|
| 468 |
+
4. **Timeouts**: Set reasonable timeouts for large images
|
| 469 |
+
5. **Validation**: Check image files exist before uploading
|
| 470 |
+
|
| 471 |
+
## 📖 More Examples
|
| 472 |
+
|
| 473 |
+
See `example_api_usage.ts` for complete working examples including:
|
| 474 |
+
- Simple usage
|
| 475 |
+
- Complete workflow with download
|
| 476 |
+
- Batch processing
|
| 477 |
+
- Error handling with retries
|
| 478 |
+
- Express.js integration
|
| 479 |
+
- React component integration
|
| 480 |
+
|
| 481 |
+
## 🆘 Support
|
| 482 |
+
|
| 483 |
+
- Check `API_GUIDE.md` for general API documentation
|
| 484 |
+
- Check `API_QUICKSTART.md` for quick start guide
|
| 485 |
+
- Review `example_api_usage.ts` for working examples
|
| 486 |
+
|
| 487 |
+
## 🔗 Related Files
|
| 488 |
+
|
| 489 |
+
- `api_client.ts` - Main TypeScript client
|
| 490 |
+
- `api_client.py` - Python equivalent
|
| 491 |
+
- `example_api_usage.ts` - Usage examples
|
| 492 |
+
- `API_GUIDE.md` - Complete API documentation
|
| 493 |
+
- `API_QUICKSTART.md` - Quick start guide
|
| 494 |
+
|
README_TS.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# InstantSplat TypeScript/JavaScript Client
|
| 2 |
+
|
| 3 |
+
TypeScript/JavaScript clients to call InstantSplat and get back Supabase Storage URLs for GLB/PLY files.
|
| 4 |
+
|
| 5 |
+
## 📦 Two Clients Available
|
| 6 |
+
|
| 7 |
+
1. **`api_client.ts`** - Modern API client with Supabase URLs (Recommended)
|
| 8 |
+
2. **`call_instantsplat.ts`** - Original client for direct file access
|
| 9 |
+
|
| 10 |
+
## Installation
|
| 11 |
+
|
| 12 |
+
```bash
|
| 13 |
+
npm install
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 🚀 API Client (Recommended)
|
| 19 |
+
|
| 20 |
+
Get Supabase URLs for your GLB/PLY files.
|
| 21 |
+
|
| 22 |
+
### Quick Start
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
# CLI usage
|
| 26 |
+
npm run api img1.jpg img2.jpg img3.jpg
|
| 27 |
+
|
| 28 |
+
# Or with tsx
|
| 29 |
+
npx tsx api_client.ts img1.jpg img2.jpg img3.jpg
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
### Programmatic Usage
|
| 33 |
+
|
| 34 |
+
```typescript
|
| 35 |
+
import { processImages } from "./api_client";
|
| 36 |
+
|
| 37 |
+
const result = await processImages(
|
| 38 |
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
| 39 |
+
"your-username/InstantSplat"
|
| 40 |
+
);
|
| 41 |
+
|
| 42 |
+
if (result.status === "success") {
|
| 43 |
+
console.log("GLB URL:", result.glb_url);
|
| 44 |
+
console.log("PLY URL:", result.ply_url);
|
| 45 |
+
}
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Complete Workflow (Process + Download)
|
| 49 |
+
|
| 50 |
+
```typescript
|
| 51 |
+
import { completeWorkflow } from "./api_client";
|
| 52 |
+
|
| 53 |
+
const localPath = await completeWorkflow(
|
| 54 |
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
| 55 |
+
"./models" // output directory
|
| 56 |
+
);
|
| 57 |
+
|
| 58 |
+
console.log("Model saved to:", localPath);
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### 📚 Full TypeScript Documentation
|
| 62 |
+
|
| 63 |
+
See **`API_TYPESCRIPT.md`** for:
|
| 64 |
+
- Complete API reference
|
| 65 |
+
- Type definitions
|
| 66 |
+
- Integration examples (Express, Next.js, React, Vue)
|
| 67 |
+
- Error handling patterns
|
| 68 |
+
- Batch processing
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## 📂 Original Client
|
| 73 |
+
|
| 74 |
+
For direct file access without Supabase.
|
| 75 |
+
|
| 76 |
+
### Basic Usage
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
npm run call <image1> <image2> [image3] ...
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
Example:
|
| 83 |
+
```bash
|
| 84 |
+
npm run call assets/example/sora-santorini-3-views/frame_00.jpg assets/example/sora-santorini-3-views/frame_06.jpg assets/example/sora-santorini-3-views/frame_12.jpg
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Or using tsx directly:
|
| 88 |
+
```bash
|
| 89 |
+
npx tsx call_instantsplat.ts image1.jpg image2.jpg image3.jpg
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
### Using as a Module
|
| 93 |
+
|
| 94 |
+
```typescript
|
| 95 |
+
import { callInstantSplat } from './call_instantsplat';
|
| 96 |
+
|
| 97 |
+
const result = await callInstantSplat([
|
| 98 |
+
'image1.jpg',
|
| 99 |
+
'image2.jpg',
|
| 100 |
+
'image3.jpg'
|
| 101 |
+
]);
|
| 102 |
+
|
| 103 |
+
console.log('Video:', result.video);
|
| 104 |
+
console.log('PLY URL:', result.plyUrl);
|
| 105 |
+
console.log('PLY Path:', result.plyPath);
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### Environment Variables
|
| 109 |
+
|
| 110 |
+
Optionally set `HF_TOKEN` environment variable if authentication is required:
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
export HF_TOKEN=your_huggingface_token
|
| 114 |
+
npm run call image1.jpg image2.jpg
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
## Requirements
|
| 118 |
+
|
| 119 |
+
- Node.js 18+
|
| 120 |
+
- At least 2 input images
|
| 121 |
+
- All images should have the same resolution
|
| 122 |
+
|
| 123 |
+
## Output
|
| 124 |
+
|
| 125 |
+
The script returns:
|
| 126 |
+
- **video**: Path to the rendered video file
|
| 127 |
+
- **plyUrl**: URL to download the PLY point cloud file
|
| 128 |
+
- **plyPath**: Local path to the PLY file
|
| 129 |
+
|
| 130 |
+
## Which Client Should I Use?
|
| 131 |
+
|
| 132 |
+
| Feature | API Client | Original Client |
|
| 133 |
+
|---------|-----------|----------------|
|
| 134 |
+
| **Returns** | Supabase URLs | Local file paths |
|
| 135 |
+
| **Best for** | Production APIs | Local testing |
|
| 136 |
+
| **File access** | Via URL | Direct file |
|
| 137 |
+
| **Storage** | Supabase Storage | Temporary |
|
| 138 |
+
| **Sharing** | Easy (URL) | Requires hosting |
|
| 139 |
+
|
| 140 |
+
**Use API Client (`api_client.ts`)** when:
|
| 141 |
+
- ✅ Building web APIs
|
| 142 |
+
- ✅ Need permanent URLs
|
| 143 |
+
- ✅ Want to share results
|
| 144 |
+
- ✅ Integrating with apps
|
| 145 |
+
|
| 146 |
+
**Use Original Client (`call_instantsplat.ts`)** when:
|
| 147 |
+
- ✅ Testing locally
|
| 148 |
+
- ✅ Need immediate file access
|
| 149 |
+
- ✅ Processing for local use only
|
| 150 |
+
|
| 151 |
+
## 📖 Documentation
|
| 152 |
+
|
| 153 |
+
- **`API_TYPESCRIPT.md`** - Complete TypeScript API docs
|
| 154 |
+
- **`API_GUIDE.md`** - General API documentation
|
| 155 |
+
- **`API_QUICKSTART.md`** - Quick start guide
|
| 156 |
+
- **`example_api_usage.ts`** - Working code examples
|
| 157 |
+
|
| 158 |
+
## Notes
|
| 159 |
+
|
| 160 |
+
- Processing may take several minutes depending on the number of images and their resolution
|
| 161 |
+
- The Hugging Face Space uses GPU resources which may have rate limits
|
| 162 |
+
- Make sure all input images have the same resolution
|
| 163 |
+
- For production use, we recommend the API Client with Supabase Storage
|
| 164 |
+
|
TYPESCRIPT_QUICKSTART.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TypeScript API Client - Quick Start
|
| 2 |
+
|
| 3 |
+
## ✅ Fixed and Ready to Use!
|
| 4 |
+
|
| 5 |
+
The TypeScript API client is now working. Here's how to use it:
|
| 6 |
+
|
| 7 |
+
## 🚀 Usage
|
| 8 |
+
|
| 9 |
+
### Option 1: With Environment Variable (Recommended)
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
# Set your Space URL
|
| 13 |
+
export INSTANTSPLAT_SPACE="your-username/InstantSplat"
|
| 14 |
+
|
| 15 |
+
# Run the client
|
| 16 |
+
npm run api test01/IMG_8533.jpeg test01/IMG_8534.jpeg test01/IMG_8535.jpeg
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
### Option 2: Programmatic Usage
|
| 20 |
+
|
| 21 |
+
```typescript
|
| 22 |
+
import { processImages } from "./api_client.js";
|
| 23 |
+
|
| 24 |
+
const result = await processImages(
|
| 25 |
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
| 26 |
+
"your-username/InstantSplat" // Specify Space URL here
|
| 27 |
+
);
|
| 28 |
+
|
| 29 |
+
if (result.status === "success") {
|
| 30 |
+
console.log("GLB URL:", result.glb_url);
|
| 31 |
+
console.log("PLY URL:", result.ply_url);
|
| 32 |
+
}
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### Option 3: Local Instance
|
| 36 |
+
|
| 37 |
+
If running the app locally:
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
# Terminal 1: Start the app
|
| 41 |
+
python app.py
|
| 42 |
+
|
| 43 |
+
# Terminal 2: Use the client (no Space URL needed)
|
| 44 |
+
npm run api img1.jpg img2.jpg img3.jpg
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## 🔧 What Was Fixed
|
| 48 |
+
|
| 49 |
+
1. ✅ Fixed `@gradio/client` import (uses `client` function, not `Client` class)
|
| 50 |
+
2. ✅ Fixed ES module compatibility (`import.meta.url` instead of `require.main`)
|
| 51 |
+
3. ✅ Updated all examples and documentation
|
| 52 |
+
|
| 53 |
+
## 📝 Complete Example
|
| 54 |
+
|
| 55 |
+
```typescript
|
| 56 |
+
import { processImages, completeWorkflow } from "./api_client.js";
|
| 57 |
+
|
| 58 |
+
// Example 1: Get URLs only
|
| 59 |
+
async function getURLs() {
|
| 60 |
+
const result = await processImages(
|
| 61 |
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
| 62 |
+
"your-username/InstantSplat"
|
| 63 |
+
);
|
| 64 |
+
|
| 65 |
+
if (result.status === "success") {
|
| 66 |
+
return {
|
| 67 |
+
glb: result.glb_url,
|
| 68 |
+
ply: result.ply_url
|
| 69 |
+
};
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Example 2: Process and download
|
| 74 |
+
async function processAndDownload() {
|
| 75 |
+
const localPath = await completeWorkflow(
|
| 76 |
+
["img1.jpg", "img2.jpg", "img3.jpg"],
|
| 77 |
+
"./models",
|
| 78 |
+
"your-username/InstantSplat"
|
| 79 |
+
);
|
| 80 |
+
|
| 81 |
+
console.log("Model saved to:", localPath);
|
| 82 |
+
}
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
## 🎯 Next Steps
|
| 86 |
+
|
| 87 |
+
1. Deploy your Space to HuggingFace
|
| 88 |
+
2. Get your Space URL (e.g., `username/InstantSplat`)
|
| 89 |
+
3. Run the client with your Space URL
|
| 90 |
+
4. Integrate into your TypeScript/JavaScript application
|
| 91 |
+
|
| 92 |
+
## 📚 Full Documentation
|
| 93 |
+
|
| 94 |
+
- `API_TYPESCRIPT.md` - Complete TypeScript API reference
|
| 95 |
+
- `example_api_usage.ts` - Working examples
|
| 96 |
+
- `README_TS.md` - General TypeScript client guide
|
| 97 |
+
|
api_client.py
CHANGED
|
@@ -16,7 +16,7 @@ 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 |
|
|
@@ -43,12 +43,11 @@ def process_images(image_paths, space_url=None):
|
|
| 43 |
|
| 44 |
try:
|
| 45 |
# Connect to Space
|
| 46 |
-
if space_url:
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 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):
|
|
|
|
| 16 |
from gradio_client import Client
|
| 17 |
|
| 18 |
|
| 19 |
+
def process_images(image_paths, space_url=None, hf_token=None):
|
| 20 |
"""
|
| 21 |
Submit images to InstantSplat and get GLB URL.
|
| 22 |
|
|
|
|
| 43 |
|
| 44 |
try:
|
| 45 |
# Connect to Space
|
| 46 |
+
if not space_url:
|
| 47 |
+
space_url = "longh37/InstantSplat"
|
| 48 |
+
|
| 49 |
+
print(f"Connecting to Space: {space_url}")
|
| 50 |
+
client = Client(space_url)
|
|
|
|
| 51 |
|
| 52 |
print(f"Submitting {len(image_paths)} images for processing...")
|
| 53 |
for i, img in enumerate(image_paths, 1):
|
api_client.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env ts-node
|
| 2 |
+
/**
|
| 3 |
+
* InstantSplat API Client (TypeScript)
|
| 4 |
+
*
|
| 5 |
+
* Submit images to InstantSplat and get back the Supabase GLB URL.
|
| 6 |
+
*
|
| 7 |
+
* Usage:
|
| 8 |
+
* npx ts-node api_client.ts image1.jpg image2.jpg image3.jpg
|
| 9 |
+
*
|
| 10 |
+
* Environment variables:
|
| 11 |
+
* INSTANTSPLAT_SPACE - HuggingFace Space URL (default: http://localhost:7860)
|
| 12 |
+
*/
|
| 13 |
+
|
| 14 |
+
// Polyfill for Node.js environment (Gradio client expects browser globals)
|
| 15 |
+
if (typeof window === "undefined") {
|
| 16 |
+
(global as any).window = {};
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
import { client } from "@gradio/client";
|
| 20 |
+
import * as fs from "fs";
|
| 21 |
+
import * as path from "path";
|
| 22 |
+
|
| 23 |
+
interface ProcessResult {
|
| 24 |
+
status: "success" | "error";
|
| 25 |
+
glb_url?: string;
|
| 26 |
+
ply_url?: string;
|
| 27 |
+
video_available?: boolean;
|
| 28 |
+
error?: string;
|
| 29 |
+
message?: string;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
interface InstantSplatResponse {
|
| 33 |
+
video_path: string;
|
| 34 |
+
ply_url: string;
|
| 35 |
+
ply_download: string;
|
| 36 |
+
ply_model: string;
|
| 37 |
+
glb_model: string;
|
| 38 |
+
glb_url: string;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
type GradioClient = Awaited<ReturnType<typeof client>>;
|
| 42 |
+
type PredictReturn = any; // Gradio client predict return type
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Submit images to InstantSplat and get GLB URL.
|
| 46 |
+
*/
|
| 47 |
+
export async function processImages(
|
| 48 |
+
imagePaths: string[],
|
| 49 |
+
spaceUrl?: string,
|
| 50 |
+
hfToken?: string
|
| 51 |
+
): Promise<ProcessResult> {
|
| 52 |
+
// Validate inputs
|
| 53 |
+
if (imagePaths.length < 2) {
|
| 54 |
+
return {
|
| 55 |
+
status: "error",
|
| 56 |
+
error: "Need at least 2 images (3+ recommended)"
|
| 57 |
+
};
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Check if files exist
|
| 61 |
+
for (const imagePath of imagePaths) {
|
| 62 |
+
if (!fs.existsSync(imagePath)) {
|
| 63 |
+
return {
|
| 64 |
+
status: "error",
|
| 65 |
+
error: `File not found: ${imagePath}`
|
| 66 |
+
};
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
try {
|
| 71 |
+
// Connect to Space
|
| 72 |
+
const url =
|
| 73 |
+
spaceUrl || process.env.INSTANTSPLAT_SPACE || "longh37/InstantSplat";
|
| 74 |
+
const token = hfToken || process.env.HF_TOKEN;
|
| 75 |
+
|
| 76 |
+
console.log(`Connecting to: ${url}`);
|
| 77 |
+
if (token) {
|
| 78 |
+
console.log(`Using HF token: ${token.substring(0, 10)}...`);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
const app = await client(url, { hf_token: token });
|
| 82 |
+
|
| 83 |
+
console.log(`✅ Connected to Space: ${url}`);
|
| 84 |
+
console.log(`Submitting ${imagePaths.length} images for processing...`);
|
| 85 |
+
imagePaths.forEach((img, i) => {
|
| 86 |
+
console.log(` ${i + 1}. ${img}`);
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
// Check available endpoints for debugging
|
| 90 |
+
try {
|
| 91 |
+
const apiInfo = await app.view_api();
|
| 92 |
+
console.log(
|
| 93 |
+
`📋 Available endpoints: ${Object.keys(
|
| 94 |
+
apiInfo.named_endpoints || {}
|
| 95 |
+
).join(", ")}`
|
| 96 |
+
);
|
| 97 |
+
} catch (e) {
|
| 98 |
+
console.log(" (Could not fetch API info)");
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// Prepare file info for logging
|
| 102 |
+
console.log("Preparing files...");
|
| 103 |
+
imagePaths.forEach((imgPath, index) => {
|
| 104 |
+
const stats = fs.statSync(imgPath);
|
| 105 |
+
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
| 106 |
+
const name = path.basename(imgPath);
|
| 107 |
+
console.log(
|
| 108 |
+
` File ${index + 1}/${imagePaths.length}: ${name} (${fileSizeMB} MB)`
|
| 109 |
+
);
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
const totalSizeMB =
|
| 113 |
+
imagePaths.reduce((sum, imgPath) => {
|
| 114 |
+
return sum + fs.statSync(imgPath).size;
|
| 115 |
+
}, 0) /
|
| 116 |
+
(1024 * 1024);
|
| 117 |
+
console.log(
|
| 118 |
+
`✅ All ${imagePaths.length} files ready (total: ${totalSizeMB.toFixed(
|
| 119 |
+
2
|
| 120 |
+
)} MB)`
|
| 121 |
+
);
|
| 122 |
+
console.log("📤 Uploading files and starting processing...");
|
| 123 |
+
console.log(
|
| 124 |
+
" (The Gradio client will handle file uploads automatically)"
|
| 125 |
+
);
|
| 126 |
+
console.log(
|
| 127 |
+
" (This may take a few minutes - processing includes 3D reconstruction)"
|
| 128 |
+
);
|
| 129 |
+
|
| 130 |
+
const startTime = Date.now();
|
| 131 |
+
let result: any;
|
| 132 |
+
try {
|
| 133 |
+
console.log(" Calling /process endpoint...");
|
| 134 |
+
// Pass file paths directly - the Gradio client will handle uploading them
|
| 135 |
+
// This matches how the Python client works
|
| 136 |
+
result = (await app.predict("/process", [imagePaths])) as any;
|
| 137 |
+
const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
|
| 138 |
+
console.log(`✅ Processing completed in ${elapsedSeconds} seconds`);
|
| 139 |
+
} catch (predictError: any) {
|
| 140 |
+
const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
|
| 141 |
+
console.error(`❌ Error after ${elapsedSeconds} seconds:`);
|
| 142 |
+
console.error(" Error type:", predictError?.constructor?.name);
|
| 143 |
+
console.error(" Error message:", predictError?.message);
|
| 144 |
+
if (predictError?.stage) {
|
| 145 |
+
console.error(" Stage:", predictError.stage);
|
| 146 |
+
}
|
| 147 |
+
throw predictError;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
// Extract results
|
| 151 |
+
// result.data is an array: [video, ply_url, download, model_ply, model_glb, glb_url]
|
| 152 |
+
const resultData = result.data || result;
|
| 153 |
+
const videoPath = resultData[0];
|
| 154 |
+
const plyUrl = resultData[1];
|
| 155 |
+
const glbUrl = resultData[5];
|
| 156 |
+
|
| 157 |
+
// Check if upload succeeded
|
| 158 |
+
if (glbUrl && !glbUrl.startsWith("Error")) {
|
| 159 |
+
return {
|
| 160 |
+
status: "success",
|
| 161 |
+
glb_url: glbUrl,
|
| 162 |
+
ply_url: plyUrl,
|
| 163 |
+
video_available: videoPath !== null,
|
| 164 |
+
message: "Processing complete!"
|
| 165 |
+
};
|
| 166 |
+
} else {
|
| 167 |
+
return {
|
| 168 |
+
status: "error",
|
| 169 |
+
error: glbUrl || "Upload failed"
|
| 170 |
+
};
|
| 171 |
+
}
|
| 172 |
+
} catch (error) {
|
| 173 |
+
console.error("Full error details:", error);
|
| 174 |
+
return {
|
| 175 |
+
status: "error",
|
| 176 |
+
error:
|
| 177 |
+
error instanceof Error
|
| 178 |
+
? error.message
|
| 179 |
+
: JSON.stringify(error, Object.getOwnPropertyNames(error))
|
| 180 |
+
};
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/**
|
| 185 |
+
* Download a file from a URL.
|
| 186 |
+
*/
|
| 187 |
+
export async function downloadFile(
|
| 188 |
+
url: string,
|
| 189 |
+
outputPath: string
|
| 190 |
+
): Promise<void> {
|
| 191 |
+
const response = await fetch(url);
|
| 192 |
+
|
| 193 |
+
if (!response.ok) {
|
| 194 |
+
throw new Error(
|
| 195 |
+
`Failed to download: ${response.status} ${response.statusText}`
|
| 196 |
+
);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
const buffer = await response.arrayBuffer();
|
| 200 |
+
fs.writeFileSync(outputPath, Buffer.from(buffer));
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
/**
|
| 204 |
+
* Complete workflow: Upload → Process → Download GLB
|
| 205 |
+
*/
|
| 206 |
+
export async function completeWorkflow(
|
| 207 |
+
imagePaths: string[],
|
| 208 |
+
outputDir: string = "./outputs",
|
| 209 |
+
spaceUrl?: string,
|
| 210 |
+
hfToken?: string
|
| 211 |
+
): Promise<string | null> {
|
| 212 |
+
console.log("🚀 Processing images...");
|
| 213 |
+
|
| 214 |
+
const result = await processImages(imagePaths, spaceUrl, hfToken);
|
| 215 |
+
|
| 216 |
+
if (result.status === "error") {
|
| 217 |
+
console.error(`❌ Error: ${result.error}`);
|
| 218 |
+
return null;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
console.log("✅ Processing complete!");
|
| 222 |
+
console.log(` GLB URL: ${result.glb_url}`);
|
| 223 |
+
console.log(` PLY URL: ${result.ply_url}`);
|
| 224 |
+
|
| 225 |
+
// Create output directory
|
| 226 |
+
if (!fs.existsSync(outputDir)) {
|
| 227 |
+
fs.mkdirSync(outputDir, { recursive: true });
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// Download GLB
|
| 231 |
+
const glbPath = path.join(outputDir, "model.glb");
|
| 232 |
+
console.log(`📥 Downloading GLB to ${glbPath}...`);
|
| 233 |
+
|
| 234 |
+
try {
|
| 235 |
+
await downloadFile(result.glb_url!, glbPath);
|
| 236 |
+
console.log(`✅ Downloaded: ${glbPath}`);
|
| 237 |
+
return glbPath;
|
| 238 |
+
} catch (error) {
|
| 239 |
+
console.error(`❌ Download failed: ${error}`);
|
| 240 |
+
return null;
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/**
|
| 245 |
+
* CLI interface
|
| 246 |
+
*/
|
| 247 |
+
async function main() {
|
| 248 |
+
const args = process.argv.slice(2);
|
| 249 |
+
|
| 250 |
+
if (args.length < 2) {
|
| 251 |
+
console.log(
|
| 252 |
+
"Usage: npx ts-node api_client.ts <image1> <image2> [image3 ...]"
|
| 253 |
+
);
|
| 254 |
+
console.log("\nExample:");
|
| 255 |
+
console.log(" npx ts-node api_client.ts img1.jpg img2.jpg img3.jpg");
|
| 256 |
+
console.log("\nEnvironment Variables:");
|
| 257 |
+
console.log(" INSTANTSPLAT_SPACE - HuggingFace Space URL (optional)");
|
| 258 |
+
console.log(" e.g., your-username/InstantSplat");
|
| 259 |
+
console.log(" HF_TOKEN - HuggingFace API token (optional)");
|
| 260 |
+
process.exit(1);
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
const imagePaths = args;
|
| 264 |
+
const spaceUrl = process.env.INSTANTSPLAT_SPACE;
|
| 265 |
+
const hfToken = process.env.HF_TOKEN;
|
| 266 |
+
|
| 267 |
+
console.log("=".repeat(80));
|
| 268 |
+
console.log("InstantSplat API Client (TypeScript)");
|
| 269 |
+
console.log("=".repeat(80));
|
| 270 |
+
|
| 271 |
+
const result = await processImages(imagePaths, spaceUrl, hfToken);
|
| 272 |
+
|
| 273 |
+
console.log("\n" + "=".repeat(80));
|
| 274 |
+
|
| 275 |
+
if (result.status === "success") {
|
| 276 |
+
console.log("✅ SUCCESS!");
|
| 277 |
+
console.log("-".repeat(80));
|
| 278 |
+
console.log(`GLB URL: ${result.glb_url}`);
|
| 279 |
+
console.log(`PLY URL: ${result.ply_url}`);
|
| 280 |
+
if (result.video_available) {
|
| 281 |
+
console.log("Video: Available");
|
| 282 |
+
}
|
| 283 |
+
console.log("-".repeat(80));
|
| 284 |
+
console.log("\n💡 Tip: You can now download the GLB file:");
|
| 285 |
+
console.log(` curl -o model.glb '${result.glb_url}'`);
|
| 286 |
+
console.log("=".repeat(80));
|
| 287 |
+
process.exit(0);
|
| 288 |
+
} else {
|
| 289 |
+
console.log("❌ ERROR!");
|
| 290 |
+
console.log("-".repeat(80));
|
| 291 |
+
console.log(`Error: ${result.error}`);
|
| 292 |
+
console.log("=".repeat(80));
|
| 293 |
+
process.exit(1);
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
// Run CLI if this file is executed directly
|
| 298 |
+
// Check if file is being run directly (ES module compatible)
|
| 299 |
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
| 300 |
+
if (isMainModule) {
|
| 301 |
+
main().catch(console.error);
|
| 302 |
+
}
|
app.py
CHANGED
|
@@ -192,208 +192,236 @@ def convert_ply_to_glb(ply_path, glb_path):
|
|
| 192 |
|
| 193 |
@spaces.GPU(duration=150)
|
| 194 |
def process(inputfiles, input_path=None):
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
inputfiles = []
|
| 201 |
-
for imgs_name in imgs_names:
|
| 202 |
-
file_path = os.path.join(imgs_path, imgs_name)
|
| 203 |
-
print(file_path)
|
| 204 |
-
inputfiles.append(file_path)
|
| 205 |
-
print(inputfiles)
|
| 206 |
-
|
| 207 |
-
# ------ (1) Coarse Geometric Initialization ------
|
| 208 |
-
parser = get_dust3r_args_parser()
|
| 209 |
-
opt = parser.parse_args()
|
| 210 |
-
|
| 211 |
-
tmp_user_folder = str(uuid.uuid4()).replace("-", "")
|
| 212 |
-
opt.img_base_path = os.path.join(opt.base_path, tmp_user_folder)
|
| 213 |
-
img_folder_path = os.path.join(opt.img_base_path, "images")
|
| 214 |
-
|
| 215 |
-
model = AsymmetricCroCo3DStereo.from_pretrained(opt.model_path).to(opt.device)
|
| 216 |
-
os.makedirs(img_folder_path, exist_ok=True)
|
| 217 |
-
|
| 218 |
-
opt.n_views = len(inputfiles)
|
| 219 |
-
if opt.n_views == 1:
|
| 220 |
-
raise gr.Error("The number of input images should be greater than 1.")
|
| 221 |
-
print("Multiple images: ", inputfiles)
|
| 222 |
-
|
| 223 |
-
for image_path in inputfiles:
|
| 224 |
if input_path is not None:
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
)
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
supabase_url=supabase_url,
|
| 374 |
supabase_key=supabase_key,
|
| 375 |
bucket_name=supabase_bucket,
|
| 376 |
-
content_type='
|
| 377 |
)
|
| 378 |
|
| 379 |
-
if
|
| 380 |
-
|
| 381 |
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
##################################################################################################################################################
|
| 398 |
|
| 399 |
|
|
@@ -410,12 +438,19 @@ def process_api(inputfiles):
|
|
| 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 |
|
|
@@ -566,7 +601,7 @@ with block:
|
|
| 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=
|
| 570 |
label='Sparse-view Examples'
|
| 571 |
)
|
| 572 |
|
|
|
|
| 192 |
|
| 193 |
@spaces.GPU(duration=150)
|
| 194 |
def process(inputfiles, input_path=None):
|
| 195 |
+
"""
|
| 196 |
+
Process images and generate 3D Gaussian Splatting model.
|
| 197 |
+
Returns: (video_path, ply_url, ply_download, ply_model, glb_model, glb_url)
|
| 198 |
+
"""
|
| 199 |
+
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
if input_path is not None:
|
| 201 |
+
imgs_path = './assets/example/' + input_path
|
| 202 |
+
imgs_names = sorted(os.listdir(imgs_path))
|
| 203 |
+
|
| 204 |
+
inputfiles = []
|
| 205 |
+
for imgs_name in imgs_names:
|
| 206 |
+
file_path = os.path.join(imgs_path, imgs_name)
|
| 207 |
+
print(file_path)
|
| 208 |
+
inputfiles.append(file_path)
|
| 209 |
+
print(inputfiles)
|
| 210 |
+
|
| 211 |
+
# ------ (1) Coarse Geometric Initialization ------
|
| 212 |
+
parser = get_dust3r_args_parser()
|
| 213 |
+
opt = parser.parse_args()
|
| 214 |
+
|
| 215 |
+
tmp_user_folder = str(uuid.uuid4()).replace("-", "")
|
| 216 |
+
opt.img_base_path = os.path.join(opt.base_path, tmp_user_folder)
|
| 217 |
+
img_folder_path = os.path.join(opt.img_base_path, "images")
|
| 218 |
+
|
| 219 |
+
model = AsymmetricCroCo3DStereo.from_pretrained(opt.model_path).to(opt.device)
|
| 220 |
+
os.makedirs(img_folder_path, exist_ok=True)
|
| 221 |
+
|
| 222 |
+
opt.n_views = len(inputfiles)
|
| 223 |
+
if opt.n_views == 1:
|
| 224 |
+
raise gr.Error("The number of input images should be greater than 1.")
|
| 225 |
+
print("Multiple images: ", inputfiles)
|
| 226 |
+
|
| 227 |
+
for image_path in inputfiles:
|
| 228 |
+
if input_path is not None:
|
| 229 |
+
shutil.copy(image_path, img_folder_path)
|
| 230 |
+
else:
|
| 231 |
+
shutil.move(image_path, img_folder_path)
|
| 232 |
+
|
| 233 |
+
train_img_list = sorted(os.listdir(img_folder_path))
|
| 234 |
+
assert len(train_img_list)==opt.n_views, f"Number of images in the folder is not equal to {opt.n_views}"
|
| 235 |
+
|
| 236 |
+
images, ori_size, imgs_resolution = load_images(img_folder_path, size=512)
|
| 237 |
+
resolutions_are_equal = len(set(imgs_resolution)) == 1
|
| 238 |
+
if resolutions_are_equal == False:
|
| 239 |
+
raise gr.Error("The resolution of the input image should be the same.")
|
| 240 |
+
print("ori_size", ori_size)
|
| 241 |
+
|
| 242 |
+
start_time = time.time()
|
| 243 |
+
|
| 244 |
+
pairs = make_pairs(images, scene_graph='complete', prefilter=None, symmetrize=True)
|
| 245 |
+
output = inference(pairs, model, opt.device, batch_size=opt.batch_size)
|
| 246 |
+
output_colmap_path=img_folder_path.replace("images", "sparse/0")
|
| 247 |
+
os.makedirs(output_colmap_path, exist_ok=True)
|
| 248 |
+
|
| 249 |
+
scene = global_aligner(output, device=opt.device, mode=GlobalAlignerMode.PointCloudOptimizer)
|
| 250 |
+
loss = compute_global_alignment(scene=scene, init="mst", niter=opt.niter, schedule=opt.schedule, lr=opt.lr, focal_avg=opt.focal_avg)
|
| 251 |
+
scene = scene.clean_pointcloud()
|
| 252 |
+
|
| 253 |
+
imgs = to_numpy(scene.imgs)
|
| 254 |
+
focals = scene.get_focals()
|
| 255 |
+
poses = to_numpy(scene.get_im_poses())
|
| 256 |
+
pts3d = to_numpy(scene.get_pts3d())
|
| 257 |
+
scene.min_conf_thr = float(scene.conf_trf(torch.tensor(1.0)))
|
| 258 |
+
confidence_masks = to_numpy(scene.get_masks())
|
| 259 |
+
intrinsics = to_numpy(scene.get_intrinsics())
|
| 260 |
+
|
| 261 |
+
end_time = time.time()
|
| 262 |
+
print(f"Time taken for {opt.n_views} views: {end_time-start_time} seconds")
|
| 263 |
+
|
| 264 |
+
save_colmap_cameras(ori_size, intrinsics, os.path.join(output_colmap_path, 'cameras.txt'))
|
| 265 |
+
save_colmap_images(poses, os.path.join(output_colmap_path, 'images.txt'), train_img_list)
|
| 266 |
+
|
| 267 |
+
pts_4_3dgs = np.concatenate([p[m] for p, m in zip(pts3d, confidence_masks)])
|
| 268 |
+
color_4_3dgs = np.concatenate([p[m] for p, m in zip(imgs, confidence_masks)])
|
| 269 |
+
color_4_3dgs = (color_4_3dgs * 255.0).astype(np.uint8)
|
| 270 |
+
storePly(os.path.join(output_colmap_path, "points3D.ply"), pts_4_3dgs, color_4_3dgs)
|
| 271 |
+
pts_4_3dgs_all = np.array(pts3d).reshape(-1, 3)
|
| 272 |
+
np.save(output_colmap_path + "/pts_4_3dgs_all.npy", pts_4_3dgs_all)
|
| 273 |
+
np.save(output_colmap_path + "/focal.npy", np.array(focals.cpu()))
|
| 274 |
+
|
| 275 |
+
### save VRAM
|
| 276 |
+
del scene
|
| 277 |
+
torch.cuda.empty_cache()
|
| 278 |
+
gc.collect()
|
| 279 |
+
##################################################################################################################################################
|
| 280 |
+
|
| 281 |
+
# ------ (2) Fast 3D-Gaussian Optimization ------
|
| 282 |
+
parser = ArgumentParser(description="Training script parameters")
|
| 283 |
+
lp = ModelParams(parser)
|
| 284 |
+
op = OptimizationParams(parser)
|
| 285 |
+
pp = PipelineParams(parser)
|
| 286 |
+
parser.add_argument('--debug_from', type=int, default=-1)
|
| 287 |
+
parser.add_argument("--test_iterations", nargs="+", type=int, default=[])
|
| 288 |
+
parser.add_argument("--save_iterations", nargs="+", type=int, default=[])
|
| 289 |
+
parser.add_argument("--checkpoint_iterations", nargs="+", type=int, default=[])
|
| 290 |
+
parser.add_argument("--start_checkpoint", type=str, default=None)
|
| 291 |
+
|
| 292 |
+
# FIX: scene must be string, not int
|
| 293 |
+
parser.add_argument("--scene", type=str, default="demo")
|
| 294 |
+
|
| 295 |
+
parser.add_argument("--n_views", type=int, default=3)
|
| 296 |
+
parser.add_argument("--get_video", action="store_true")
|
| 297 |
+
parser.add_argument("--optim_pose", type=bool, default=True)
|
| 298 |
+
parser.add_argument("--skip_train", action="store_true")
|
| 299 |
+
parser.add_argument("--skip_test", action="store_true")
|
| 300 |
+
|
| 301 |
+
# FIX: do NOT parse system argv
|
| 302 |
+
args, _ = parser.parse_known_args([])
|
| 303 |
+
args.save_iterations.append(args.iterations)
|
| 304 |
+
args.model_path = opt.img_base_path + '/output/'
|
| 305 |
+
args.source_path = opt.img_base_path
|
| 306 |
+
args.iteration = 1000
|
| 307 |
+
os.makedirs(args.model_path, exist_ok=True)
|
| 308 |
+
|
| 309 |
+
training(lp.extract(args), op.extract(args), pp.extract(args), args.test_iterations, args.save_iterations, args.checkpoint_iterations, args.start_checkpoint, args.debug_from, args)
|
| 310 |
+
|
| 311 |
+
##################################################################################################################################################
|
| 312 |
+
|
| 313 |
+
# ------ (3) Render video by interpolation ------
|
| 314 |
+
parser = ArgumentParser(description="Testing script parameters")
|
| 315 |
+
model = ModelParams(parser, sentinel=True)
|
| 316 |
+
pipeline = PipelineParams(parser)
|
| 317 |
+
args.eval = True
|
| 318 |
+
args.get_video = True
|
| 319 |
+
args.n_views = opt.n_views
|
| 320 |
+
|
| 321 |
+
render_sets(
|
| 322 |
+
model.extract(args),
|
| 323 |
+
args.iteration,
|
| 324 |
+
pipeline.extract(args),
|
| 325 |
+
args.skip_train,
|
| 326 |
+
args.skip_test,
|
| 327 |
+
args,
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
output_ply_path = opt.img_base_path + f'/output/point_cloud/iteration_{args.iteration}/point_cloud.ply'
|
| 331 |
+
output_video_path = opt.img_base_path + f'/output/demo_{opt.n_views}_view.mp4'
|
| 332 |
+
|
| 333 |
+
# sanity checks
|
| 334 |
+
if not os.path.exists(output_ply_path):
|
| 335 |
+
print("PLY not found at:", output_ply_path)
|
| 336 |
+
raise gr.Error(f"PLY file not found at {output_ply_path}")
|
| 337 |
+
|
| 338 |
+
if not os.path.exists(output_video_path):
|
| 339 |
+
print("Video not found at:", output_video_path)
|
| 340 |
+
raise gr.Error(f"Video file not found at {output_video_path}")
|
| 341 |
+
|
| 342 |
+
# Convert PLY to GLB for visualization
|
| 343 |
+
output_glb_path = output_ply_path.replace('.ply', '.glb')
|
| 344 |
+
if not convert_ply_to_glb(output_ply_path, output_glb_path):
|
| 345 |
+
output_glb_path = None
|
| 346 |
+
|
| 347 |
+
# ------ (4) upload .ply and .glb to Supabase Storage ------
|
| 348 |
+
ply_url = None
|
| 349 |
+
glb_url = None
|
| 350 |
+
rel_remote_path_ply = f"{tmp_user_folder}_point_cloud.ply"
|
| 351 |
+
rel_remote_path_glb = f"{tmp_user_folder}_point_cloud.glb"
|
| 352 |
+
|
| 353 |
+
supabase_url = os.environ.get("SUPABASE_URL")
|
| 354 |
+
supabase_key = os.environ.get("SUPABASE_KEY")
|
| 355 |
+
supabase_bucket = os.environ.get("SUPABASE_BUCKET", "outputs")
|
| 356 |
+
|
| 357 |
+
if supabase_url and supabase_key:
|
| 358 |
+
try:
|
| 359 |
+
# Upload PLY
|
| 360 |
+
ply_url = upload_to_supabase_storage(
|
| 361 |
+
file_path=output_ply_path,
|
| 362 |
+
remote_path=rel_remote_path_ply,
|
| 363 |
supabase_url=supabase_url,
|
| 364 |
supabase_key=supabase_key,
|
| 365 |
bucket_name=supabase_bucket,
|
| 366 |
+
content_type='application/octet-stream'
|
| 367 |
)
|
| 368 |
|
| 369 |
+
if ply_url is None:
|
| 370 |
+
ply_url = "Error uploading PLY file"
|
| 371 |
|
| 372 |
+
# Upload GLB if it exists
|
| 373 |
+
if output_glb_path and os.path.exists(output_glb_path):
|
| 374 |
+
glb_url = upload_to_supabase_storage(
|
| 375 |
+
file_path=output_glb_path,
|
| 376 |
+
remote_path=rel_remote_path_glb,
|
| 377 |
+
supabase_url=supabase_url,
|
| 378 |
+
supabase_key=supabase_key,
|
| 379 |
+
bucket_name=supabase_bucket,
|
| 380 |
+
content_type='model/gltf-binary'
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
if glb_url is None:
|
| 384 |
+
glb_url = "Error uploading GLB file"
|
| 385 |
+
|
| 386 |
+
except Exception as e:
|
| 387 |
+
print("Failed to upload files to Supabase Storage:", e)
|
| 388 |
+
ply_url = f"Error uploading: {e}"
|
| 389 |
+
else:
|
| 390 |
+
print("SUPABASE_URL or SUPABASE_KEY not found, skipping upload.")
|
| 391 |
+
ply_url = "Supabase credentials not set"
|
| 392 |
+
|
| 393 |
+
# return:
|
| 394 |
+
# 1) video path (for gr.Video)
|
| 395 |
+
# 2) ply URL (for API + textbox)
|
| 396 |
+
# 3) ply file path (for gr.File download)
|
| 397 |
+
# 4) ply file path (for gr.Model3D viewer)
|
| 398 |
+
# 5) glb file path (for gr.Model3D viewer)
|
| 399 |
+
# 6) glb URL (for API)
|
| 400 |
+
return output_video_path, ply_url, output_ply_path, output_ply_path, output_glb_path, glb_url
|
| 401 |
+
|
| 402 |
+
except Exception as e:
|
| 403 |
+
# Catch all errors and return them in the API response
|
| 404 |
+
error_msg = f"Error: {str(e)}"
|
| 405 |
+
error_traceback = f"Traceback:\n{__import__('traceback').format_exc()}"
|
| 406 |
+
full_error = f"{error_msg}\n\n{error_traceback}"
|
| 407 |
+
|
| 408 |
+
print("=" * 80)
|
| 409 |
+
print("ERROR IN PROCESS FUNCTION:")
|
| 410 |
+
print("=" * 80)
|
| 411 |
+
print(full_error)
|
| 412 |
+
print("=" * 80)
|
| 413 |
+
|
| 414 |
+
# Return error messages in the same format as successful returns
|
| 415 |
+
# This allows API clients to see the exact error
|
| 416 |
+
error_prefix = "ERROR: "
|
| 417 |
+
return (
|
| 418 |
+
None, # video path
|
| 419 |
+
f"{error_prefix}{error_msg}", # ply_url
|
| 420 |
+
None, # ply_download
|
| 421 |
+
None, # ply_model
|
| 422 |
+
None, # glb_model
|
| 423 |
+
f"{error_prefix}{error_msg}" # glb_url
|
| 424 |
+
)
|
| 425 |
##################################################################################################################################################
|
| 426 |
|
| 427 |
|
|
|
|
| 438 |
"""
|
| 439 |
result = process(inputfiles, input_path=None)
|
| 440 |
video_path, ply_url, _, _, glb_path, glb_url = result
|
| 441 |
+
|
| 442 |
+
# Detect error responses by prefix
|
| 443 |
+
is_error = False
|
| 444 |
+
for v in (glb_url, ply_url):
|
| 445 |
+
if isinstance(v, str) and v.startswith("ERROR:"):
|
| 446 |
+
is_error = True
|
| 447 |
+
break
|
| 448 |
+
|
| 449 |
return {
|
| 450 |
"glb_url": glb_url if glb_url else "Upload failed",
|
| 451 |
"ply_url": ply_url if ply_url else "Upload failed",
|
| 452 |
"video_available": video_path is not None,
|
| 453 |
+
"status": "error" if is_error else ("success" if glb_url else "error"),
|
| 454 |
}
|
| 455 |
|
| 456 |
|
|
|
|
| 601 |
inputs=[input_path],
|
| 602 |
outputs=[output_video, output_file, output_download, output_model_ply, output_model_glb, output_glb_url],
|
| 603 |
fn=lambda x: process(inputfiles=None, input_path=x),
|
| 604 |
+
cache_examples=False, # Disabled for faster startup
|
| 605 |
label='Sparse-view Examples'
|
| 606 |
)
|
| 607 |
|
example_api_usage.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Example usage of the InstantSplat TypeScript API client
|
| 3 |
+
*
|
| 4 |
+
* Run with: npx tsx example_api_usage.ts
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { processImages, completeWorkflow } from "./api_client.js";
|
| 8 |
+
|
| 9 |
+
// Example 1: Simple usage - Get GLB URL
|
| 10 |
+
async function example1() {
|
| 11 |
+
console.log("\n=== Example 1: Get GLB URL ===\n");
|
| 12 |
+
|
| 13 |
+
const result = await processImages(
|
| 14 |
+
["test01/IMG_8533.jpeg", "test01/IMG_8534.jpeg", "test01/IMG_8535.jpeg"],
|
| 15 |
+
"your-username/InstantSplat" // or use env var
|
| 16 |
+
);
|
| 17 |
+
|
| 18 |
+
if (result.status === "success") {
|
| 19 |
+
console.log("✅ Success!");
|
| 20 |
+
console.log("GLB URL:", result.glb_url);
|
| 21 |
+
console.log("PLY URL:", result.ply_url);
|
| 22 |
+
} else {
|
| 23 |
+
console.error("❌ Error:", result.error);
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Example 2: Complete workflow - Process and download
|
| 28 |
+
async function example2() {
|
| 29 |
+
console.log("\n=== Example 2: Complete Workflow ===\n");
|
| 30 |
+
|
| 31 |
+
const localPath = await completeWorkflow(
|
| 32 |
+
["test01/IMG_8533.jpeg", "test01/IMG_8534.jpeg", "test01/IMG_8535.jpeg"],
|
| 33 |
+
"./my_models", // output directory
|
| 34 |
+
"your-username/InstantSplat"
|
| 35 |
+
);
|
| 36 |
+
|
| 37 |
+
if (localPath) {
|
| 38 |
+
console.log("🎉 Model saved to:", localPath);
|
| 39 |
+
} else {
|
| 40 |
+
console.error("Failed to process or download model");
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Example 3: Batch processing multiple scenes
|
| 45 |
+
async function example3() {
|
| 46 |
+
console.log("\n=== Example 3: Batch Processing ===\n");
|
| 47 |
+
|
| 48 |
+
const scenes = [
|
| 49 |
+
["scene1_img1.jpg", "scene1_img2.jpg", "scene1_img3.jpg"],
|
| 50 |
+
["scene2_img1.jpg", "scene2_img2.jpg", "scene2_img3.jpg"],
|
| 51 |
+
];
|
| 52 |
+
|
| 53 |
+
const results = [];
|
| 54 |
+
|
| 55 |
+
for (let i = 0; i < scenes.length; i++) {
|
| 56 |
+
console.log(`\nProcessing scene ${i + 1}/${scenes.length}...`);
|
| 57 |
+
|
| 58 |
+
const result = await processImages(
|
| 59 |
+
scenes[i],
|
| 60 |
+
"your-username/InstantSplat"
|
| 61 |
+
);
|
| 62 |
+
|
| 63 |
+
results.push({
|
| 64 |
+
scene: i + 1,
|
| 65 |
+
...result
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
// Rate limiting - be nice to the server
|
| 69 |
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Summary
|
| 73 |
+
console.log("\n=== Batch Processing Summary ===\n");
|
| 74 |
+
results.forEach(r => {
|
| 75 |
+
if (r.status === "success") {
|
| 76 |
+
console.log(`Scene ${r.scene}: ✅ ${r.glb_url}`);
|
| 77 |
+
} else {
|
| 78 |
+
console.log(`Scene ${r.scene}: ❌ ${r.error}`);
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Example 4: With error handling and retries
|
| 84 |
+
async function example4() {
|
| 85 |
+
console.log("\n=== Example 4: With Retries ===\n");
|
| 86 |
+
|
| 87 |
+
async function processWithRetry(
|
| 88 |
+
images: string[],
|
| 89 |
+
maxRetries: number = 3
|
| 90 |
+
) {
|
| 91 |
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
| 92 |
+
console.log(`Attempt ${attempt}/${maxRetries}...`);
|
| 93 |
+
|
| 94 |
+
const result = await processImages(
|
| 95 |
+
images,
|
| 96 |
+
"your-username/InstantSplat"
|
| 97 |
+
);
|
| 98 |
+
|
| 99 |
+
if (result.status === "success") {
|
| 100 |
+
return result;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
console.log(`Failed: ${result.error}`);
|
| 104 |
+
|
| 105 |
+
if (attempt < maxRetries) {
|
| 106 |
+
const delay = Math.pow(2, attempt) * 1000; // exponential backoff
|
| 107 |
+
console.log(`Retrying in ${delay}ms...`);
|
| 108 |
+
await new Promise(resolve => setTimeout(resolve, delay));
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
throw new Error("All retry attempts failed");
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
try {
|
| 116 |
+
const result = await processWithRetry([
|
| 117 |
+
"test01/IMG_8533.jpeg",
|
| 118 |
+
"test01/IMG_8534.jpeg",
|
| 119 |
+
"test01/IMG_8535.jpeg"
|
| 120 |
+
]);
|
| 121 |
+
|
| 122 |
+
console.log("✅ Success after retry!");
|
| 123 |
+
console.log("GLB URL:", result.glb_url);
|
| 124 |
+
} catch (error) {
|
| 125 |
+
console.error("❌ Failed after all retries:", error);
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Example 5: Integration with Express.js API
|
| 130 |
+
function example5Code() {
|
| 131 |
+
return `
|
| 132 |
+
// Express.js API endpoint example
|
| 133 |
+
import express from 'express';
|
| 134 |
+
import { processImages } from './api_client';
|
| 135 |
+
|
| 136 |
+
const app = express();
|
| 137 |
+
|
| 138 |
+
app.post('/api/process-images', async (req, res) => {
|
| 139 |
+
try {
|
| 140 |
+
const { images } = req.body; // Array of image paths or URLs
|
| 141 |
+
|
| 142 |
+
const result = await processImages(
|
| 143 |
+
images,
|
| 144 |
+
process.env.INSTANTSPLAT_SPACE
|
| 145 |
+
);
|
| 146 |
+
|
| 147 |
+
if (result.status === 'success') {
|
| 148 |
+
res.json({
|
| 149 |
+
success: true,
|
| 150 |
+
glb_url: result.glb_url,
|
| 151 |
+
ply_url: result.ply_url
|
| 152 |
+
});
|
| 153 |
+
} else {
|
| 154 |
+
res.status(500).json({
|
| 155 |
+
success: false,
|
| 156 |
+
error: result.error
|
| 157 |
+
});
|
| 158 |
+
}
|
| 159 |
+
} catch (error) {
|
| 160 |
+
res.status(500).json({
|
| 161 |
+
success: false,
|
| 162 |
+
error: String(error)
|
| 163 |
+
});
|
| 164 |
+
}
|
| 165 |
+
});
|
| 166 |
+
|
| 167 |
+
app.listen(3000, () => {
|
| 168 |
+
console.log('API running on http://localhost:3000');
|
| 169 |
+
});
|
| 170 |
+
`;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Example 6: React component integration
|
| 174 |
+
function example6Code() {
|
| 175 |
+
return `
|
| 176 |
+
// React component example
|
| 177 |
+
import { useState } from 'react';
|
| 178 |
+
import { processImages } from './api_client';
|
| 179 |
+
|
| 180 |
+
function InstantSplatUploader() {
|
| 181 |
+
const [loading, setLoading] = useState(false);
|
| 182 |
+
const [result, setResult] = useState(null);
|
| 183 |
+
|
| 184 |
+
const handleSubmit = async (files: File[]) => {
|
| 185 |
+
setLoading(true);
|
| 186 |
+
|
| 187 |
+
try {
|
| 188 |
+
// Convert files to paths or upload them first
|
| 189 |
+
const imagePaths = files.map(f => f.path);
|
| 190 |
+
|
| 191 |
+
const result = await processImages(
|
| 192 |
+
imagePaths,
|
| 193 |
+
process.env.NEXT_PUBLIC_INSTANTSPLAT_SPACE
|
| 194 |
+
);
|
| 195 |
+
|
| 196 |
+
setResult(result);
|
| 197 |
+
} catch (error) {
|
| 198 |
+
console.error(error);
|
| 199 |
+
} finally {
|
| 200 |
+
setLoading(false);
|
| 201 |
+
}
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
return (
|
| 205 |
+
<div>
|
| 206 |
+
{loading && <p>Processing...</p>}
|
| 207 |
+
|
| 208 |
+
{result?.status === 'success' && (
|
| 209 |
+
<div>
|
| 210 |
+
<h3>Your 3D Model:</h3>
|
| 211 |
+
<a href={result.glb_url}>Download GLB</a>
|
| 212 |
+
<a href={result.ply_url}>Download PLY</a>
|
| 213 |
+
</div>
|
| 214 |
+
)}
|
| 215 |
+
|
| 216 |
+
{result?.status === 'error' && (
|
| 217 |
+
<p>Error: {result.error}</p>
|
| 218 |
+
)}
|
| 219 |
+
</div>
|
| 220 |
+
);
|
| 221 |
+
}
|
| 222 |
+
`;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
// Run examples
|
| 226 |
+
async function main() {
|
| 227 |
+
console.log("InstantSplat API - TypeScript Examples");
|
| 228 |
+
console.log("=" .repeat(80));
|
| 229 |
+
|
| 230 |
+
// Uncomment the examples you want to run:
|
| 231 |
+
|
| 232 |
+
// await example1(); // Simple usage
|
| 233 |
+
// await example2(); // Complete workflow
|
| 234 |
+
// await example3(); // Batch processing
|
| 235 |
+
// await example4(); // With retries
|
| 236 |
+
|
| 237 |
+
// Code examples (not executable):
|
| 238 |
+
console.log("\n=== Example 5: Express.js Integration ===");
|
| 239 |
+
console.log(example5Code());
|
| 240 |
+
|
| 241 |
+
console.log("\n=== Example 6: React Component ===");
|
| 242 |
+
console.log(example6Code());
|
| 243 |
+
|
| 244 |
+
console.log("\n💡 Uncomment the examples in main() to run them!");
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// Run if this file is executed directly (ES module compatible)
|
| 248 |
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
| 249 |
+
if (isMainModule) {
|
| 250 |
+
main().catch(console.error);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
export {
|
| 254 |
+
example1,
|
| 255 |
+
example2,
|
| 256 |
+
example3,
|
| 257 |
+
example4
|
| 258 |
+
};
|
| 259 |
+
|