InstantSplat / api_client.ts
Long Hoang
attempt api fix
d1eac9d
#!/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<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);
}