File size: 6,018 Bytes
cd9ae77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import os
import traceback
import uuid

import torch
from dotenv import load_dotenv
from flask import Flask, jsonify, render_template, request
from flask_cors import CORS
from ultralytics import YOLO
from werkzeug.utils import secure_filename

from processing import process_images


BASE_DIR = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(BASE_DIR, '.env'))

app = Flask(__name__)
CORS(app)

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}


def resolve_app_path(configured_path, default_relative_path):
    """Resolve configured relative paths from the application directory."""
    path = configured_path or default_relative_path
    if os.path.isabs(path):
        return path
    return os.path.join(BASE_DIR, path)


UPLOAD_FOLDER = resolve_app_path(os.getenv('UPLOAD_FOLDER'), os.path.join('static', 'uploads'))
MODELS_FOLDER = resolve_app_path(os.getenv('MODELS_FOLDER'), 'models')
PARTS_MODEL_NAME = os.getenv('PARTS_MODEL_NAME', 'best_parts_EP336.pt')
DAMAGE_MODEL_NAME = os.getenv('DAMAGE_MODEL_NAME', 'best_new_EP382.pt')


def resolve_model_path(model_name):
    """Resolve an absolute model path or a filename relative to MODELS_FOLDER."""
    if os.path.isabs(model_name):
        return model_name
    return os.path.join(MODELS_FOLDER, model_name)


PARTS_MODEL_PATH = resolve_model_path(PARTS_MODEL_NAME)
DAMAGE_MODEL_PATH = resolve_model_path(DAMAGE_MODEL_NAME)

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(MODELS_FOLDER, exist_ok=True)

device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'Using device: {device}')


def load_model(model_path, label):
    """Load a YOLO model if its configured file exists."""
    if not os.path.isfile(model_path):
        print(f'Warning: {label} model file not found at {model_path}')
        return None

    try:
        model = YOLO(model_path)
        model.to(device)
        print(f"Successfully loaded {label.lower()} model '{os.path.basename(model_path)}' on {device}.")
        return model
    except Exception as exc:
        print(f'Error loading {label} model ({model_path}): {exc}')
        traceback.print_exc()
        return None


parts_model = load_model(PARTS_MODEL_PATH, 'Parts')
damage_model = load_model(DAMAGE_MODEL_PATH, 'Damage')


def allowed_file(filename):
    """Return True when filename has an allowed image extension."""
    return bool(filename) and '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


def model_readiness_error():
    """Return a useful model readiness payload, or None when both models are loaded."""
    missing = []
    if parts_model is None:
        missing.append({'model': 'parts', 'path': PARTS_MODEL_PATH})
    if damage_model is None:
        missing.append({'model': 'damage', 'path': DAMAGE_MODEL_PATH})

    if not missing:
        return None

    return {
        'error': 'Prediction models are not ready.',
        'missing_or_unloaded_models': missing,
        'hint': 'Place the configured .pt files at the listed paths or update MODELS_FOLDER/model names in .env.'
    }


@app.route('/')
def home():
    """Serve the main HTML page."""
    return render_template('index.html')


@app.route('/health', methods=['GET'])
def health():
    """Expose basic service/model readiness without running inference."""
    readiness = model_readiness_error()
    if readiness:
        return jsonify({'status': 'degraded', **readiness}), 503
    return jsonify({'status': 'ok', 'device': device}), 200


@app.route('/predict', methods=['POST'])
def predict():
    """Receive one or more images and return part/damage predictions."""
    readiness = model_readiness_error()
    if readiness:
        return jsonify(readiness), 503

    raw_session_key = request.form.get('session_key', '').strip()
    session_key = secure_filename(raw_session_key)[:100] or uuid.uuid4().hex

    if 'file' not in request.files:
        return jsonify({'error': 'No file part in the request'}), 400

    files = request.files.getlist('file')
    if not files or all(not file.filename for file in files):
        return jsonify({'error': 'No selected files'}), 400

    saved_images = []
    skipped_files = []

    try:
        for index, file in enumerate(files):
            original_filename = file.filename or ''
            if not (file and allowed_file(original_filename)):
                skipped_files.append(original_filename or f'file_{index}')
                continue

            safe_original = secure_filename(original_filename) or f'image_{index}.jpg'
            unique_filename = f'{uuid.uuid4().hex}_{safe_original}'
            filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
            file.save(filepath)
            saved_images.append({
                'path': filepath,
                'filename': original_filename,
                'index': index,
            })

        if not saved_images:
            return jsonify({
                'error': 'No valid files were uploaded. Allowed types: png, jpg, jpeg',
                'skipped_files': skipped_files,
            }), 400

        print(f"Processing {len(saved_images)} file(s) for session '{session_key}'...")
        results = process_images(parts_model, damage_model, saved_images)
        print(f"Processing complete for session '{session_key}'.")
        return jsonify(results)

    except Exception as exc:
        print(f'An error occurred during processing for session {session_key}: {exc}')
        traceback.print_exc()
        return jsonify({'error': f'An error occurred during processing: {exc}'}), 500

    finally:
        for image in saved_images:
            filepath = image['path']
            try:
                if os.path.exists(filepath):
                    os.remove(filepath)
            except Exception as exc:
                print(f'Error cleaning up file {filepath}: {exc}')


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=7860, debug=False)