Skip to content

Commit 5ea1fe4

Browse files
Merge pull request #35 from stefanbobrowski/feature/mvp
Feature/mvp - Vertex 2.5 update
2 parents 21ff38a + 5abb840 commit 5ea1fe4

File tree

23 files changed

+516
-516
lines changed

23 files changed

+516
-516
lines changed

backend/routes/analyze-image.js

Lines changed: 72 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,116 @@
11
const express = require("express");
22
const multer = require("multer");
33
const fs = require("fs");
4-
const fetch = require("node-fetch");
54
const vision = require("@google-cloud/vision");
5+
const { VertexAI } = require("@google-cloud/vertexai");
66
const { verifyRecaptcha } = require("../helpers/verifyRecaptcha");
77

88
const router = express.Router();
9-
const upload = multer({ dest: "uploads/" });
109

11-
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
12-
const RECAPTCHA_SECRET_KEY = process.env.RECAPTCHA_SECRET_KEY;
13-
14-
if (!GEMINI_API_KEY) throw new Error("Gemini API key is required.");
15-
if (!RECAPTCHA_SECRET_KEY) throw new Error("reCAPTCHA secret key is required.");
10+
// Multer config
11+
const upload = multer({
12+
dest: "uploads/",
13+
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB max
14+
});
1615

1716
const visionClient = new vision.ImageAnnotatorClient();
17+
const vertexAI = new VertexAI({
18+
project: process.env.GCLOUD_PROJECT,
19+
location: process.env.GCLOUD_LOCATION || "us-central1",
20+
});
1821

