Coots commited on
Commit
61ffb8e
·
verified ·
1 Parent(s): 9f9b6af

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +201 -0
app.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ os.environ["TOKENIZERS_PARALLELISM"] = "false" # Prevent tokenizer hangs (if HF tokenizer used)
3
+ from flask import Flask, request, jsonify
4
+ from flask_cors import CORS
5
+ import joblib
6
+ import numpy as np
7
+ import json
8
+ import math
9
+ import xgboost as xgb
10
+ import logging
11
+
12
+ app = Flask(__name__)
13
+ CORS(app) # Allow cross-origin requests (important for frontend integration)
14
+
15
+ # Setup logging
16
+ logging.basicConfig(level=logging.INFO)
17
+
18
+ # Load models
19
+ try:
20
+ rf = joblib.load("rf_model.pkl")
21
+ xgb_model = xgb.Booster()
22
+ xgb_model.load_model("xgb_model.json")
23
+ app.logger.info("✅ Models loaded successfully.")
24
+ except Exception as e:
25
+ app.logger.error(f"❌ Error loading models: {e}")
26
+ raise e
27
+
28
+ # Load tile data
29
+ with open("tile_catalog.json", "r", encoding="utf-8") as f:
30
+ tile_catalog = json.load(f)
31
+
32
+ with open("tile_sizes.json", "r", encoding="utf-8") as f:
33
+ tile_sizes = json.load(f)
34
+
35
+ @app.route('/recommend', methods=['POST'])
36
+ def recommend():
37
+ """
38
+ Endpoint for product recommendations
39
+ Expected JSON payload:
40
+ {
41
+ "tile_type": "floor"|"wall",
42
+ "coverage": float,
43
+ "area": float,
44
+ "price_range": [min, max],
45
+ "preferred_sizes": [size1, size2] (optional)
46
+ }
47
+ """
48
+ try:
49
+ data = request.get_json()
50
+
51
+ required_fields = ['tile_type', 'coverage', 'area', 'price_range']
52
+ if not all(field in data for field in required_fields):
53
+ return jsonify({"error": "Missing required fields"}), 400
54
+
55
+ tile_type = data['tile_type'].lower()
56
+ if tile_type not in ['floor', 'wall']:
57
+ return jsonify({"error": "Invalid tile type. Use 'floor' or 'wall'"}), 400
58
+
59
+ # Validate numeric inputs
60
+ validate_positive_number(data['coverage'], "coverage")
61
+ validate_positive_number(data['area'], "area")
62
+ if (not isinstance(data['price_range'], list) or
63
+ len(data['price_range']) != 2 or
64
+ data['price_range'][0] < 0 or
65
+ data['price_range'][1] <= 0 or
66
+ data['price_range'][0] >= data['price_range'][1]):
67
+ return jsonify({"error": "Invalid price range"}), 400
68
+
69
+ features = prepare_features(data)
70
+
71
+ xgb_pred = xgb_model.predict(xgb.DMatrix(features))[0]
72
+ rf_pred = rf.predict_proba(features)[0][1]
73
+ combined_score = (xgb_pred + rf_pred) / 2
74
+
75
+ recommended_products = filter_products(
76
+ tile_type=tile_type,
77
+ min_price=data['price_range'][0],
78
+ max_price=data['price_range'][1],
79
+ preferred_sizes=data.get('preferred_sizes', []),
80
+ min_score=0.5
81
+ )
82
+
83
+ response = {
84
+ "recommendation_score": round(float(combined_score), 3),
85
+ "total_matches": len(recommended_products),
86
+ "recommended_products": recommended_products[:5],
87
+ "calculation": calculate_requirements(data['area'], data['coverage'])
88
+ }
89
+ return jsonify(response)
90
+
91
+ except Exception as e:
92
+ app.logger.error(f"Error in /recommend: {str(e)}")
93
+ return jsonify({"error": "Internal server error"}), 500
94
+
95
+ @app.route('/calculate', methods=['POST'])
96
+ def calculate():
97
+ """
98
+ Endpoint for tile calculation
99
+ Expected JSON payload:
100
+ {
101
+ "tile_type": "floor"|"wall",
102
+ "area": float,
103
+ "tile_size": "12x12"|etc
104
+ }
105
+ """
106
+ try:
107
+ data = request.get_json()
108
+
109
+ if 'tile_type' not in data or 'area' not in data or 'tile_size' not in data:
110
+ return jsonify({"error": "Missing required fields"}), 400
111
+
112
+ tile_type = data['tile_type'].lower()
113
+ if tile_type not in ['floor', 'wall']:
114
+ return jsonify({"error": "Invalid tile type"}), 400
115
+
116
+ if data['tile_size'] not in tile_sizes:
117
+ return jsonify({"error": "Invalid tile size"}), 400
118
+
119
+ validate_positive_number(data['area'], "area")
120
+
121
+ tile_info = tile_sizes[data['tile_size']]
122
+ area_per_tile = tile_info['length'] * tile_info['width']
123
+ tiles_needed = math.ceil((data['area'] / area_per_tile) * 1.1)
124
+ tiles_per_box = tile_info.get('tiles_per_box', 10)
125
+ boxes_needed = math.ceil(tiles_needed / tiles_per_box)
126
+
127
+ matching_products = [
128
+ p for p in tile_catalog
129
+ if p['type'].lower() == tile_type and p['size'] == data['tile_size']
130
+ ]
131
+
132
+ return jsonify({
133
+ "tile_type": tile_type,
134
+ "area": data['area'],
135
+ "tile_size": data['tile_size'],
136
+ "tiles_needed": tiles_needed,
137
+ "boxes_needed": boxes_needed,
138
+ "matching_products": matching_products[:3],
139
+ "total_matches": len(matching_products)
140
+ })
141
+
142
+ except Exception as e:
143
+ app.logger.error(f"Error in /calculate: {str(e)}")
144
+ return jsonify({"error": "Internal server error"}), 500
145
+
146
+ def prepare_features(data):
147
+ """Prepare feature vector for ML prediction"""
148
+ tile_type_num = 0 if data['tile_type'] == 'floor' else 1
149
+ price_per_sqft = data['price_range'][1] / data['coverage']
150
+ budget_efficiency = data['coverage'] / data['price_range'][1]
151
+
152
+ return np.array([[
153
+ tile_type_num,
154
+ data['area'],
155
+ data['coverage'],
156
+ data['price_range'][0],
157
+ data['price_range'][1],
158
+ price_per_sqft,
159
+ budget_efficiency
160
+ ]])
161
+
162
+ def filter_products(tile_type, min_price, max_price, preferred_sizes, min_score=0.5):
163
+ """Filter and score products"""
164
+ filtered = []
165
+ for product in tile_catalog:
166
+ if (product['type'].lower() == tile_type and
167
+ min_price <= product['price'] <= max_price and
168
+ (not preferred_sizes or product['size'] in preferred_sizes)):
169
+
170
+ price_score = 1 - ((product['price'] - min_price) / (max_price - min_price + 1e-6))
171
+ size_score = 1 if not preferred_sizes or product['size'] in preferred_sizes else 0.5
172
+ product_score = (price_score + size_score) / 2
173
+
174
+ if product_score >= min_score:
175
+ filtered.append({
176
+ **product,
177
+ "recommendation_score": round(product_score, 2)
178
+ })
179
+
180
+ return sorted(filtered, key=lambda x: x['recommendation_score'], reverse=True)
181
+
182
+ def calculate_requirements(area, coverage):
183
+ """Calculate tile quantities and estimated costs"""
184
+ min_tiles = math.ceil(area / coverage)
185
+ suggested_tiles = math.ceil(min_tiles * 1.1)
186
+ return {
187
+ "minimum_tiles": min_tiles,
188
+ "suggested_tiles": suggested_tiles,
189
+ "estimated_cost_range": [
190
+ round(area * 3, 2), # example: ₹3 per sqft
191
+ round(area * 10, 2) # example: ₹10 per sqft
192
+ ]
193
+ }
194
+
195
+ def validate_positive_number(value, field):
196
+ """Raise ValueError if value is not a positive number"""
197
+ if not isinstance(value, (int, float)) or value <= 0:
198
+ raise ValueError(f"{field} must be a positive number")
199
+
200
+ if __name__ == '__main__':
201
+ app.run(host='0.0.0.0', port=5000, debug=False)