AIDA / cloudflare-worker /image-upload-worker.js
destinyebuka's picture
fyp
8c9362b
// src/index.js - Updated Image Upload Worker with AI Vision Validation
// Features:
// 1. AI Vision validation (is this a property photo?)
// 2. Get image name from AIDA
// 3. Handle add/replace operations
// 4. Duplicate name numbering
export default {
async fetch(request, env) {
// CORS preflight
if (request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "*"
}
});
}
if (request.method !== "POST") {
return new Response("Only POST allowed", {
status: 405,
headers: { "Access-Control-Allow-Origin": "*" }
});
}
const contentType = request.headers.get("Content-Type") || "";
if (!contentType.includes("multipart/form-data")) {
return new Response("Unsupported Media Type", {
status: 415,
headers: { "Access-Control-Allow-Origin": "*" }
});
}
const contentLength = request.headers.get("content-length");
if (contentLength && parseInt(contentLength) > 5 * 1024 * 1024) {
return new Response("Image too large. Max 5MB.", {
status: 400,
headers: { "Access-Control-Allow-Origin": "*" }
});
}
// Configuration - MOVE THESE TO ENVIRONMENT VARIABLES IN PRODUCTION
const ACCOUNT_ID = env.CF_ACCOUNT_ID || "375f7ecab9677eb4e6362cbed2e09358";
const API_TOKEN = env.CF_API_TOKEN || "Kyx_WSRHASBktvx6GbI5W-HRxPiBuO6J867uegXw";
const ACCOUNT_HASH = env.CF_ACCOUNT_HASH || "0utJlkqgAVuawL5OpMWxgw";
const AIDA_BASE_URL = env.AIDA_BASE_URL || "https://destinyebuka-aida.hf.space";
try {
// Parse form data
const formData = await request.formData();
const imageFile = formData.get("file");
const userMessage = formData.get("message") || "";
const userId = formData.get("user_id") || "";
const sessionId = formData.get("session_id") || "";
const operation = formData.get("operation") || "add"; // "add" or "replace"
const replaceIndex = formData.get("replace_index"); // For replace operations
const existingImageId = formData.get("existing_image_id"); // ID of image to replace
if (!imageFile) {
return jsonResponse({ success: false, error: "no_image", message: "No image file provided" }, 400);
}
// Convert image to bytes for AI validation
const imageBytes = await imageFile.arrayBuffer();
const imageArray = [...new Uint8Array(imageBytes)];
// ============================================================
// STEP 1: AI Vision Validation
// ============================================================
let isPropertyImage = false;
let validationReason = "";
try {
const aiResult = await env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', {
image: imageArray,
prompt: "Is this image showing a real estate property such as a house, apartment, room, building, or property exterior/interior? Answer with ONLY 'YES' or 'NO' followed by a brief reason.",
max_tokens: 50
});
const response = aiResult.description || aiResult.response || String(aiResult);
isPropertyImage = response.toUpperCase().includes("YES");
validationReason = response;
} catch (aiError) {
// If AI fails, allow the image through (fail-open for better UX)
console.error("AI validation error:", aiError);
isPropertyImage = true;
validationReason = "AI validation skipped due to error";
}
// If not a property image, return error (AIDA will handle friendly message)
if (!isPropertyImage) {
return jsonResponse({
success: false,
error: "not_property_image",
reason: validationReason,
message: userMessage,
user_id: userId,
session_id: sessionId
}, 400);
}
// ============================================================
// STEP 2: Get Image Name from AIDA (if new image)
// ============================================================
let imageName = "";
if (operation === "add") {
try {
const nameResponse = await fetch(`${AIDA_BASE_URL}/ai/get-image-name`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_id: userId,
session_id: sessionId
})
});
if (nameResponse.ok) {
const nameData = await nameResponse.json();
imageName = nameData.name || `property-${Date.now()}`;
} else {
imageName = `property-${Date.now()}`;
}
} catch (nameError) {
console.error("Failed to get image name from AIDA:", nameError);
imageName = `property-${Date.now()}`;
}
}
// ============================================================
// STEP 3: Handle Replace Operation (delete old image)
// ============================================================
if (operation === "replace" && existingImageId) {
try {
// Get existing image name before deleting
const listResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/images/v1/${existingImageId}`,
{
method: "GET",
headers: { Authorization: `Bearer ${API_TOKEN}` }
}
);
if (listResponse.ok) {
const existingData = await listResponse.json();
if (existingData.result?.filename) {
imageName = existingData.result.filename.replace(/\.[^/.]+$/, ""); // Remove extension
}
}
// Delete old image
await fetch(
`https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/images/v1/${existingImageId}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${API_TOKEN}` }
}
);
} catch (deleteError) {
console.error("Failed to delete old image:", deleteError);
// Continue with upload anyway
}
}
// ============================================================
// STEP 4: Upload to Cloudflare Images
// ============================================================
// Clean the image name for use as filename
const cleanName = imageName
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
|| `property-${Date.now()}`;
// Create new FormData for Cloudflare upload
const uploadFormData = new FormData();
uploadFormData.append("file", new Blob([imageBytes], { type: imageFile.type }), `${cleanName}.jpg`);
const cloudflareUploadURL = `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/images/v1`;
const uploadResponse = await fetch(cloudflareUploadURL, {
method: "POST",
headers: { Authorization: `Bearer ${API_TOKEN}` },
body: uploadFormData
});
const uploadResponseBody = await uploadResponse.json();
if (uploadResponse.ok && uploadResponseBody.success && uploadResponseBody.result) {
const imageId = uploadResponseBody.result.id;
const imageUrl = `https://imagedelivery.net/${ACCOUNT_HASH}/${imageId}/public`;
// Return success with all context for AIDA
return jsonResponse({
success: true,
id: imageId,
url: imageUrl,
filename: cleanName,
message: userMessage,
operation: operation,
replace_index: replaceIndex,
user_id: userId,
session_id: sessionId
}, 200);
} else {
const errorDetails = uploadResponseBody.errors || [];
return jsonResponse({
success: false,
error: "upload_failed",
details: errorDetails,
message: userMessage,
user_id: userId,
session_id: sessionId
}, uploadResponse.status);
}
} catch (error) {
console.error("Worker error:", error);
return jsonResponse({
success: false,
error: "worker_error",
details: error.message
}, 500);
}
}
};
// Helper function for JSON responses
function jsonResponse(data, status) {
return new Response(JSON.stringify(data), {
status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
});
}