Files
2026-02-13 00:21:53 +01:00

246 lines
9.0 KiB
JavaScript

/**
* Room Photo Analyzer using OpenAI GPT-4 Vision
* Analyzes room photos to extract dimensions, detect doors/windows, and suggest wardrobe placement
*/
import OpenAI from 'openai';
// Lazy initialization to avoid errors when API key is not set
let openaiClient = null;
function getOpenAIClient() {
if (!openaiClient) {
if (!process.env.OPENAI_API_KEY) {
throw new Error('OPENAI_API_KEY is not configured. Add it to server/.env');
}
openaiClient = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
}
return openaiClient;
}
/**
* Prompt for GPT-4 Vision to analyze room photos
*/
const ROOM_ANALYSIS_PROMPT = `You are a professional interior designer and architect specializing in custom wardrobe installations. Analyze this room photo with a STRONG FOCUS on the visible wall surfaces (parois) and ceiling (plafond).
PRIORITY: Carefully examine every visible wall and the ceiling. Detect their exact colors, materials, and textures as they appear in the photo. This is critical for accurate 3D room reconstruction.
Return a JSON object with the following structure:
{
"roomDimensions": {
"widthM": <estimated width in meters, e.g., 4.2>,
"lengthM": <estimated length/depth in meters, e.g., 3.8>,
"heightM": <estimated ceiling height in meters, typically 2.4-2.7>
},
"walls": [
{
"side": "back" | "left" | "right",
"visible": <boolean - is this wall clearly visible in the photo?>,
"color": "<EXACT hex color code sampled from the wall surface, e.g., #f5f5f0>",
"material": "paint" | "wallpaper" | "brick" | "wood_panel" | "concrete" | "plaster" | "stone" | "tile",
"texture": "smooth" | "textured" | "patterned" | "rough" | "glossy",
"hasWindow": <boolean>,
"hasDoor": <boolean>,
"surfaceArea": <approximate visible surface area percentage 0-100, accounting for windows/doors>
}
],
"windows": [
{
"wall": "back" | "left" | "right",
"xPercent": <horizontal position from left edge, 0-100>,
"yPercent": <vertical position from floor, 0-100>,
"widthPercent": <width as percentage of wall, e.g., 25>,
"heightPercent": <height as percentage of wall, e.g., 40>,
"type": "window" | "french_window" | "skylight" | "bay_window"
}
],
"doors": [
{
"wall": "back" | "left" | "right",
"xPercent": <horizontal position from left edge, 0-100>,
"widthPercent": <width as percentage of wall, typically 15-25>,
"heightPercent": <height as percentage of wall, typically 80-90>,
"type": "door" | "double_door" | "opening" | "closet" | "sliding_door"
}
],
"floor": {
"color": "<exact hex color code>",
"material": "wood" | "tile" | "carpet" | "concrete" | "laminate" | "vinyl" | "parquet"
},
"ceiling": {
"visible": <boolean - is the ceiling visible in the photo?>,
"color": "<EXACT hex color code detected from the ceiling surface>",
"type": "flat" | "sloped" | "beamed" | "coffered" | "vaulted",
"material": "paint" | "plaster" | "wood" | "concrete",
"slopeAngleDeg": <angle in degrees if sloped, 0 for flat, null if not applicable>,
"slopeDirection": "front_to_back" | "left_to_right" | null
},
"lighting": {
"natural": "bright" | "moderate" | "dim",
"windowSide": "left" | "right" | "back" | "front" | null
},
"suggestedWardrobeWall": "back" | "left" | "right",
"suggestedWardrobePosition": {
"xPercent": <start position from left, 0-100>,
"widthPercent": <suggested width as percentage of wall, e.g., 50>,
"reason": "<brief explanation of why this position is recommended>"
},
"roomType": "bedroom" | "living_room" | "hallway" | "dressing" | "other",
"confidence": <confidence score 0-100>
}
IMPORTANT GUIDELINES FOR WALL AND CEILING DETECTION:
- ALWAYS include all three walls (back, left, right). Set visible=false only if a wall is truly not visible at all.
- Sample the EXACT hex color from the dominant area of each wall surface — do NOT default to #f5f5f0.
- Pay close attention to paint color nuances: beige vs cream vs off-white vs warm grey etc.
- Detect the wall material: painted plaster, wallpaper, exposed brick, wood paneling, etc.
- For the ceiling: detect its actual color (it may not be pure white), its type, and material.
- If the ceiling is sloped (mansarde/sous-pente), estimate the slope angle and direction.
- Estimate dimensions based on standard furniture sizes if visible (beds, doors, windows).
- Standard door height is ~2.1m, standard door width is ~0.8-0.9m.
- Standard ceiling height is 2.4-2.7m in residential.
- For wardrobe placement, prefer walls without windows/doors and with enough clear space.
- Return ONLY valid JSON, no additional text.`;
/**
* Analyze a room photo using GPT-4 Vision
* @param {string} imageBase64 - Base64 encoded image data
* @param {string} mimeType - Image MIME type (image/jpeg, image/png, etc.)
* @returns {Object} Room analysis result
*/
export async function analyzeRoomPhoto(imageBase64, mimeType = 'image/jpeg') {
const openai = getOpenAIClient();
try {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: [
{ type: 'text', text: ROOM_ANALYSIS_PROMPT },
{
type: 'image_url',
image_url: {
url: `data:${mimeType};base64,${imageBase64}`,
detail: 'high'
}
}
]
}
],
max_tokens: 2000,
response_format: { type: 'json_object' }
});
const content = response.choices[0]?.message?.content;
if (!content) {
throw new Error('No response from OpenAI');
}
const analysis = JSON.parse(content);
// Validate required fields
if (!analysis.roomDimensions || !analysis.suggestedWardrobeWall) {
throw new Error('Incomplete room analysis');
}
return {
success: true,
analysis,
tokensUsed: response.usage?.total_tokens || 0
};
} catch (error) {
console.error('Room analysis error:', error);
if (error.code === 'invalid_api_key') {
throw new Error('Invalid OpenAI API key');
}
if (error.code === 'rate_limit_exceeded') {
throw new Error('API rate limit exceeded. Please try again later.');
}
throw error;
}
}
/**
* Convert analysis to 3D room configuration for Three.js
* @param {Object} analysis - Room analysis from GPT-4 Vision
* @returns {Object} 3D room configuration
*/
export function analysisTo3DConfig(analysis) {
const { roomDimensions, walls, windows, doors, floor, ceiling, suggestedWardrobeWall, suggestedWardrobePosition } = analysis;
return {
// Room dimensions in meters
width: roomDimensions.widthM || 4,
length: roomDimensions.lengthM || 4,
height: roomDimensions.heightM || 2.5,
// Wall configurations with material info
walls: {
back: (() => {
const w = walls?.find(w => w.side === 'back');
return w ? { color: w.color || '#f5f5f0', visible: w.visible !== false, material: w.material || 'paint', texture: w.texture || 'smooth' }
: { color: '#f5f5f0', visible: true, material: 'paint', texture: 'smooth' };
})(),
left: (() => {
const w = walls?.find(w => w.side === 'left');
return w ? { color: w.color || '#f5f5f0', visible: w.visible !== false, material: w.material || 'paint', texture: w.texture || 'smooth' }
: { color: '#f5f5f0', visible: true, material: 'paint', texture: 'smooth' };
})(),
right: (() => {
const w = walls?.find(w => w.side === 'right');
return w ? { color: w.color || '#f5f5f0', visible: w.visible !== false, material: w.material || 'paint', texture: w.texture || 'smooth' }
: { color: '#f5f5f0', visible: true, material: 'paint', texture: 'smooth' };
})()
},
// Windows for each wall
windows: (windows || []).map(w => ({
wall: w.wall,
x: (w.xPercent || 50) / 100,
y: (w.yPercent || 30) / 100,
width: (w.widthPercent || 25) / 100,
height: (w.heightPercent || 40) / 100,
type: w.type || 'window'
})),
// Doors for each wall
doors: (doors || []).map(d => ({
wall: d.wall,
x: (d.xPercent || 10) / 100,
width: (d.widthPercent || 20) / 100,
height: (d.heightPercent || 85) / 100,
type: d.type || 'door'
})),
// Floor and ceiling
floor: {
color: floor?.color || '#c4a574',
material: floor?.material || 'wood'
},
ceiling: {
color: ceiling?.color || '#ffffff',
type: ceiling?.type || 'flat',
visible: ceiling?.visible !== false,
material: ceiling?.material || 'paint',
slopeAngleDeg: ceiling?.slopeAngleDeg || 0,
slopeDirection: ceiling?.slopeDirection || null
},
// Wardrobe placement
wardrobePlacement: {
wall: suggestedWardrobeWall || 'back',
xPercent: suggestedWardrobePosition?.xPercent || 25,
widthPercent: suggestedWardrobePosition?.widthPercent || 50
}
};
}
export default { analyzeRoomPhoto, analysisTo3DConfig };