#!/usr/bin/env ts-node /** * 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 { // 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 = { "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 { 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 { 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 [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); }