// index.js const functions = require("firebase-functions"); const admin = require("firebase-admin"); const { GoogleGenerativeAI } = require("@google/generative-ai"); // Load .env only for local testing if (process.env.NODE_ENV !== "production") { require("dotenv").config(); } // Initialize Firebase Admin // In production (Firebase Functions), this uses default credentials // For local testing, it uses the service account file if (!admin.apps.length) { if (process.env.NODE_ENV !== "production") { const serviceAccount = require("./firebase-service-account.json"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), }); } else { admin.initializeApp(); } } // Get API key from environment // In Firebase Functions, set this using: firebase functions:config:set gemini.api_key="YOUR_KEY" // For local testing, use .env file const api_key = process.env.GOOGLE_AI_API_KEY || functions.config().gemini?.api_key; const MODEL_NAME = "gemini-2.5-flash"; /** * Fetches an image from a public URL and returns base64 + mimeType. * This helper can be reused inside a Firebase Function later. */ const fetch_image_as_base64 = async (image_url) => { const response = await fetch(image_url); if (!response.ok) { throw new Error( `Failed to fetch image: ${response.status} ${response.statusText}`, ); } const array_buffer = await response.arrayBuffer(); const base64_data = Buffer.from(array_buffer).toString("base64"); const mime_type = response.headers.get("content-type") || "image/jpeg"; return { base64_data, mime_type }; }; /** * Fetches categories from Firestore and returns them in a format * that Gemini can use to assign a category ID. * Only returns categories where visible = true. */ const fetch_categories_from_firestore = async () => { const db = admin.firestore(); const categories_snapshot = await db .collection("categories") .where("visible", "==", true) .get(); const categories = []; categories_snapshot.forEach((doc) => { let cat_data = doc.data(); cat_data.id = doc.id; categories.push(cat_data); }); return categories; }; /** * Main logic: takes an image URL, calls Gemini, and returns the description text. * This function is what we'll later call from Firebase Functions. */ const describe_image_from_url = async (image_url) => { if (!api_key) { throw new Error("Missing Gemini API key"); } // Initialize Gemini const gen_ai = new GoogleGenerativeAI(api_key); const model = gen_ai.getGenerativeModel({ model: MODEL_NAME }); // Fetch categories from Firestore const categories = await fetch_categories_from_firestore(); // Format categories for the prompt const categories_list = categories .map((cat) => `${cat.id}: ${cat.name}`) .join(", "); const { base64_data, mime_type } = await fetch_image_as_base64(image_url); const prompt = `Analyze this rental item for marketplace listing with value estimation. Return raw JSON only, no markdown: { "title": "Clear, concise title (max 50 chars)", "description": "Detailed description (2-3 sentences)", "categoryId": "Category ID from this list: [${categories_list}]. If none match, leave this field as an empty string", "condition": "One of: New, Like New, Good, Fair, Poor", "suggestedDailyPrice": 15.99, "suggestedWeeklyPrice": 89.99, "suggestedMonthlyPrice": 299.99, "estimatedValue": 599.99, "brand": "Brand name if identifiable", "model": "Model if identifiable", "features": ["Key feature 1", "Feature 2", "Feature 3"], "targetAudience": ["Students", "Professionals", "Families"], "seasonality": "One of: Year-round, Summer, Winter, Spring, Fall, Holiday", "insuranceRecommended": true, "depositSuggestion": 100.00 }`; const result = await model.generateContent({ contents: [ { role: "user", parts: [ { text: prompt }, { inlineData: { data: base64_data, mimeType: mime_type, }, }, ], }, ], }); return result.response.text(); }; /** * CLI entrypoint for local testing: * node index.js https://example.com/image.jpg */ const main = async () => { const image_url = process.argv[2]; if (!image_url) { console.error("Usage: node index.js "); process.exit(1); } try { // First, fetch categories from Firestore console.log("Fetching categories from Firestore..."); const categories = await fetch_categories_from_firestore(); console.log("Categories found:", categories); console.log(""); console.log("Fetching and describing image from:", image_url); const description = await describe_image_from_url(image_url); console.log("\n=== GEMINI DESCRIPTION ===\n"); console.log(description); console.log("\n==========================\n"); } catch (err) { console.error("Error describing image:", err); } }; // Run main only if called directly: `node index.js ...` if (require.main === module) { main(); } // Firebase Cloud Function // Call this from your Flutter app exports.analyzeRentalItem = functions.https.onRequest(async (req, res) => { // Enable CORS res.set("Access-Control-Allow-Origin", "*"); if (req.method === "OPTIONS") { res.set("Access-Control-Allow-Methods", "POST"); res.set("Access-Control-Allow-Headers", "Content-Type"); res.set("Access-Control-Max-Age", "3600"); return res.status(204).send(""); } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } try { const { imageUrl } = req.body; if (!imageUrl) { return res.status(400).json({ error: "Missing imageUrl in request body" }); } const result = await describe_image_from_url(imageUrl); // Parse the JSON response from Gemini const parsed_result = JSON.parse(result); return res.status(200).json({ success: true, data: parsed_result, }); } catch (error) { console.error("Error analyzing rental item:", error); return res.status(500).json({ success: false, error: error.message, }); } }); // Export helper function for local testing module.exports.describe_image_from_url = describe_image_from_url;