Trae Assistant
Enhance app with Excel support, UI improvements, and fix triggerUpload
171dd0a
import os
import json
import pandas as pd
import io
import logging
from flask import Flask, render_template, request, jsonify, send_file
from werkzeug.exceptions import HTTPException
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload
# Global Error Handler
@app.errorhandler(Exception)
def handle_exception(e):
# Pass through HTTP errors
if isinstance(e, HTTPException):
return jsonify(error=str(e)), e.code
# Log non-HTTP errors
logger.error(f"Unhandled Exception: {e}", exc_info=True)
return jsonify(error=f"Internal Server Error: {str(e)}"), 500
# Core Logic: 1D Cutting Stock Optimization
def optimize_cutting(stock_sizes, requirements, kerf_width=0):
"""
stock_sizes: List of available stock lengths [6000, 3000]
requirements: List of dicts [{'length': 500, 'quantity': 10}, ...]
kerf_width: Blade width (loss per cut)
"""
try:
# 1. Expand requirements into individual items
items = []
for req in requirements:
qty = int(req.get('quantity', 0))
if qty <= 0:
continue
for _ in range(qty):
items.append({
'length': float(req['length']),
'id': req.get('label', '')
})
# 2. Sort items descending (First Fit Decreasing heuristic)
items.sort(key=lambda x: x['length'], reverse=True)
# 3. Initialize used stocks
used_stocks = []
# Sort stock sizes to facilitate Best Fit
stock_sizes.sort()
unfitted_items = []
for item in items:
item_len = item['length']
placed = False
# Try to fit in existing used stocks (First Fit)
for stock in used_stocks:
needed = item_len + (kerf_width if stock['cuts'] else 0)
if stock['remaining'] >= needed:
stock['cuts'].append({
'length': item_len,
'label': item['id']
})
stock['remaining'] -= needed
placed = True
break
# If not placed, try to open a new stock
if not placed:
best_stock_len = None
for s_len in stock_sizes:
if s_len >= item_len:
best_stock_len = s_len
break
if best_stock_len:
used_stocks.append({
'length': best_stock_len,
'cuts': [{'length': item_len, 'label': item['id']}],
'remaining': best_stock_len - item_len
})
placed = True
else:
unfitted_items.append(item)
# 4. Calculate Statistics
total_stock_length = sum(s['length'] for s in used_stocks)
total_parts_length = sum(c['length'] for s in used_stocks for c in s['cuts'])
total_waste = total_stock_length - total_parts_length
waste_percent = (total_waste / total_stock_length * 100) if total_stock_length > 0 else 0
return {
'solution': used_stocks,
'unfitted': unfitted_items,
'stats': {
'total_stock_used': len(used_stocks),
'total_length_consumed': total_stock_length,
'total_parts_length': total_parts_length,
'waste_length': total_waste,
'waste_percent': round(waste_percent, 2),
'efficiency': round(100 - waste_percent, 2)
}
}
except Exception as e:
logger.error(f"Optimization error: {e}")
raise
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/optimize', methods=['POST'])
def api_optimize():
try:
data = request.json
if not data:
return jsonify({'error': 'Invalid JSON data'}), 400
stock_sizes = [float(x) for x in data.get('stock_sizes', [])]
requirements = data.get('requirements', [])
kerf = float(data.get('kerf', 0))
if not stock_sizes:
return jsonify({'error': '未提供原材料尺寸 (No stock sizes provided)'}), 400
result = optimize_cutting(stock_sizes, requirements, kerf)
return jsonify(result)
except ValueError as e:
return jsonify({'error': f'数据格式错误: {str(e)}'}), 400
except Exception as e:
return jsonify({'error': f'计算出错: {str(e)}'}), 500
@app.route('/api/import', methods=['POST'])
def api_import():
try:
if 'file' not in request.files:
return jsonify({'error': 'No file uploaded'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
if file.filename.endswith('.json'):
data = json.load(file)
return jsonify(data)
elif file.filename.endswith(('.xlsx', '.xls')):
# Assume headers: Length, Quantity, Label
df = pd.read_excel(file)
requirements = []
# Try to map columns
cols = df.columns.astype(str).str.lower()
len_col = next((c for c in cols if 'len' in c or '长' in c), None)
qty_col = next((c for c in cols if 'qty' in c or 'num' in c or 'count' in c or '数' in c), None)
lbl_col = next((c for c in cols if 'lbl' in c or 'label' in c or 'rem' in c or 'id' in c or '注' in c or '号' in c), None)
if not len_col or not qty_col:
# Fallback to index 0, 1, 2
if len(df.columns) >= 2:
len_col = df.columns[0]
qty_col = df.columns[1]
lbl_col = df.columns[2] if len(df.columns) > 2 else None
else:
return jsonify({'error': 'Excel must have at least Length and Quantity columns'}), 400
for _, row in df.iterrows():
try:
l = float(row[len_col])
q = int(row[qty_col])
if l > 0 and q > 0:
requirements.append({
'length': l,
'quantity': q,
'label': str(row[lbl_col]) if lbl_col else ''
})
except:
continue
return jsonify({'requirements': requirements})
return jsonify({'error': 'Unsupported file format'}), 400
except Exception as e:
logger.error(f"Import error: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/api/export', methods=['POST'])
def api_export():
try:
data = request.json
solution = data.get('solution', [])
stats = data.get('stats', {})
# Flatten solution for Excel
rows = []
for stock_idx, stock in enumerate(solution):
stock_len = stock['length']
for cut in stock['cuts']:
rows.append({
'Stock Index': stock_idx + 1,
'Stock Length': stock_len,
'Part Length': cut['length'],
'Part Label': cut['label'],
'Waste/Remnant': ''
})
# Add remnant row
if stock['remaining'] > 0:
rows.append({
'Stock Index': stock_idx + 1,
'Stock Length': stock_len,
'Part Length': '',
'Part Label': 'REMNANT',
'Waste/Remnant': stock['remaining']
})
df = pd.DataFrame(rows)
# Summary sheet
summary_data = [{k: v for k, v in stats.items()}]
df_stats = pd.DataFrame(summary_data)
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Cutting List', index=False)
df_stats.to_excel(writer, sheet_name='Statistics', index=False)
output.seek(0)
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name='cutting_optimization_result.xlsx'
)
except Exception as e:
logger.error(f"Export error: {e}")
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
# Use 0.0.0.0 for container/HF spaces
app.run(host='0.0.0.0', port=7860)