Coots commited on
Commit
5d5165f
Β·
verified Β·
1 Parent(s): a43c45a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +172 -46
app.py CHANGED
@@ -1,75 +1,201 @@
1
- from flask import Flask, request, jsonify
 
 
 
2
  import json
3
  import math
4
- import joblib
5
  import xgboost as xgb
 
6
 
7
- app = Flask(__name__)
 
 
8
 
9
- # Load ML models
10
  try:
11
- rf_model = joblib.load("rf_model.pkl")
12
  xgb_model = xgb.Booster()
13
  xgb_model.load_model("xgb_model.json")
14
- print("βœ… Models loaded")
15
  except Exception as e:
16
- print(f"❌ Model loading failed: {e}")
17
- rf_model = None
18
- xgb_model = None
19
 
20
- # Load product catalog
21
- with open("tile_catalog.json") as f:
22
  tile_catalog = json.load(f)
23
 
24
- @app.route("/api/calculate", methods=["POST"])
25
- def calculate_tiles():
26
- data = request.json
 
 
 
 
 
 
 
 
 
 
 
 
27
  try:
28
- length = float(data.get("length", 0))
29
- width = float(data.get("width", 0))
30
- tile_length = float(data.get("tile_length", 0))
31
- tile_width = float(data.get("tile_width", 0))
 
 
 
 
 
32
 
33
- if not all([length, width, tile_length, tile_width]):
34
- return jsonify({"error": "All dimensions must be provided"}), 400
 
35
 
 
 
 
 
 
36
  area = length * width
37
- tile_area = tile_length * tile_width
38
- tiles_needed = math.ceil((area / tile_area) * 1.1) # 10% buffer
39
- boxes_needed = math.ceil(tiles_needed / 10) # Assume 10 tiles/box
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  return jsonify({
42
- "area": area,
43
- "tile_area": tile_area,
 
 
 
44
  "tiles_needed": tiles_needed,
45
- "boxes_needed": boxes_needed
 
 
46
  })
 
47
  except Exception as e:
48
- return jsonify({"error": str(e)}), 500
 
49
 
50
- @app.route("/recommend", methods=["POST"])
 
51
  def recommend():
52
- data = request.json
53
- tile_type = data.get("tile_type", "").lower()
54
- area = float(data.get("area_range", 0))
 
 
 
55
 
56
- if not tile_catalog:
57
- return jsonify({"result": "❌ No catalog available"})
 
58
 
59
- suggestions = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  for product in tile_catalog:
61
- if product["type"].lower() == tile_type:
62
- if product.get("area_range_min", 0) <= area <= product.get("area_range_max", float("inf")):
63
- suggestions.append({
64
- "name": product.get("name", "Tile Product"),
65
- "price": product.get("price", "β‚Ή0"),
66
- "link": product.get("link", "#")
 
 
 
 
 
 
 
67
  })
68
 
69
- if suggestions:
70
- return jsonify({"result": "βœ… Recommended", "suggestions": suggestions[:4]})
71
- else:
72
- return jsonify({"result": "❌ No matching products", "suggestions": []})
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- if __name__ == "__main__":
75
- app.run(debug=True)
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory
2
+ from flask_cors import CORS
3
+ import joblib
4
+ import numpy as np
5
  import json
6
  import math
7
+ import os
8
  import xgboost as xgb
9
+ import logging
10
 
11
+ app = Flask(__name__, static_folder=".", static_url_path="")
12
+ CORS(app)
13
+ logging.basicConfig(level=logging.INFO)
14
 
15
+ # === Load models ===
16
  try:
17
+ rf = joblib.load("rf_model.pkl")
18
  xgb_model = xgb.Booster()
19
  xgb_model.load_model("xgb_model.json")
20
+ app.logger.info("βœ… Models loaded successfully.")
21
  except Exception as e:
22
+ app.logger.error(f"❌ Error loading models: {e}")
23
+ raise e
 
24
 
25
+ # === Load data ===
26
+ with open("tile_catalog.json", "r", encoding="utf-8") as f:
27
  tile_catalog = json.load(f)
28
 
29
+ with open("tile_sizes.json", "r", encoding="utf-8") as f:
30
+ tile_sizes = json.load(f)
31
+
32
+ # === Serve Frontend ===
33
+ @app.route('/')
34
+ def serve_index():
35
+ return send_from_directory('.', 'index.html')
36
+
37
+ @app.route('/<path:path>')
38
+ def serve_static(path):
39
+ return send_from_directory('.', path)
40
+
41
+ # === Calculate tile requirement ===
42
+ @app.route('/calculate', methods=['POST'])
43
+ def calculate():
44
  try:
45
+ data = request.get_json()
46
+ required_fields = ['tile_type', 'length', 'width', 'tile_size']
47
+ for field in required_fields:
48
+ if field not in data:
49
+ return jsonify({"error": f"Missing field: {field}"}), 400
50
+
51
+ tile_type = data['tile_type'].lower()
52
+ if tile_type not in ['floor', 'wall']:
53
+ return jsonify({"error": "Invalid tile type"}), 400
54
 
