Long Hoang commited on
Commit
1575924
·
1 Parent(s): 803c132

Use Supabase for outputs and add Python/TS API clients

Browse files
Files changed (7) hide show
  1. API_TYPESCRIPT.md +494 -0
  2. README_TS.md +164 -0
  3. TYPESCRIPT_QUICKSTART.md +97 -0
  4. api_client.py +6 -7
  5. api_client.ts +302 -0
  6. app.py +233 -198
  7. 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
- 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):
 
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
- if input_path is not None:
197
- imgs_path = './assets/example/' + input_path
198
- imgs_names = sorted(os.listdir(imgs_path))
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
- shutil.copy(image_path, img_folder_path)
226
- else:
227
- shutil.move(image_path, img_folder_path)
228
-
229
- train_img_list = sorted(os.listdir(img_folder_path))
230
- assert len(train_img_list)==opt.n_views, f"Number of images in the folder is not equal to {opt.n_views}"
231
-
232
- images, ori_size, imgs_resolution = load_images(img_folder_path, size=512)
233
- resolutions_are_equal = len(set(imgs_resolution)) == 1
234
- if resolutions_are_equal == False:
235
- raise gr.Error("The resolution of the input image should be the same.")
236
- print("ori_size", ori_size)
237
-
238
- start_time = time.time()
239
-
240
- pairs = make_pairs(images, scene_graph='complete', prefilter=None, symmetrize=True)
241
- output = inference(pairs, model, opt.device, batch_size=opt.batch_size)
242
- output_colmap_path=img_folder_path.replace("images", "sparse/0")
243
- os.makedirs(output_colmap_path, exist_ok=True)
244
-
245
- scene = global_aligner(output, device=opt.device, mode=GlobalAlignerMode.PointCloudOptimizer)
246
- loss = compute_global_alignment(scene=scene, init="mst", niter=opt.niter, schedule=opt.schedule, lr=opt.lr, focal_avg=opt.focal_avg)
247
- scene = scene.clean_pointcloud()
248
-
249
- imgs = to_numpy(scene.imgs)
250
- focals = scene.get_focals()
251
- poses = to_numpy(scene.get_im_poses())
252
- pts3d = to_numpy(scene.get_pts3d())
253
- scene.min_conf_thr = float(scene.conf_trf(torch.tensor(1.0)))
254
- confidence_masks = to_numpy(scene.get_masks())
255
- intrinsics = to_numpy(scene.get_intrinsics())
256
-
257
- end_time = time.time()
258
- print(f"Time taken for {opt.n_views} views: {end_time-start_time} seconds")
259
-
260
- save_colmap_cameras(ori_size, intrinsics, os.path.join(output_colmap_path, 'cameras.txt'))
261
- save_colmap_images(poses, os.path.join(output_colmap_path, 'images.txt'), train_img_list)
262
-
263
- pts_4_3dgs = np.concatenate([p[m] for p, m in zip(pts3d, confidence_masks)])
264
- color_4_3dgs = np.concatenate([p[m] for p, m in zip(imgs, confidence_masks)])
265
- color_4_3dgs = (color_4_3dgs * 255.0).astype(np.uint8)
266
- storePly(os.path.join(output_colmap_path, "points3D.ply"), pts_4_3dgs, color_4_3dgs)
267
- pts_4_3dgs_all = np.array(pts3d).reshape(-1, 3)
268
- np.save(output_colmap_path + "/pts_4_3dgs_all.npy", pts_4_3dgs_all)
269
- np.save(output_colmap_path + "/focal.npy", np.array(focals.cpu()))
270
-
271
- ### save VRAM
272
- del scene
273
- torch.cuda.empty_cache()
274
- gc.collect()
275
- ##################################################################################################################################################
276
-
277
- # ------ (2) Fast 3D-Gaussian Optimization ------
278
- parser = ArgumentParser(description="Training script parameters")
279
- lp = ModelParams(parser)
280
- op = OptimizationParams(parser)
281
- pp = PipelineParams(parser)
282
- parser.add_argument('--debug_from', type=int, default=-1)
283
- parser.add_argument("--test_iterations", nargs="+", type=int, default=[])
284
- parser.add_argument("--save_iterations", nargs="+", type=int, default=[])
285
- parser.add_argument("--checkpoint_iterations", nargs="+", type=int, default=[])
286
- parser.add_argument("--start_checkpoint", type=str, default=None)
287
-
288
- # FIX: scene must be string, not int
289
- parser.add_argument("--scene", type=str, default="demo")
290
-
291
- parser.add_argument("--n_views", type=int, default=3)
292
- parser.add_argument("--get_video", action="store_true")
293
- parser.add_argument("--optim_pose", type=bool, default=True)
294
- parser.add_argument("--skip_train", action="store_true")
295
- parser.add_argument("--skip_test", action="store_true")
296
-
297
- # FIX: do NOT parse system argv
298
- args, _ = parser.parse_known_args([])
299
- args.save_iterations.append(args.iterations)
300
- args.model_path = opt.img_base_path + '/output/'
301
- args.source_path = opt.img_base_path
302
- args.iteration = 1000
303
- os.makedirs(args.model_path, exist_ok=True)
304
-
305
- 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)
306
-
307
- ##################################################################################################################################################
308
-
309
- # ------ (3) Render video by interpolation ------
310
- parser = ArgumentParser(description="Testing script parameters")
311
- model = ModelParams(parser, sentinel=True)
312
- pipeline = PipelineParams(parser)
313
- args.eval = True
314
- args.get_video = True
315
- args.n_views = opt.n_views
316
-
317
- render_sets(
318
- model.extract(args),
319
- args.iteration,
320
- pipeline.extract(args),
321
- args.skip_train,
322
- args.skip_test,
323
- args,
324
- )
325
-
326
- output_ply_path = opt.img_base_path + f'/output/point_cloud/iteration_{args.iteration}/point_cloud.ply'
327
- output_video_path = opt.img_base_path + f'/output/demo_{opt.n_views}_view.mp4'
328
-
329
- # sanity checks
330
- if not os.path.exists(output_ply_path):
331
- print("PLY not found at:", output_ply_path)
332
- raise gr.Error(f"PLY file not found at {output_ply_path}")
333
-
334
- if not os.path.exists(output_video_path):
335
- print("Video not found at:", output_video_path)
336
- raise gr.Error(f"Video file not found at {output_video_path}")
337
-
338
- # Convert PLY to GLB for visualization
339
- output_glb_path = output_ply_path.replace('.ply', '.glb')
340
- if not convert_ply_to_glb(output_ply_path, output_glb_path):
341
- output_glb_path = None
342
-
343
- # ------ (4) upload .ply and .glb to Supabase Storage ------
344
- ply_url = None
345
- glb_url = None
346
- rel_remote_path_ply = f"{tmp_user_folder}_point_cloud.ply"
347
- rel_remote_path_glb = f"{tmp_user_folder}_point_cloud.glb"
348
-
349
- supabase_url = os.environ.get("SUPABASE_URL")
350
- supabase_key = os.environ.get("SUPABASE_KEY")
351
- supabase_bucket = os.environ.get("SUPABASE_BUCKET", "outputs")
352
-
353
- if supabase_url and supabase_key:
354
- try:
355
- # Upload PLY
356
- ply_url = upload_to_supabase_storage(
357
- file_path=output_ply_path,
358
- remote_path=rel_remote_path_ply,
359
- supabase_url=supabase_url,
360
- supabase_key=supabase_key,
361
- bucket_name=supabase_bucket,
362
- content_type='application/octet-stream'
363
- )
364
-
365
- if ply_url is None:
366
- ply_url = "Error uploading PLY file"
367
-
368
- # Upload GLB if it exists
369
- if output_glb_path and os.path.exists(output_glb_path):
370
- glb_url = upload_to_supabase_storage(
371
- file_path=output_glb_path,
372
- remote_path=rel_remote_path_glb,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  supabase_url=supabase_url,
374
  supabase_key=supabase_key,
375
  bucket_name=supabase_bucket,
376
- content_type='model/gltf-binary'
377
  )
378
 
379
- if glb_url is None:
380
- glb_url = "Error uploading GLB file"
381
 
382
- except Exception as e:
383
- print("Failed to upload files to Supabase Storage:", e)
384
- ply_url = f"Error uploading: {e}"
385
- else:
386
- print("SUPABASE_URL or SUPABASE_KEY not found, skipping upload.")
387
- ply_url = "Supabase credentials not set"
388
-
389
- # return:
390
- # 1) video path (for gr.Video)
391
- # 2) ply URL (for API + textbox)
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
 
@@ -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=True,
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
+