Initial commit

This commit is contained in:
Yas Opisso
2025-12-12 14:42:43 -05:00
commit b3a57088c1
12 changed files with 10585 additions and 0 deletions

215
index.js Normal file
View File

@@ -0,0 +1,215 @@
// 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 <image_url>");
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;