v1.0
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user