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

Add Supabase upload and base64 API + TS client

Browse files
Files changed (2) hide show
  1. api_client.ts +84 -95
  2. app.py +67 -0
api_client.ts CHANGED
@@ -8,15 +8,10 @@
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
 
@@ -29,24 +24,12 @@ interface ProcessResult {
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
@@ -68,36 +51,6 @@ export async function processImages(
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) => {
@@ -119,56 +72,90 @@ export async function processImages(
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 {
@@ -206,12 +193,12 @@ export async function downloadFile(
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}`);
@@ -254,21 +241,23 @@ async function main() {
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
 
 
8
  * npx ts-node api_client.ts image1.jpg image2.jpg image3.jpg
9
  *
10
  * Environment variables:
11
+ * INSTANTSPLAT_API_URL - Base URL of the Space (e.g. https://longh37-InstantSplat.hf.space)
12
+ * HF_TOKEN - Hugging Face API token (optional, for private Spaces)
13
  */
14
 
 
 
 
 
 
 
15
  import * as fs from "fs";
16
  import * as path from "path";
17
 
 
24
  message?: string;
25
  }
26
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  /**
28
  * Submit images to InstantSplat and get GLB URL.
29
  */
30
  export async function processImages(
31
  imagePaths: string[],
32
+ apiBaseUrl?: string,
33
  hfToken?: string
34
  ): Promise<ProcessResult> {
35
  // Validate inputs
 
51
  }
52
 
53
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  // Prepare file info for logging
55
  console.log("Preparing files...");
56
  imagePaths.forEach((imgPath, index) => {
 
72
  2
73
  )} MB)`
74
  );
 
 
 
 
 
 
 
75
 
76
+ // Convert files to base64 data URLs
77
+ const imagesB64 = imagePaths.map((imgPath) => {
78
+ const data = fs.readFileSync(imgPath);
79
+ const ext = path.extname(imgPath).toLowerCase();
80
+ let mimeType = "image/jpeg";
81
+ if (ext === ".png") mimeType = "image/png";
82
+ else if (ext === ".jpg" || ext === ".jpeg") mimeType = "image/jpeg";
83
+ else if (ext === ".webp") mimeType = "image/webp";
84
+ const base64 = data.toString("base64");
85
+ return `data:${mimeType};base64,${base64}`;
86
+ });
87
+
88
+ // Determine API base URL
89
+ const baseUrl =
90
+ apiBaseUrl ||
91
+ process.env.INSTANTSPLAT_API_URL ||
92
+ process.env.INSTANTSPLAT_SPACE ||
93
+ "https://longh37-InstantSplat.hf.space";
94
+
95
+ const url = `${baseUrl.replace(/\/+$/, "")}/api/base64`;
96
+ const token = hfToken || process.env.HF_TOKEN;
97
+
98
+ console.log(`Connecting to API: ${url}`);
99
+ if (token) {
100
+ console.log(`Using HF token: ${token.substring(0, 10)}...`);
101
+ }
102
+
103
+ const headers: Record<string, string> = {
104
+ "Content-Type": "application/json"
105
+ };
106
+ if (token) {
107
+ headers["Authorization"] = `Bearer ${token}`;
108
+ }
109
+
110
+ console.log("📤 Uploading images as base64 and starting processing...");
111
  const startTime = Date.now();
112
+
113
+ const resp = await fetch(url, {
114
+ method: "POST",
115
+ headers,
116
+ body: JSON.stringify({ images: imagesB64 })
117
+ });
118
+
119
+ const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
120
+ console.log(`⏱ Server responded in ${elapsedSeconds} seconds`);
121
+
122
+ if (!resp.ok) {
123
+ const text = await resp.text();
124
+ console.error(
125
+ `❌ HTTP ${resp.status} ${resp.statusText} from API: ${text}`
126
+ );
127
+ return {
128
+ status: "error",
129
+ error: `HTTP ${resp.status} ${resp.statusText}: ${text}`
130
+ };
131
  }
132
 
133
+ const json = (await resp.json()) as {
134
+ glb_url?: string;
135
+ ply_url?: string;
136
+ video_available?: boolean;
137
+ status?: string;
138
+ };
139
+
140
+ const glbUrl = json.glb_url;
141
+ const plyUrl = json.ply_url;
142
 
143
+ if (json.status === "success" && glbUrl && !glbUrl.startsWith("ERROR:")) {
 
144
  return {
145
  status: "success",
146
  glb_url: glbUrl,
147
  ply_url: plyUrl,
148
+ video_available: json.video_available ?? false,
149
  message: "Processing complete!"
150
  };
 
 
 
 
 
151
  }
152
+
153
+ // Error case
154
+ return {
155
+ status: "error",
156
+ error:
157
+ glbUrl || plyUrl || json.status || "Unknown error from API (base64)"
158
+ };
159
  } catch (error) {
160
  console.error("Full error details:", error);
161
  return {
 
193
  export async function completeWorkflow(
194
  imagePaths: string[],
195
  outputDir: string = "./outputs",
196
+ apiBaseUrl?: string,
197
  hfToken?: string
198
  ): Promise<string | null> {
199
  console.log("🚀 Processing images...");
200
 
201
+ const result = await processImages(imagePaths, apiBaseUrl, hfToken);
202
 
203
  if (result.status === "error") {
204
  console.error(`❌ Error: ${result.error}`);
 
241
  console.log("\nExample:");
242
  console.log(" npx ts-node api_client.ts img1.jpg img2.jpg img3.jpg");
243
  console.log("\nEnvironment Variables:");
244
+ console.log(
245
+ " INSTANTSPLAT_API_URL - Base URL of the Space (e.g. https://longh37-InstantSplat.hf.space)"
246
+ );
247
+ console.log(" HF_TOKEN - Hugging Face API token (optional)");
248
  process.exit(1);
249
  }
250
 
251
  const imagePaths = args;
252
+ const apiBaseUrl =
253
+ process.env.INSTANTSPLAT_API_URL || process.env.INSTANTSPLAT_SPACE;
254
  const hfToken = process.env.HF_TOKEN;
255
 
256
  console.log("=".repeat(80));
257
  console.log("InstantSplat API Client (TypeScript)");
258
  console.log("=".repeat(80));
259
 
260
+ const result = await processImages(imagePaths, apiBaseUrl, hfToken);
261
 
262
  console.log("\n" + "=".repeat(80));
263
 
app.py CHANGED
@@ -8,6 +8,9 @@ import uuid
8
  import spaces
9
  import requests
10
  from typing import Optional
 
 
 
11
 
12
  # --- FIX: make gradio compatible by downgrading huggingface_hub -----------
13
  # Gradio 5.0.1 requires huggingface_hub<1.0.0 due to HfFolder import
@@ -454,6 +457,53 @@ def process_api(inputfiles):
454
  }
455
 
456
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  _TITLE = '''InstantSplat'''
458
  _DESCRIPTION = '''
459
  <div style="display: flex; justify-content: center; align-items: center;">
@@ -605,4 +655,21 @@ with block:
605
  label='Sparse-view Examples'
606
  )
607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  block.launch(server_name="0.0.0.0", share=False, show_api=True)
 
8
  import spaces
9
  import requests
10
  from typing import Optional
11
+ import base64
12
+ import tempfile
13
+ from pydantic import BaseModel
14
 
15
  # --- FIX: make gradio compatible by downgrading huggingface_hub -----------
16
  # Gradio 5.0.1 requires huggingface_hub<1.0.0 due to HfFolder import
 
457
  }
458
 
459
 
460
+ def process_base64_api(images_b64):
461
+ """
462
+ API entrypoint that accepts a list of base64-encoded images,
463
+ decodes them to temporary files, and runs the full pipeline.
464
+ """
465
+ # Create a temporary directory under the Gradio cache folder
466
+ tmp_root = os.path.join(GRADIO_CACHE_FOLDER, "api_uploads")
467
+ os.makedirs(tmp_root, exist_ok=True)
468
+ tmp_dir = tempfile.mkdtemp(prefix="api_", dir=tmp_root)
469
+
470
+ decoded_paths = []
471
+ for idx, img_str in enumerate(images_b64):
472
+ if not isinstance(img_str, str):
473
+ continue
474
+
475
+ # Handle optional data URL prefix
476
+ if img_str.startswith("data:"):
477
+ try:
478
+ header, b64_data = img_str.split(",", 1)
479
+ except ValueError:
480
+ b64_data = img_str
481
+ else:
482
+ b64_data = img_str
483
+
484
+ try:
485
+ img_bytes = base64.b64decode(b64_data)
486
+ except Exception:
487
+ # Skip invalid entries
488
+ continue
489
+
490
+ out_path = os.path.join(tmp_dir, f"img_{idx:02d}.jpg")
491
+ with open(out_path, "wb") as f:
492
+ f.write(img_bytes)
493
+ decoded_paths.append(out_path)
494
+
495
+ if len(decoded_paths) < 2:
496
+ return {
497
+ "glb_url": "ERROR: Need at least 2 valid base64 images",
498
+ "ply_url": "ERROR: Need at least 2 valid base64 images",
499
+ "video_available": False,
500
+ "status": "error",
501
+ }
502
+
503
+ # Reuse process_api to run the pipeline and format the response
504
+ return process_api(decoded_paths)
505
+
506
+
507
  _TITLE = '''InstantSplat'''
508
  _DESCRIPTION = '''
509
  <div style="display: flex; justify-content: center; align-items: center;">
 
655
  label='Sparse-view Examples'
656
  )
657
 
658
+
659
+ class Base64Request(BaseModel):
660
+ images: list[str]
661
+
662
+
663
+ fastapi_app = block.app
664
+
665
+
666
+ @fastapi_app.post("/api/base64")
667
+ async def api_base64_endpoint(req: Base64Request):
668
+ """
669
+ FastAPI endpoint that accepts base64-encoded images and returns
670
+ the same JSON structure as process_api/process_base64_api.
671
+ """
672
+ return process_base64_api(req.images)
673
+
674
+
675
  block.launch(server_name="0.0.0.0", share=False, show_api=True)