Spaces:
Runtime error
Runtime error
| import express from "express"; | |
| import { AppDataSource } from "../utils/dataSource"; | |
| import { User } from "../entity/User"; | |
| import { authenticateToken, AuthRequest } from "../middleware/auth"; | |
| import { readyPlayerMeClient } from "../utils/readyPlayerMe"; | |
| const router = express.Router(); | |
| /** | |
| * @openapi | |
| * /api/avatar/create: | |
| * post: | |
| * summary: Save Ready Player Me avatar ID for user | |
| * tags: [Avatar] | |
| * security: | |
| * - bearerAuth: [] | |
| * requestBody: | |
| * required: true | |
| * content: | |
| * application/json: | |
| * schema: | |
| * type: object | |
| * required: | |
| * - avatarId | |
| * properties: | |
| * avatarId: | |
| * type: string | |
| * description: Ready Player Me avatar ID | |
| * responses: | |
| * 200: | |
| * description: Avatar ID saved successfully | |
| * 400: | |
| * description: Bad request (missing avatarId) | |
| * 401: | |
| * description: Unauthorized | |
| * 404: | |
| * description: User not found | |
| * 500: | |
| * description: Server error | |
| */ | |
| router.post("/create", authenticateToken, async (req: AuthRequest, res) => { | |
| try { | |
| const userId = req.userId!; | |
| const { avatarId } = req.body; | |
| if (!avatarId) { | |
| return res.status(400).json({ | |
| success: false, | |
| error: "avatarId is required", | |
| }); | |
| } | |
| const userRepo = AppDataSource.getRepository(User); | |
| const user = await userRepo.findOne({ where: { id: userId } }); | |
| if (!user) { | |
| return res.status(404).json({ | |
| success: false, | |
| error: "User not found", | |
| }); | |
| } | |
| user.readyPlayerMeAvatarId = avatarId; | |
| await userRepo.save(user); | |
| res.json({ | |
| success: true, | |
| message: "Avatar ID saved successfully", | |
| avatarId, | |
| }); | |
| } catch (error: any) { | |
| console.error("Avatar creation error:", error); | |
| res.status(500).json({ | |
| success: false, | |
| error: error.message || "Failed to save avatar ID", | |
| }); | |
| } | |
| }); | |
| /** | |
| * @openapi | |
| * /api/avatar/glb: | |
| * get: | |
| * summary: Get 3D avatar GLB file URL | |
| * tags: [Avatar] | |
| * security: | |
| * - bearerAuth: [] | |
| * parameters: | |
| * - in: query | |
| * name: quality | |
| * schema: | |
| * type: string | |
| * enum: [low, medium, high, ultra] | |
| * description: Avatar quality preset | |
| * - in: query | |
| * name: lod | |
| * schema: | |
| * type: integer | |
| * enum: [0, 1, 2] | |
| * description: Level of detail (0=full, 1=50%, 2=25%) | |
| * - in: query | |
| * name: textureAtlas | |
| * schema: | |
| * type: string | |
| * description: Texture atlas size or "none" | |
| * - in: query | |
| * name: textureFormat | |
| * schema: | |
| * type: string | |
| * enum: [webp, jpeg, png] | |
| * description: Texture format | |
| * - in: query | |
| * name: useDracoMeshCompression | |
| * schema: | |
| * type: boolean | |
| * description: Enable Draco mesh compression | |
| * responses: | |
| * 200: | |
| * description: Avatar GLB URL retrieved successfully | |
| * 401: | |
| * description: Unauthorized | |
| * 404: | |
| * description: Avatar not found | |
| * 500: | |
| * description: Server error | |
| */ | |
| router.get("/glb", authenticateToken, async (req: AuthRequest, res) => { | |
| try { | |
| const userId = req.userId!; | |
| const { quality, lod, textureAtlas, textureFormat, useDracoMeshCompression } = req.query; | |
| const userRepo = AppDataSource.getRepository(User); | |
| const user = await userRepo.findOne({ where: { id: userId } }); | |
| if (!user || !user.readyPlayerMeAvatarId) { | |
| return res.status(404).json({ | |
| success: false, | |
| error: "Avatar not found. Please create an avatar first.", | |
| }); | |
| } | |
| const avatarUrl = await readyPlayerMeClient.getAvatarGLB(user.readyPlayerMeAvatarId, { | |
| quality: quality as any, | |
| lod: lod ? parseInt(lod as string) : undefined, | |
| textureAtlas: textureAtlas === "none" ? "none" : textureAtlas ? parseInt(textureAtlas as string) : undefined, | |
| textureFormat: textureFormat as any, | |
| useDracoMeshCompression: useDracoMeshCompression === "true", | |
| }); | |
| res.json({ | |
| success: true, | |
| avatarUrl, | |
| avatarId: user.readyPlayerMeAvatarId, | |
| }); | |
| } catch (error: any) { | |
| console.error("Get avatar GLB error:", error); | |
| res.status(500).json({ | |
| success: false, | |
| error: error.message || "Failed to get avatar GLB", | |
| }); | |
| } | |
| }); | |
| /** | |
| * @openapi | |
| * /api/avatar/render: | |
| * get: | |
| * summary: Get 2D avatar render (portrait) URL | |
| * tags: [Avatar] | |
| * security: | |
| * - bearerAuth: [] | |
| * parameters: | |
| * - in: query | |
| * name: size | |
| * schema: | |
| * type: integer | |
| * minimum: 1 | |
| * maximum: 1024 | |
| * description: Image size in pixels | |
| * - in: query | |
| * name: quality | |
| * schema: | |
| * type: integer | |
| * minimum: 0 | |
| * maximum: 100 | |
| * description: Image compression quality | |
| * - in: query | |
| * name: camera | |
| * schema: | |
| * type: string | |
| * enum: [portrait, fullbody, fit] | |
| * description: Camera preset | |
| * - in: query | |
| * name: background | |
| * schema: | |
| * type: string | |
| * description: Background color (RGB format) | |
| * - in: query | |
| * name: expression | |
| * schema: | |
| * type: string | |
| * description: Facial expression | |
| * - in: query | |
| * name: pose | |
| * schema: | |
| * type: string | |
| * description: Avatar pose | |
| * responses: | |
| * 200: | |
| * description: Avatar render URL retrieved successfully | |
| * 401: | |
| * description: Unauthorized | |
| * 404: | |
| * description: Avatar not found | |
| * 500: | |
| * description: Server error | |
| */ | |
| router.get("/render", authenticateToken, async (req: AuthRequest, res) => { | |
| try { | |
| const userId = req.userId!; | |
| const { size, quality, camera, background, expression, pose } = req.query; | |
| const userRepo = AppDataSource.getRepository(User); | |
| const user = await userRepo.findOne({ where: { id: userId } }); | |
| if (!user || !user.readyPlayerMeAvatarId) { | |
| return res.status(404).json({ | |
| success: false, | |
| error: "Avatar not found. Please create an avatar first.", | |
| }); | |
| } | |
| const renderUrl = await readyPlayerMeClient.getAvatar2DRender(user.readyPlayerMeAvatarId, { | |
| size: size ? parseInt(size as string) : undefined, | |
| quality: quality ? parseInt(quality as string) : undefined, | |
| camera: camera as any, | |
| background: background as string, | |
| expression: expression as string, | |
| pose: pose as string, | |
| }); | |
| res.json({ | |
| success: true, | |
| renderUrl, | |
| avatarId: user.readyPlayerMeAvatarId, | |
| }); | |
| } catch (error: any) { | |
| console.error("Get avatar render error:", error); | |
| res.status(500).json({ | |
| success: false, | |
| error: error.message || "Failed to get avatar render", | |
| }); | |
| } | |
| }); | |
| /** | |
| * @openapi | |
| * /api/avatar/metadata: | |
| * get: | |
| * summary: Get avatar metadata | |
| * tags: [Avatar] | |
| * security: | |
| * - bearerAuth: [] | |
| * responses: | |
| * 200: | |
| * description: Avatar metadata retrieved successfully | |
| * 401: | |
| * description: Unauthorized | |
| * 404: | |
| * description: Avatar not found | |
| * 500: | |
| * description: Server error | |
| */ | |
| router.get("/metadata", authenticateToken, async (req: AuthRequest, res) => { | |
| try { | |
| const userId = req.userId!; | |
| const userRepo = AppDataSource.getRepository(User); | |
| const user = await userRepo.findOne({ where: { id: userId } }); | |
| if (!user || !user.readyPlayerMeAvatarId) { | |
| return res.status(404).json({ | |
| success: false, | |
| error: "Avatar not found. Please create an avatar first.", | |
| }); | |
| } | |
| const metadata = await readyPlayerMeClient.getAvatarMetadata(user.readyPlayerMeAvatarId); | |
| if (!metadata) { | |
| return res.status(404).json({ | |
| success: false, | |
| error: "Avatar metadata not found", | |
| }); | |
| } | |
| res.json({ | |
| success: true, | |
| metadata, | |
| }); | |
| } catch (error: any) { | |
| console.error("Get avatar metadata error:", error); | |
| res.status(500).json({ | |
| success: false, | |
| error: error.message || "Failed to get avatar metadata", | |
| }); | |
| } | |
| }); | |
| /** | |
| * @openapi | |
| * /api/avatar/try-on: | |
| * post: | |
| * summary: Equip assets to avatar and get GLB URL with outfit | |
| * tags: [Avatar] | |
| * security: | |
| * - bearerAuth: [] | |
| * requestBody: | |
| * required: true | |
| * content: | |
| * application/json: | |
| * schema: | |
| * type: object | |
| * required: | |
| * - assetIds | |
| * properties: | |
| * assetIds: | |
| * type: array | |
| * items: | |
| * type: string | |
| * description: Array of Ready Player Me asset IDs to equip | |
| * quality: | |
| * type: string | |
| * enum: [low, medium, high, ultra] | |
| * description: Avatar quality preset | |
| * responses: | |
| * 200: | |
| * description: Avatar GLB URL with equipped outfit | |
| * 400: | |
| * description: Bad request | |
| * 401: | |
| * description: Unauthorized | |
| * 404: | |
| * description: Avatar not found | |
| * 500: | |
| * description: Server error | |
| */ | |
| router.post("/try-on", authenticateToken, async (req: AuthRequest, res) => { | |
| try { | |
| const userId = req.userId!; | |
| const { assetIds, quality } = req.body; | |
| if (!assetIds || !Array.isArray(assetIds) || assetIds.length === 0) { | |
| return res.status(400).json({ | |
| success: false, | |
| error: "assetIds array is required", | |
| }); | |
| } | |
| const userRepo = AppDataSource.getRepository(User); | |
| const user = await userRepo.findOne({ where: { id: userId } }); | |
| if (!user || !user.readyPlayerMeAvatarId) { | |
| return res.status(404).json({ | |
| success: false, | |
| error: "Avatar not found. Please create an avatar first.", | |
| }); | |
| } | |
| // Equip all assets to avatar | |
| const equipPromises = assetIds.map((assetId: string) => | |
| readyPlayerMeClient.equipAsset(user.readyPlayerMeAvatarId!, assetId) | |
| ); | |
| const results = await Promise.all(equipPromises); | |
| const allEquipped = results.every((result) => result === true); | |
| if (!allEquipped) { | |
| console.warn("Some assets failed to equip"); | |
| } | |
| // Get avatar GLB URL with equipped outfit | |
| const avatarUrl = await readyPlayerMeClient.getAvatarGLB(user.readyPlayerMeAvatarId, { | |
| quality: quality || "medium", | |
| lod: 1, | |
| textureFormat: "webp", | |
| useDracoMeshCompression: true, | |
| }); | |
| res.json({ | |
| success: true, | |
| avatarUrl, | |
| avatarId: user.readyPlayerMeAvatarId, | |
| equippedAssets: assetIds, | |
| }); | |
| } catch (error: any) { | |
| console.error("Try on avatar error:", error); | |
| res.status(500).json({ | |
| success: false, | |
| error: error.message || "Failed to equip outfit on avatar", | |
| }); | |
| } | |
| }); | |
| export default router; | |