Files
pettalk/pettalk-server-new.ts
2026-10-06 23:43:43 -07:00

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();