Spaces:
Running
Running
| // 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": "*" | |
| } | |
| }); | |
| } | |