// index.js const { onRequest } = require("firebase-functions/v2/https"); const { defineString } = require("firebase-functions/params"); const admin = require("firebase-admin"); const { GoogleGenerativeAI } = require("@google/generative-ai"); // Initialize Firebase Admin admin.initializeApp(); // Define environment parameter for Gemini API key // Set this using: firebase functions:secrets:set GEMINI_API_KEY const GEMINI_API_KEY = defineString("GEMINI_API_KEY"); const MODEL_NAME = "gemini-2.5-flash"; /** * Fetches an image from a public URL and returns base64 + mimeType. */ 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. */ const describe_image_from_url = async (image_url, api_key) => { 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(); }; // Firebase Cloud Function // Call this from your Flutter app // Function name: hum-process-image exports.humprocessimage = onRequest( { cors: true, }, async (req, res) => { 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, GEMINI_API_KEY.value() ); // Parse the JSON response from Gemini // Remove markdown code blocks if present let cleaned_result = result.trim(); if (cleaned_result.startsWith("```json")) { cleaned_result = cleaned_result.replace(/^```json\n/, "").replace(/\n```$/, ""); } else if (cleaned_result.startsWith("```")) { cleaned_result = cleaned_result.replace(/^```\n/, "").replace(/\n```$/, ""); } const parsed_result = JSON.parse(cleaned_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, }); } } );