19-
router.post("/", upload.single("image"), async (req, res) => {
20-
const userPrompt = req.body.prompt || "Describe this image.";
21-
const prompt = `Respond briefly: ${userPrompt} (Limit your answer to one short sentence.)`;
22-
const cleanPrompt = prompt.trim().replace(/[^a-zA-Z0-9 ?.,!"()\-]/g, "");
23-
const recaptchaToken = req.body.recaptchaToken;
24-
25-
if (!recaptchaToken) {
26-
return res.status(400).json({ error: "Missing reCAPTCHA token." });
22+
// Function-calling schema
23+
const analyzeImageSchema = {
24+
name: "analyze_image",
25+
description: "Analyze an image and return a short structured description.",
26+
parameters: {
27+
type: "object",
28+
properties: {
29+
description: {
30+
type: "string",
31+
description: "A short, safe description of the image content."
32+
}
33+
},
34+
required: ["description"]
2735
}
36+
};
2837

29-
if (!req.file) {
30-
return res.status(400).json({ error: "Image file is required." });
31-
}
38+
router.post("/", upload.single("image"), async (req, res) => {
39+
const prompt = (req.body.prompt || "Describe this image.").trim().slice(0, 300);
40+
const recaptchaToken = req.body.recaptchaToken;
3241

33-
if (!cleanPrompt || cleanPrompt.length > 300) {
34-
return res.status(400).json({ error: "Prompt must be 1–300 characters." });
35-
}
42+
if (!recaptchaToken) return res.status(400).json({ error: "Missing reCAPTCHA token." });
43+
if (!req.file) return res.status(400).json({ error: "Image file is required." });
3644

37-
// reCAPTCHA verification
45+
// Verify reCAPTCHA
3846
try {
39-
const recaptchaResult = await verifyRecaptcha(recaptchaToken);
40-
41-
const score = recaptchaResult.score ?? 0;
42-
const success = recaptchaResult.success === true;
43-
47+
const { success, score } = await verifyRecaptcha(recaptchaToken);
4448
if (!success || score < 0.5) {
45-
console.warn("⚠️ reCAPTCHA verification failed", {
46-
ip: req.ip,
47-
score,
48-
success,
49-
});
5049
return res.status(403).json({ error: "reCAPTCHA verification failed." });
5150
}
5251
} catch (err) {
53-
console.error("Error verifying reCAPTCHA:", err);
52+
console.error("reCAPTCHA error:", err);
5453
return res.status(500).json({ error: "Failed to verify reCAPTCHA." });
5554
}
5655

5756
const imagePath = req.file.path;
5857

5958
try {
60-
// NSFW filtering using Cloud Vision SafeSearch
59+
// ✅ Content safety via Vision API
6160
const [result] = await visionClient.safeSearchDetection(imagePath);
6261
const safe = result.safeSearchAnnotation;
63-
6462
if (
65-
safe.adult === "LIKELY" ||
66-
safe.adult === "VERY_LIKELY" ||
67-
safe.violence === "LIKELY" ||
68-
safe.violence === "VERY_LIKELY" ||
63+
["LIKELY", "VERY_LIKELY"].includes(safe.adult) ||
64+
["LIKELY", "VERY_LIKELY"].includes(safe.violence) ||
6965
safe.racy === "VERY_LIKELY"
7066
) {
71-
console.warn("Blocked NSFW image:", safe);
72-
return res
73-
.status(403)
74-
.json({ error: "Image flagged as unsafe by content filter." });
67+
return res.status(403).json({ error: "Image flagged as unsafe." });
7568
}
7669

77-
const imageBuffer = fs.readFileSync(imagePath);
78-
const base64Image = imageBuffer.toString("base64");
79-
80-
const geminiRes = await fetch(
81-
`https://generativelanguage.googleapis.com/v1/models/gemini-1.5-pro:generateContent?key=${GEMINI_API_KEY}`,
82-
{
83-
method: "POST",
84-
headers: { "Content-Type": "application/json" },
85-
body: JSON.stringify({
86-
contents: [
87-
{
88-
parts: [
89-
{
90-
inline_data: {
91-
mime_type: req.file.mimetype,
92-
data: base64Image,
93-
},
94-
},
95-
{
96-
text: cleanPrompt,
97-
},
98-
],
99-
},
70+
// ✅ Read + encode
71+
const base64Image = fs.readFileSync(imagePath).toString("base64");
72+
73+
// ✅ Vertex AI with function calling
74+
const model = vertexAI.getGenerativeModel({
75+
model: "gemini-2.5-flash",
76+
tools: [{ functionDeclarations: [analyzeImageSchema] }],
77+
});
78+
79+
const resultAI = await model.generateContent({
80+
contents: [
81+
{
82+
role: "user",
83+
parts: [
84+
{ inline_data: { mime_type: req.file.mimetype, data: base64Image } },
85+
{ text: prompt },
10086
],
101-
}),
87+
},
88+
],
89+
toolConfig: {
90+
functionCallingConfig: {
91+
mode: "ANY", // Force Gemini to pick a function instead of free text
92+
},
10293
},
103-
);
104-
105-
if (!geminiRes.ok) {
106-
const errorData = await geminiRes.json();
107-
console.error("Gemini API error:", errorData);
108-
return res.status(geminiRes.status).json({
109-
error: errorData.error?.message || "Unknown error from Gemini API.",
110-
});
111-
}
94+
});
11295

113-
const data = await geminiRes.json();
114-
console.log("Gemini API response:", JSON.stringify(data, null, 2));
96+
// ✅ Extract function call
97+
const fnCall = resultAI.response?.candidates?.[0]?.content?.parts?.find(
98+
(p) => p.functionCall
99+
)?.functionCall;
115100

116-
const responseText = data.candidates?.length
117-
? data.candidates[0].content?.parts?.[0]?.text ||
118-
"Response format unexpected."
119-
: "No candidates returned from Gemini.";
101+
if (!fnCall || !fnCall.args) {
102+
console.error("❌ No structured functionCall:", JSON.stringify(resultAI, null, 2));
103+
return res.status(500).json({ error: "No structured description returned." });
104+
}
120105

121-
res.json({ response: responseText });
106+
// ✅ Clean, structured result
107+
const response = fnCall.args;
108+
res.json({ response });
122109
} catch (err) {
123-
console.error("Error analyzing image:", err);
110+
console.error("Image analysis error:", err);
124111
res.status(500).json({ error: "Error analyzing image." });
125112
} finally {
126-
if (fs.existsSync(imagePath)) {
127-
fs.unlinkSync(imagePath); // Cleanup temp file
128-
}
113+
if (fs.existsSync(imagePath)) fs.unlinkSync(imagePath); // cleanup temp upload
129114
}
130115
});
131116

backend/routes/analyze-text.js

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,77 @@
11
const express = require("express");
22
const { Storage } = require("@google-cloud/storage");
3-
const { GoogleGenerativeAI } = require("@google/generative-ai");
3+
const { VertexAI } = require("@google-cloud/vertexai");
44
const { verifyRecaptcha } = require("../helpers/verifyRecaptcha");
55

66
const router = express.Router();
77
const storage = new Storage();
8-
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
9-
108
const bucketName = "upload-center-bucket";
119

10+
const vertexAI = new VertexAI({
11+
project: process.env.GCLOUD_PROJECT,
12+
location: process.env.GCLOUD_LOCATION || "us-central1",
13+
});
14+
1215
router.post("/", async (req, res) => {
1316
const { gcsUrl } = req.body;
1417
const recaptchaToken = req.headers["x-recaptcha-token"];
1518

16-
if (!gcsUrl) {
17-
return res.status(400).json({ error: "Missing GCS URL." });
18-
}
19-
20-
if (!recaptchaToken) {
21-
return res.status(400).json({ error: "Missing reCAPTCHA token." });
22-
}
19+
if (!gcsUrl) return res.status(400).json({ error: "Missing GCS URL." });
20+
if (!recaptchaToken) return res.status(400).json({ error: "Missing reCAPTCHA token." });
2321

24-
// reCAPTCHA verification
22+
// reCAPTCHA check
2523
try {
26-
const recaptchaResult = await verifyRecaptcha(recaptchaToken);
27-
28-
const score = recaptchaResult.score ?? 0;
29-
const success = recaptchaResult.success === true;
30-
24+
const { success, score } = await verifyRecaptcha(recaptchaToken);
3125
if (!success || score < 0.5) {
32-
console.warn("⚠️ reCAPTCHA verification failed", {
33-
ip: req.ip,
34-
score,
35-
success,
36-
});
3726
return res.status(403).json({ error: "reCAPTCHA verification failed." });
3827
}
3928
} catch (err) {
40-
console.error("Error verifying reCAPTCHA:", err);
29+
console.error("reCAPTCHA error:", err);
4130
return res.status(500).json({ error: "Failed to verify reCAPTCHA." });
4231
}
4332

4433
try {
45-
// Extract file contents
34+
// Download text file from GCS
4635
const filename = decodeURIComponent(gcsUrl.split("/").pop());
47-
const file = storage
48-
.bucket(bucketName)
49-
.file(`uploads/text-files/${filename}`);
36+
const file = storage.bucket(bucketName).file(`uploads/text-files/${filename}`);
5037
const [contents] = await file.download();
5138
const text = contents.toString("utf8");
5239

53-
// Analyze with Gemini
54-
const model = genAI.getGenerativeModel({ model: "gemini-1.5-pro-latest" });
40+
const MAX_CHARS = 7000;
41+
const safeText = text.slice(0, MAX_CHARS);
42+
43+
// Vertex function-calling with enforced JSON
44+
const model = vertexAI.getGenerativeModel({
45+
model: "gemini-2.5-flash",
46+
generationConfig: { responseMimeType: "application/json" },
47+
});
5548

5649
const result = await model.generateContent({
5750
contents: [
5851
{
5952
role: "user",
60-
parts: [{ text: `Summarize this text in 2-3 sentences:\n\n${text}` }],
53+
parts: [
54+
{
55+
text: `Summarize the following text into 2–3 sentences.
56+
Return only JSON with this shape:
57+
{ "summary": string }
58+
59+
Text:
60+
${safeText}`,
61+
},
62+
],
6163
},
6264
],
6365
});
6466

65-
const responseText = result.response.text();
67+
// ✅ Pull structured JSON directly from response
68+
const raw = result.response?.candidates?.[0]?.content?.parts?.[0]?.text || "{}";
69+
const parsed = JSON.parse(raw);
6670

67-
res.json({ result: responseText });
71+
res.json({ summary: parsed.summary });
6872
} catch (err) {
69-
console.error("Vertex AI analysis failed:", err);
70-
res.status(500).json({ error: "Vertex AI analysis failed." });
73+
console.error("Vertex AI text analysis failed:", err);
74+
res.status(500).json({ error: "Text analysis failed." });
7175
}
7276
});
7377

0 commit comments

Comments
 (0)