55
+ tile_size = data['tile_size']
56
+ if tile_size not in tile_sizes:
57
+ return jsonify({"error": "Invalid tile size"}), 400
58
 
59
+ # Validate and compute area
60
+ length = float(data['length'])
61
+ width = float(data['width'])
62
+ validate_positive_number(length, "length")
63
+ validate_positive_number(width, "width")
64
  area = length * width
65
+ validate_positive_number(area, "area")
66
+
67
+ # Calculate tile needs
68
+ tile_info = tile_sizes[tile_size]
69
+ area_per_tile = tile_info['length'] * tile_info['width']
70
+ tiles_needed = math.ceil((area / area_per_tile) * 1.1)
71
+ tiles_per_box = tile_info.get('tiles_per_box', 10)
72
+ boxes_needed = math.ceil(tiles_needed / tiles_per_box)
73
+
74
+ # Find matching products
75
+ matching_products = [
76
+ {
77
+ **p,
78
+ "link": p.get("url", "#")
79
+ }
80
+ for p in tile_catalog
81
+ if p['type'].lower() == tile_type and p['size'] == tile_size
82
+ ]
83
 
84
  return jsonify({
85
+ "tile_type": tile_type,
86
+ "tile_size": tile_size,
87
+ "length": round(length, 2),
88
+ "width": round(width, 2),
89
+ "area": round(area, 2),
90
  "tiles_needed": tiles_needed,
91
+ "boxes_needed": boxes_needed,
92
+ "matching_products": matching_products[:5],
93
+ "total_matches": len(matching_products)
94
  })
95
+
96
  except Exception as e:
97
+ app.logger.error(f"Error in /calculate: {str(e)}")
98
+ return jsonify({"error": "Internal server error"}), 500
99
 
100
+ # === Recommendation endpoint ===
101
+ @app.route('/recommend', methods=['POST'])
102
  def recommend():
103
+ try:
104
+ data = request.get_json()
105
+ required_fields = ['tile_type', 'coverage', 'length', 'width', 'price_range']
106
+ for field in required_fields:
107
+ if field not in data:
108
+ return jsonify({"error": f"Missing field: {field}"}), 400
109
 
110
+ tile_type = data['tile_type'].lower()
111
+ if tile_type not in ['floor', 'wall']:
112
+ return jsonify({"error": "Invalid tile type"}), 400
113
 
114
+ length = float(data['length'])
115
+ width = float(data['width'])
116
+ validate_positive_number(length, "length")
117
+ validate_positive_number(width, "width")
118
+ area = length * width
119
+ validate_positive_number(area, "area")
120
+ coverage = float(data['coverage'])
121
+ validate_positive_number(coverage, "coverage")
122
+
123
+ if not isinstance(data['price_range'], list) or len(data['price_range']) != 2:
124
+ return jsonify({"error": "Invalid price range"}), 400
125
+
126
+ # Prepare features
127
+ features = prepare_features({
128
+ **data,
129
+ "area": area
130
+ })
131
+
132
+ xgb_pred = xgb_model.predict(xgb.DMatrix(features))[0]
133
+ rf_pred = rf.predict_proba(features)[0][1]
134
+ combined_score = (xgb_pred + rf_pred) / 2
135
+
136
+ recommended_products = filter_products(
137
+ tile_type=tile_type,
138
+ min_price=data['price_range'][0],
139
+ max_price=data['price_range'][1],
140
+ preferred_sizes=data.get('preferred_sizes', []),
141
+ min_score=0.5
142
+ )
143
+
144
+ return jsonify({
145
+ "recommendation_score": round(float(combined_score), 3),
146
+ "recommended_products": recommended_products[:5],
147
+ "calculation": calculate_requirements(area, coverage)
148
+ })
149
+
150
+ except Exception as e:
151
+ app.logger.error(f"Error in /recommend: {str(e)}")
152
+ return jsonify({"error": "Internal server error"}), 500
153
+
154
+ # === Utility Functions ===
155
+ def prepare_features(data):
156
+ tile_type_num = 0 if data['tile_type'] == 'floor' else 1
157
+ price_per_sqft = data['price_range'][1] / data['coverage']
158
+ budget_efficiency = data['coverage'] / data['price_range'][1]
159
+ return np.array([[tile_type_num, data['area'], data['coverage'],
160
+ data['price_range'][0], data['price_range'][1],
161
+ price_per_sqft, budget_efficiency]])
162
+
163
+ def filter_products(tile_type, min_price, max_price, preferred_sizes, min_score=0.5):
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
+ "link": product.get("url", "#")
179
  })
180
 
181
+ return sorted(filtered, key=lambda x: x['recommendation_score'], reverse=True)
182
+
183
+ def calculate_requirements(area, coverage):
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),
191
+ round(area * 10, 2)
192
+ ]
193
+ }
194
+
195
+ def validate_positive_number(value, field):
196
+ if not isinstance(value, (int, float)) or value <= 0:
197
+ raise ValueError(f"{field} must be a positive number")
198
 
199
+ # === Run the App ===
200
+ if __name__ == '__main__':
201
+ app.run(host='0.0.0.0', port=7860, debug=False)