173 lines
7.6 KiB
JavaScript
173 lines
7.6 KiB
JavaScript
/**
|
|
* ============================================================================
|
|
* WSC2026 Web Technologies - Test Project: AI Middleware Server
|
|
* ============================================================================
|
|
* 🚨 [Competitor Notice]
|
|
* You do NOT need to modify this file. This is a black-box AI microservice.
|
|
* Your task is to run this server, test the APIs, and build your own
|
|
* Front-end and Back-end (Database) application that communicates with it.
|
|
* ============================================================================
|
|
*/
|
|
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const multer = require('multer');
|
|
|
|
const app = express();
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// Use memory storage (Converts image buffer directly to Base64)
|
|
const upload = multer({ storage: multer.memoryStorage() });
|
|
const OLLAMA_API = 'http://localhost:11434/api/generate';
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// API 1: Text AI (Generate City Description and Country Info)
|
|
// ----------------------------------------------------------------------------
|
|
app.post('/api/generate-text', async (req, res) => {
|
|
const { city_name, country } = req.body;
|
|
if (!city_name || !country) {
|
|
return res.status(400).json({ error: 'city_name and country are required.' });
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(OLLAMA_API, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'phi3',
|
|
// 🚀 Advanced Prompt: Request city description and country info in JSON format
|
|
prompt: `Provide information about ${city_name} in ${country}.
|
|
Return a JSON object with exactly two keys:
|
|
1. "city_description": A 2-sentence tourist introduction for the city.
|
|
2. "country_info": One interesting or essential fact about the country ${country}.
|
|
DO NOT output any explanations. JUST the JSON object.`,
|
|
format: 'json',
|
|
stream: false
|
|
})
|
|
});
|
|
const data = await response.json();
|
|
const rawText = data.response;
|
|
|
|
console.log(`[Text API] AI Original Response for ${city_name}, ${country}:`, rawText);
|
|
|
|
// --- 🛡️ Text Hallucination Defense Logic ---
|
|
let cityDescription = "Failed to generate city description. Please enter manually.";
|
|
let countryInfo = "Failed to generate country information. Please enter manually.";
|
|
|
|
try {
|
|
const parsed = JSON.parse(rawText);
|
|
cityDescription = parsed.city_description || cityDescription;
|
|
countryInfo = parsed.country_info || countryInfo;
|
|
} catch (e) {
|
|
// Fallback: Use regular expressions if JSON parsing fails
|
|
console.warn("[Text API] JSON parsing failed. Attempting regex extraction.");
|
|
const descMatch = rawText.match(/"city_description"\s*:\s*"([^"]+)"/i);
|
|
if (descMatch) cityDescription = descMatch[1];
|
|
const infoMatch = rawText.match(/"country_info"\s*:\s*"([^"]+)"/i);
|
|
if (infoMatch) countryInfo = infoMatch[1];
|
|
}
|
|
|
|
res.json({
|
|
city_description: cityDescription,
|
|
country_info: countryInfo
|
|
});
|
|
} catch (error) {
|
|
console.error("[Text API] Error:", error);
|
|
res.status(500).json({ error: 'Text AI processing failed. Ensure Ollama is running.' });
|
|
}
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// API 2: Vision AI (Integrated Image Analysis - Description and Keywords)
|
|
// ----------------------------------------------------------------------------
|
|
app.post('/api/analyze-image', upload.single('image'), async (req, res) => {
|
|
const file = req.file;
|
|
const { city_name, country } = req.body;
|
|
|
|
if (!file || !city_name || !country) {
|
|
return res.status(400).json({ error: 'image, city_name, and country are required.' });
|
|
}
|
|
|
|
try {
|
|
const base64Image = file.buffer.toString('base64');
|
|
const response = await fetch(OLLAMA_API, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'llava',
|
|
prompt: `Analyze this image of ${city_name}, ${country}.
|
|
Return a JSON object with:
|
|
1. "description": A short 1-2 sentence description of the image.
|
|
2. "keywords": An array of exactly 3 descriptive words.
|
|
JUST the JSON object. DO NOT output any markdown, tags, or extra text.`,
|
|
images: [base64Image],
|
|
format: 'json',
|
|
stream: false
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
const rawAiText = data.response;
|
|
|
|
console.log(`[Vision API] AI Original Response for ${city_name}, ${country}:`, rawAiText);
|
|
|
|
// --- 🛡️ Hallucination Defense & Data Sanitization Logic ---
|
|
let finalDescription = "No description generated. Please enter manually.";
|
|
let finalKeywords = [];
|
|
|
|
try {
|
|
// Attempt 1: Force extract inside curly braces {} in case AI adds markdown wrappers
|
|
const jsonMatch = rawAiText.match(/\{[\s\S]*\}/);
|
|
const targetText = jsonMatch ? jsonMatch[0] : rawAiText;
|
|
const parsed = JSON.parse(targetText);
|
|
|
|
if (parsed.description) finalDescription = parsed.description;
|
|
if (parsed.keywords) {
|
|
if (Array.isArray(parsed.keywords)) {
|
|
finalKeywords = parsed.keywords;
|
|
} else if (typeof parsed.keywords === 'string') {
|
|
// Split into an array if keywords arrive as a comma-separated string
|
|
finalKeywords = parsed.keywords.split(',');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn("[Vision API] JSON parsing failed. Attempting regex fallback.");
|
|
|
|
// Attempt fallback extraction for description
|
|
const descMatch = rawAiText.match(/"description"\s*:\s*"([^"]+)"/i);
|
|
if (descMatch) finalDescription = descMatch[1];
|
|
|
|
// Attempt fallback extraction for keywords array
|
|
const arrMatch = rawAiText.match(/\[(.*?)\]/s);
|
|
if (arrMatch) finalKeywords = arrMatch[1].replace(/["']/g, '').split(',');
|
|
}
|
|
|
|
// Sanitize the keywords array (trim, remove brackets/quotes, filter empty, limit to 3)
|
|
finalKeywords = finalKeywords
|
|
.map(w => w.trim().replace(/[{}"\[\]]/g, ''))
|
|
.filter(w => w.length > 0)
|
|
.slice(0, 3);
|
|
|
|
if (finalKeywords.length === 0) {
|
|
finalKeywords = ["analysis_failed", "manual_input_required", "error"];
|
|
}
|
|
|
|
// Finally, return the description (Text) and keywords (Stringified Array)
|
|
res.json({
|
|
description: finalDescription,
|
|
keywords: JSON.stringify(finalKeywords)
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error("[Vision API] Error:", error);
|
|
res.status(500).json({ error: 'Vision AI processing failed. Ensure Ollama is running.' });
|
|
}
|
|
});
|
|
|
|
const PORT = 3000;
|
|
app.listen(PORT, () => {
|
|
console.log(`🚀 AI Middleware Server running on http://localhost:${PORT}`);
|
|
console.log(`- Text API: POST http://localhost:${PORT}/api/generate-text`);
|
|
console.log(`- Vision API: POST http://localhost:${PORT}/api/analyze-image`);
|
|
}); |