AIDA / cloudflare-worker /image-upload-worker.js
destinyebuka's picture
fyp
ec34ad8
// src/index.js - Image Upload Worker
// ============================================================
// SUPPORTS:
// 1. Profile pictures (type=profile) - named as {user_id}/profile.jpg
// 2. Property images (type=property) - for listing photos
// ============================================================
// NOTE: AI Vision validation is PAUSED - not currently in use
// ============================================================
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 uploadType = formData.get("type") || "property"; // "profile" or "property"
const userId = formData.get("user_id") || "";
const userName = formData.get("user_name") || ""; // For profile naming
const sessionId = formData.get("session_id") || "";
const userMessage = formData.get("message") || "";
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
const imageBytes = await imageFile.arrayBuffer();
// ============================================================
// AI VISION VALIDATION - PAUSED
// ============================================================
// NOTE: AI Vision validation is currently disabled/paused.
// The HuggingFace vision API is not being used.
// All images are allowed through without property validation.
// To re-enable, uncomment the validation block below.
// ============================================================
/*
// PAUSED: AI Vision Validation Block
let isPropertyImage = false;
let validationReason = "";
try {
const aiResult = await env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', {
image: [...new Uint8Array(imageBytes)],
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) {
console.error("AI validation error:", aiError);
isPropertyImage = true;
validationReason = "AI validation skipped due to error";
}
// If not a property image and type is property, return error
if (!isPropertyImage && uploadType === "property") {
return jsonResponse({
success: false,
error: "not_property_image",
reason: validationReason,
message: userMessage,
user_id: userId,
session_id: sessionId
}, 400);
}
*/
// END PAUSED BLOCK
// ============================================================
// DETERMINE IMAGE NAME
// ============================================================
let imageName = "";
if (uploadType === "profile") {
// Profile pictures: {user_id}/profile or {user_name}/profile
const identifier = userName || userId || `user_${Date.now()}`;
const cleanIdentifier = identifier
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_|_$/g, '');
imageName = `${cleanIdentifier}_profile`;
} else if (operation === "add") {
// Property images: get name from AIDA or use timestamp
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()}`;
}
}
// ============================================================
// 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
}
}
// ============================================================
// UPLOAD TO CLOUDFLARE IMAGES
// ============================================================
// Clean the image name for use as filename
const cleanName = imageName
.toLowerCase()
.replace(/[^a-z0-9_]+/g, '-')
.replace(/^-|-$/g, '')
|| `image-${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
return jsonResponse({
success: true,
id: imageId,
url: imageUrl,
filename: cleanName,
type: uploadType,
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": "*"
}
});
}