143 lines
4.8 KiB
TypeScript
143 lines
4.8 KiB
TypeScript
import express from "express";
|
|
import path from "path";
|
|
import { createServer as createViteServer } from "vite";
|
|
import { GoogleGenAI } from "@google/genai";
|
|
import { defaultMLProvider } from "./src/lib/mlProvider";
|
|
import { Pet } from "./src/types";
|
|
import { authMiddleware } from "./src/auth/middleware";
|
|
import authRouter from "./src/auth/routes";
|
|
|
|
async function startServer() {
|
|
const app = express();
|
|
const PORT = 3000;
|
|
|
|
app.use(express.json({ limit: "10mb" }));
|
|
|
|
// Auth middleware — protects /api/* except register, login, health
|
|
app.use(authMiddleware);
|
|
|
|
// Auth routes (register, login, me)
|
|
app.use("/api", authRouter);
|
|
|
|
// API Routes
|
|
app.get("/api/health", (_req, res) => {
|
|
res.json({ status: "ok", app: "PetTalk", timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
/**
|
|
* Analyze Pet Signal API endpoint.
|
|
* Leverages Gemini API when GEMINI_API_KEY is available, or uses local ML Provider.
|
|
*/
|
|
app.post("/api/analyze", async (req, res) => {
|
|
try {
|
|
const { pet, selectedZoneId, audio, video, customNote } = req.body as {
|
|
pet: Pet;
|
|
selectedZoneId?: string;
|
|
audio?: any;
|
|
video?: any;
|
|
customNote?: string;
|
|
};
|
|
|
|
if (!pet) {
|
|
return res.status(400).json({ error: "Pet profile is required for analysis." });
|
|
}
|
|
|
|
// Base ML prediction
|
|
const baseResult = defaultMLProvider.combineSignals({
|
|
pet,
|
|
timeOfDay: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
|
selectedZoneId,
|
|
audio,
|
|
video,
|
|
customNote
|
|
});
|
|
|
|
// If GEMINI_API_KEY is configured, enrich interpretation reasoning with Gemini
|
|
if (process.env.GEMINI_API_KEY) {
|
|
try {
|
|
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
const zoneObj = pet.homeZones.find(z => z.id === selectedZoneId) || pet.homeZones[0];
|
|
const zoneName = zoneObj ? zoneObj.name : "Home Area";
|
|
|
|
const prompt = `You are a certified veterinary behaviorist and pet communication expert assisting a pet owner on PetTalk.
|
|
Pet Name: ${pet.name}
|
|
Species: ${pet.species}
|
|
Breed: ${pet.breed}
|
|
Age: ${pet.ageYears} years old
|
|
Personality: ${pet.personality.join(", ")}
|
|
Feeding Schedule: ${pet.feedingSchedule}
|
|
Current Home Zone: ${zoneName}
|
|
Acoustic Trait Detected: ${audio?.pitchVariation || 'rising pitch'} sound (${audio?.averageVolume ? Math.round(audio.averageVolume * 100) : 50}% volume)
|
|
Primary Predicted Intent: ${baseResult.primaryIntent.title} (${baseResult.primaryIntent.confidence}% confidence)
|
|
|
|
Write a short, empathetic, scientifically grounded "Why" reasoning breakdown (maximum 3 sentences).
|
|
Follow this STRICT honesty rule:
|
|
- Frame everything as a probabilistic likelihood based on sound, body language, routine, location, and owner feedback.
|
|
- NEVER claim pets speak English or that this is a literal translation.
|
|
- Maintain a warm, modern, intelligent tone.
|
|
- Do NOT make medical diagnoses or claims.`;
|
|
|
|
const response = await ai.models.generateContent({
|
|
model: "gemini-2.5-flash",
|
|
contents: prompt
|
|
});
|
|
|
|
if (response.text) {
|
|
baseResult.reasoning.summary = response.text.trim();
|
|
}
|
|
} catch (geminiErr) {
|
|
console.warn("Gemini API enrichment skipped/failed, using ML provider output:", geminiErr);
|
|
}
|
|
}
|
|
|
|
return res.json({ success: true, result: baseResult });
|
|
} catch (err: any) {
|
|
console.error("Error in /api/analyze:", err);
|
|
return res.status(500).json({ error: "Failed to analyze pet signal", details: err?.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Train Pet Profile Feedback API endpoint
|
|
*/
|
|
app.post("/api/train", (req, res) => {
|
|
try {
|
|
const { pet, feedback, correctedCategory } = req.body as {
|
|
pet: Pet;
|
|
feedback: 'confirmed' | 'corrected';
|
|
correctedCategory?: string;
|
|
};
|
|
|
|
if (!pet || !feedback) {
|
|
return res.status(400).json({ error: "Pet and feedback are required." });
|
|
}
|
|
|
|
const updatedPet = defaultMLProvider.trainPetProfile(pet, feedback, correctedCategory);
|
|
return res.json({ success: true, pet: updatedPet });
|
|
} catch (err: any) {
|
|
return res.status(500).json({ error: "Failed to process training feedback" });
|
|
}
|
|
});
|
|
|
|
// Vite development vs production setup
|
|
if (process.env.NODE_ENV !== "production") {
|
|
const vite = await createViteServer({
|
|
server: { middlewareMode: true },
|
|
appType: "spa",
|
|
});
|
|
app.use(vite.middlewares);
|
|
} else {
|
|
const distPath = path.join(process.cwd(), "dist");
|
|
app.use(express.static(distPath));
|
|
app.get("*", (_req, res) => {
|
|
res.sendFile(path.join(distPath, "index.html"));
|
|
});
|
|
}
|
|
|
|
app.listen(PORT, "0.0.0.0", () => {
|
|
console.log(`PetTalk Server running on http://localhost:${PORT}`);
|
|
});
|
|
}
|
|
|
|
startServer();
|