Mr-Saab29/poseflow_backend
0
1// src/server.ts2import express from 'express';3import cors from 'cors';4import { z } from 'zod';5import {6 classifyLandmarks,7 loadModel,8 initTf,9 detectLandmarksFromImage,10} from "./pose.js";11 12const app = express();13app.use(cors());14app.use(express.json({ limit: '10mb', strict: true }));15 16// ---------------- Pose catalog (5 poses) ----------------17const ASSETS_BASE =18 process.env.POSE_ASSETS_BASE ??19 "https://huggingface.co/spaces/Mr-Saab29/poseflow_backend/resolve/main/file/assets";20 21export const POSES = ["Warrior", "Cobra", "Tree", "Triangle", "Dog"] as const;22type PoseName = typeof POSES[number];23 24const REF_IMAGES: Record<PoseName, string> = {25 Cobra: `${ASSETS_BASE}/cobra.jpg`,26 Dog: `${ASSETS_BASE}/dog.jpg`,27 Triangle: `${ASSETS_BASE}/traingle.jpg`, // note: file is spelled "traingle.jpg"28 Tree: `${ASSETS_BASE}/tree.jpg`,29 Warrior: `${ASSETS_BASE}/warrior.jpg`,30};31 32// ---------------- Info / health ----------------33app.get('/', (_req, res) =>34 res.json({35 ok: true,36 service: 'poseflow-backend',37 routes: ['/health', '/warmup', '/poses', '/classify', '/pose/predict', '/pose/verify'],38 })39);40app.get('/health', (_req, res) => res.json({ ok: true }));41 42// List the available poses and their reference URLs (for your UI)43app.get('/poses', (_req, res) => {44 res.json({45 poses: POSES.map((p) => ({ name: p, referenceUrl: REF_IMAGES[p] })),46 });47});48 49// ---------------- Warmup ----------------50app.post("/warmup", async (_req, res) => {51 try {52 await initTf(); // init backend + load model53 return res.json({ ok: true, warmed: true });54 } catch (e: any) {55 console.error("warmup error:", e);56 return res.status(500).json({ ok: false, error: "warmup_failed", message: e?.message ?? String(e) });57 }58});59 60// ---------------- Schemas ----------------61const Pair = z.tuple([z.number(), z.number()]);62const Landmarks = z.array(Pair).length(17);63const Threshold = z.number().min(0).max(1).default(0.8);64 65const ClassifyBody = z.object({66 landmarks: Landmarks,67 threshold: Threshold.optional(),68});69 70const PredictBody = z.object({71 landmarks: Landmarks.optional(),72 imageUrl: z.string().url().optional(),73 imageB64: z.string().min(1).optional(),74 threshold: Threshold.optional(),75}).refine((b) => !!b.landmarks || !!b.imageUrl || !!b.imageB64, {76 message: 'Provide either `landmarks` OR (`imageUrl` | `imageB64`).',77 path: ['landmarks'],78});79 80// User chooses a pose and uploads an image; we verify that pose’s probability81const VerifyBody = z.object({82 expectedPose: z.enum(POSES),83 imageUrl: z.string().url().optional(),84 imageB64: z.string().min(1).optional(),85 threshold: Threshold.optional(),86}).refine((b) => !!b.imageUrl || !!b.imageB64, {87 message: 'Provide `imageUrl` or `imageB64`.',88 path: ['imageUrl'],89});90 91// ---------------- Routes ----------------92app.post('/classify', async (req, res) => {93 const parsed = ClassifyBody.safeParse(req.body);94 if (!parsed.success) return res.status(400).json({ error: 'bad_request', details: parsed.error.errors });95 try {96 const { landmarks, threshold } = parsed.data;97 const out = await classifyLandmarks(landmarks, threshold ?? 0.8);98 res.json(out);99 } catch (e: any) {100 console.error('classify error:', e);101 res.status(500).json({ error: 'inference_failed', message: e?.message ?? String(e) });102 }103});104 105app.get('/pose/predict', (_req, res) =>106 res.json({ ok: true, usage: 'POST /pose/predict {landmarks:[[x,y]x17], threshold?} or {imageUrl|imageB64}' })107);108 109app.post("/pose/predict", async (req, res) => {110 const parsed = PredictBody.safeParse(req.body);111 if (!parsed.success) {112 return res.status(400).json({ error: "bad_request", details: parsed.error.errors });113 }114 const { landmarks, imageUrl, imageB64, threshold } = parsed.data;115 116 try {117 if (landmarks) {118 const out = await classifyLandmarks(landmarks, threshold ?? 0.8);119 return res.json(out);120 }121 if (imageUrl || imageB64) {122 const lm = await detectLandmarksFromImage({ imageUrl, imageB64 });123 const out = await classifyLandmarks(lm, threshold ?? 0.8);124 return res.json(out);125 }126 return res.status(400).json({ error: "bad_request", message: "Provide landmarks or imageUrl/imageB64" });127 } catch (e: any) {128 console.error("predict error:", e);129 return res.status(500).json({ error: "inference_failed", message: e?.message ?? String(e) });130 }131});132 133// NEW: verify a specific pose using the classifier probability for that pose134app.post("/pose/verify", async (req, res) => {135 const parsed = VerifyBody.safeParse(req.body);136 if (!parsed.success) {137 return res.status(400).json({ error: "bad_request", details: parsed.error.errors });138 }139 const { expectedPose, imageUrl, imageB64, threshold } = parsed.data;140 const th = threshold ?? 0.8;141 142 try {143 const landmarks = await detectLandmarksFromImage({ imageUrl, imageB64 });144 const result = await classifyLandmarks(landmarks, th);145 146 const expectedScore = Number(result.scores?.[expectedPose] ?? 0);147 const passed = expectedScore >= th;148 149 return res.json({150 ok: true,151 expectedPose,152 referenceUrl: REF_IMAGES[expectedPose],153 threshold: th,154 passed,155 expectedScore,156 top: result.top,157 scores: result.scores,158 });159 } catch (e: any) {160 console.error("verify error:", e);161 return res.status(500).json({ error: "inference_failed", message: e?.message ?? String(e) });162 }163});164 165// ---------------- Start ----------------166const port = Number(process.env.PORT || 7860);167 168initTf()169 .catch((e) => console.warn("initTf failed (will lazily init later):", e))170 .finally(() => {171 app.listen(port, "0.0.0.0", () => {172 console.log(`poseflow_backend listening on http://0.0.0.0:${port}`);173 });174 });175 176export default app;