Spaces:
Paused
Paused
| /** | |
| * InstantSplat API Client (TypeScript) | |
| * | |
| * Submit images to InstantSplat and get back the Supabase GLB URL. | |
| * | |
| * Usage: | |
| * npx ts-node api_client.ts image1.jpg image2.jpg image3.jpg | |
| * | |
| * Environment variables: | |
| * INSTANTSPLAT_API_URL - Base URL of the Space (e.g. https://longh37-InstantSplat.hf.space) | |
| * HF_TOKEN - Hugging Face API token (optional, for private Spaces) | |
| */ | |
| import * as fs from "fs"; | |
| import * as path from "path"; | |
| interface ProcessResult { | |
| status: "success" | "error"; | |
| glb_url?: string; | |
| ply_url?: string; | |
| video_available?: boolean; | |
| error?: string; | |
| message?: string; | |
| } | |
| /** | |
| * Submit images to InstantSplat and get GLB URL. | |
| */ | |
| export async function processImages( | |
| imagePaths: string[], | |
| apiBaseUrl?: string, | |
| hfToken?: string | |
| ): Promise<ProcessResult> { | |
| // Validate inputs | |
| if (imagePaths.length < 2) { | |
| return { | |
| status: "error", | |
| error: "Need at least 2 images (3+ recommended)" | |
| }; | |
| } | |
| // Check if files exist | |
| for (const imagePath of imagePaths) { | |
| if (!fs.existsSync(imagePath)) { | |
| return { | |
| status: "error", | |
| error: `File not found: ${imagePath}` | |
| }; | |
| } | |
| } | |
| try { | |
| // Prepare file info for logging | |
| console.log("Preparing files..."); | |
| imagePaths.forEach((imgPath, index) => { | |
| const stats = fs.statSync(imgPath); | |
| const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2); | |
| const name = path.basename(imgPath); | |
| console.log( | |
| ` File ${index + 1}/${imagePaths.length}: ${name} (${fileSizeMB} MB)` | |
| ); | |
| }); | |
| const totalSizeMB = | |
| imagePaths.reduce((sum, imgPath) => { | |
| return sum + fs.statSync(imgPath).size; | |
| }, 0) / | |
| (1024 * 1024); | |
| console.log( | |
| `✅ All ${imagePaths.length} files ready (total: ${totalSizeMB.toFixed( | |
| 2 | |
| )} MB)` | |
| ); | |
| // Convert files to base64 data URLs | |
| const imagesB64 = imagePaths.map((imgPath) => { | |
| const data = fs.readFileSync(imgPath); | |
| const ext = path.extname(imgPath).toLowerCase(); | |
| let mimeType = "image/jpeg"; | |
| if (ext === ".png") mimeType = "image/png"; | |
| else if (ext === ".jpg" || ext === ".jpeg") mimeType = "image/jpeg"; | |
| else if (ext === ".webp") mimeType = "image/webp"; | |
| const base64 = data.toString("base64"); | |
| return `data:${mimeType};base64,${base64}`; | |
| }); | |
| // Determine API base URL | |
| const baseUrl = | |
| apiBaseUrl || | |
| process.env.INSTANTSPLAT_API_URL || | |
| process.env.INSTANTSPLAT_SPACE || | |
| "https://longh37-InstantSplat.hf.space"; | |
| // Our FastAPI route is mounted at /base64 (not /api/base64) | |
| const url = `${baseUrl.replace(/\/+$/, "")}/base64`; | |
| const token = hfToken || process.env.HF_TOKEN; | |
| console.log(`Connecting to API: ${url}`); | |
| if (token) { | |
| console.log(`Using HF token: ${token.substring(0, 10)}...`); | |
| } | |
| const headers: Record<string, string> = { | |
| "Content-Type": "application/json" | |
| }; | |
| if (token) { | |
| headers["Authorization"] = `Bearer ${token}`; | |
| } | |
| console.log("📤 Uploading images as base64 and starting processing..."); | |
| const startTime = Date.now(); | |
| const resp = await fetch(url, { | |
| method: "POST", | |
| headers, | |
| body: JSON.stringify({ images: imagesB64 }) | |
| }); | |
| const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1); | |
| console.log(`⏱ Server responded in ${elapsedSeconds} seconds`); | |
| if (!resp.ok) { | |
| const text = await resp.text(); | |
| const snippet = | |
| text.length > 400 ? text.slice(0, 400) + "... [truncated]" : text; | |
| console.error( | |
| `❌ HTTP ${resp.status} ${resp.statusText} from API (body snippet): ${snippet}` | |
| ); | |
| return { | |
| status: "error", | |
| error: `HTTP ${resp.status} ${resp.statusText}: ${snippet}` | |
| }; | |
| } | |
| const json = (await resp.json()) as { | |
| glb_url?: string; | |
| ply_url?: string; | |
| video_available?: boolean; | |
| status?: string; | |
| }; | |
| console.log("API response summary:", { | |
| status: json.status, | |
| has_glb_url: !!json.glb_url, | |
| has_ply_url: !!json.ply_url, | |
| video_available: json.video_available ?? false | |
| }); | |
| const glbUrl = json.glb_url; | |
| const plyUrl = json.ply_url; | |
| if (json.status === "success" && glbUrl && !glbUrl.startsWith("ERROR:")) { | |
| return { | |
| status: "success", | |
| glb_url: glbUrl, | |
| ply_url: plyUrl, | |
| video_available: json.video_available ?? false, | |
| message: "Processing complete!" | |
| }; | |
| } | |
| // Error case | |
| return { | |
| status: "error", | |
| error: | |
| glbUrl || plyUrl || json.status || "Unknown error from API (base64)" | |
| }; | |
| } catch (error) { | |
| console.error("Full error details:", error); | |
| return { | |
| status: "error", | |
| error: error instanceof Error ? error.message : String(error) | |
| }; | |
| } | |
| } | |
| /** | |
| * Download a file from a URL. | |
| */ | |
| export async function downloadFile( | |
| url: string, | |
| outputPath: string | |
| ): Promise<void> { | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error( | |
| `Failed to download: ${response.status} ${response.statusText}` | |
| ); | |
| } | |
| const buffer = await response.arrayBuffer(); | |
| fs.writeFileSync(outputPath, Buffer.from(buffer)); | |
| } | |
| /** | |
| * Complete workflow: Upload → Process → Download GLB | |
| */ | |
| export async function completeWorkflow( | |
| imagePaths: string[], | |
| outputDir: string = "./outputs", | |
| apiBaseUrl?: string, | |
| hfToken?: string | |
| ): Promise<string | null> { | |
| console.log("🚀 Processing images..."); | |
| const result = await processImages(imagePaths, apiBaseUrl, hfToken); | |
| if (result.status === "error") { | |
| console.error(`❌ Error: ${result.error}`); | |
| return null; | |
| } | |
| console.log("✅ Processing complete!"); | |
| console.log(` GLB URL: ${result.glb_url}`); | |
| console.log(` PLY URL: ${result.ply_url}`); | |
| // Create output directory | |
| if (!fs.existsSync(outputDir)) { | |
| fs.mkdirSync(outputDir, { recursive: true }); | |
| } | |
| // Download GLB | |
| const glbPath = path.join(outputDir, "model.glb"); | |
| console.log(`📥 Downloading GLB to ${glbPath}...`); | |
| try { | |
| await downloadFile(result.glb_url!, glbPath); | |
| console.log(`✅ Downloaded: ${glbPath}`); | |
| return glbPath; | |
| } catch (error) { | |
| console.error(`❌ Download failed: ${error}`); | |
| return null; | |
| } | |
| } | |
| /** | |
| * CLI interface | |
| */ | |
| async function main() { | |
| const args = process.argv.slice(2); | |
| if (args.length < 2) { | |
| console.log( | |
| "Usage: npx ts-node api_client.ts <image1> <image2> [image3 ...]" | |
| ); | |
| console.log("\nExample:"); | |
| console.log(" npx ts-node api_client.ts img1.jpg img2.jpg img3.jpg"); | |
| console.log("\nEnvironment Variables:"); | |
| console.log( | |
| " INSTANTSPLAT_API_URL - Base URL of the Space (e.g. https://longh37-InstantSplat.hf.space)" | |
| ); | |
| console.log(" HF_TOKEN - Hugging Face API token (optional)"); | |
| process.exit(1); | |
| } | |
| const imagePaths = args; | |
| const apiBaseUrl = | |
| process.env.INSTANTSPLAT_API_URL || process.env.INSTANTSPLAT_SPACE; | |
| const hfToken = process.env.HF_TOKEN; | |
| console.log("=".repeat(80)); | |
| console.log("InstantSplat API Client (TypeScript)"); | |
| console.log("=".repeat(80)); | |
| const result = await processImages(imagePaths, apiBaseUrl, hfToken); | |
| console.log("\n" + "=".repeat(80)); | |
| if (result.status === "success") { | |
| console.log("✅ SUCCESS!"); | |
| console.log("-".repeat(80)); | |
| console.log(`GLB URL: ${result.glb_url}`); | |
| console.log(`PLY URL: ${result.ply_url}`); | |
| if (result.video_available) { | |
| console.log("Video: Available"); | |
| } | |
| console.log("-".repeat(80)); | |
| console.log("\n💡 Tip: You can now download the GLB file:"); | |
| console.log(` curl -o model.glb '${result.glb_url}'`); | |
| console.log("=".repeat(80)); | |
| process.exit(0); | |
| } else { | |
| console.log("❌ ERROR!"); | |
| console.log("-".repeat(80)); | |
| console.log(`Error: ${result.error}`); | |
| console.log("=".repeat(80)); | |
| process.exit(1); | |
| } | |
| } | |
| // Run CLI if this file is executed directly | |
| // Check if file is being run directly (ES module compatible) | |
| const isMainModule = import.meta.url === `file://${process.argv[1]}`; | |
| if (isMainModule) { | |
| main().catch(console.error); | |
| } | |