Spaces:
Sleeping
Sleeping
Commit
·
7954a1d
1
Parent(s):
7d41249
Improve error handling in both frontend and backend
Browse files- app.py +176 -143
- static/script.js +11 -0
app.py
CHANGED
|
@@ -5,6 +5,8 @@ from pathlib import Path
|
|
| 5 |
import uuid
|
| 6 |
import pandas as pd # Added for CSV parsing
|
| 7 |
from werkzeug.utils import secure_filename # Added for security
|
|
|
|
|
|
|
| 8 |
|
| 9 |
app = Flask(__name__)
|
| 10 |
# Use absolute paths for robustness within Docker/HF Spaces
|
|
@@ -20,6 +22,15 @@ app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'tif', 'tiff'}
|
|
| 20 |
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
|
| 21 |
RESULT_FOLDER.mkdir(parents=True, exist_ok=True)
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
def allowed_file(filename):
|
| 24 |
return '.' in filename and \
|
| 25 |
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
|
@@ -30,160 +41,172 @@ def index():
|
|
| 30 |
|
| 31 |
@app.route('/process', methods=['POST'])
|
| 32 |
def process_images():
|
| 33 |
-
if 'files' not in request.files:
|
| 34 |
-
return jsonify({"error": "No file part"}), 400
|
| 35 |
-
|
| 36 |
-
files = request.files.getlist('files')
|
| 37 |
-
input_mode = request.form.get('input_mode', 'single') # Get from form data
|
| 38 |
-
confidence = request.form.get('confidence_threshold', '0.6') # Get from form data
|
| 39 |
-
|
| 40 |
-
if not files or files[0].filename == '':
|
| 41 |
-
return jsonify({"error": "No selected file"}), 400
|
| 42 |
-
|
| 43 |
-
# Create a unique job directory within results
|
| 44 |
-
job_id = str(uuid.uuid4())
|
| 45 |
-
job_input_dir = RESULT_FOLDER / job_id / 'input' # Save inputs within job dir
|
| 46 |
-
job_output_dir = RESULT_FOLDER / job_id / 'output' # Save outputs within job dir
|
| 47 |
-
job_input_dir.mkdir(parents=True, exist_ok=True)
|
| 48 |
-
job_output_dir.mkdir(parents=True, exist_ok=True)
|
| 49 |
-
|
| 50 |
-
saved_files = []
|
| 51 |
-
error_files = []
|
| 52 |
-
|
| 53 |
-
for file in files:
|
| 54 |
-
if file and allowed_file(file.filename):
|
| 55 |
-
filename = secure_filename(file.filename)
|
| 56 |
-
save_path = job_input_dir / filename
|
| 57 |
-
file.save(str(save_path))
|
| 58 |
-
saved_files.append(save_path)
|
| 59 |
-
elif file:
|
| 60 |
-
error_files.append(file.filename)
|
| 61 |
-
|
| 62 |
-
if not saved_files:
|
| 63 |
-
return jsonify({"error": f"No valid files uploaded. Invalid files: {error_files}"}), 400
|
| 64 |
-
|
| 65 |
-
# --- Prepare and Run nemaquant.py ---
|
| 66 |
-
# Determine input target for nemaquant.py
|
| 67 |
-
if input_mode == 'single' and len(saved_files) == 1:
|
| 68 |
-
input_target = str(saved_files[0])
|
| 69 |
-
img_mode_arg = 'file' # nemaquant uses file/dir, not single/directory
|
| 70 |
-
elif input_mode == 'directory' and len(saved_files) >= 1:
|
| 71 |
-
input_target = str(job_input_dir) # Pass the directory containing the images
|
| 72 |
-
img_mode_arg = 'dir'
|
| 73 |
-
else:
|
| 74 |
-
# Mismatch between mode and number of files
|
| 75 |
-
return jsonify({"error": f"Input mode '{input_mode}' requires {'1 file' if input_mode == 'single' else '1 or more files'}, but received {len(saved_files)}."}), 400
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
output_csv = job_output_dir / f"{job_id}_results.csv"
|
| 79 |
-
annotated_dir = job_output_dir # Save annotated images directly in job output dir
|
| 80 |
-
|
| 81 |
-
cmd = [
|
| 82 |
-
'python', str(APP_ROOT / 'nemaquant.py'),
|
| 83 |
-
'-i', input_target,
|
| 84 |
-
'-w', str(WEIGHTS_FILE), # Use absolute path
|
| 85 |
-
'-o', str(output_csv),
|
| 86 |
-
'-a', str(annotated_dir),
|
| 87 |
-
'--conf', confidence
|
| 88 |
-
]
|
| 89 |
-
|
| 90 |
-
# We don't need --key or XY mode for this web interface initially
|
| 91 |
-
|
| 92 |
try:
|
| 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 |
-
error_message = f"Error processing results: {e}"
|
| 155 |
-
print(error_message)
|
| 156 |
-
return jsonify({"error": "Could not find output file", "log": error_message}), 500
|
| 157 |
except Exception as e:
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
error_message += traceback.format_exc()
|
| 161 |
print(error_message)
|
| 162 |
-
return jsonify({"error": "
|
| 163 |
|
| 164 |
|
| 165 |
@app.route('/results/<job_id>/<path:filename>')
|
| 166 |
def download_file(job_id, filename):
|
| 167 |
-
# Construct the full path to the file within the job's output directory
|
| 168 |
-
# Use secure_filename on the incoming filename part for safety? Maybe not needed if we trust our generated paths.
|
| 169 |
-
# Crucially, validate job_id and filename to prevent directory traversal.
|
| 170 |
-
# A simple check: ensure job_id is a valid UUID format and filename doesn't contain '..'
|
| 171 |
-
try:
|
| 172 |
-
uuid.UUID(job_id, version=4) # Validate UUID format
|
| 173 |
-
except ValueError:
|
| 174 |
-
return "Invalid job ID format", 400
|
| 175 |
-
|
| 176 |
-
if '..' in filename or filename.startswith('/'):
|
| 177 |
-
return "Invalid filename", 400
|
| 178 |
-
|
| 179 |
-
file_dir = RESULT_FOLDER / job_id / 'output'
|
| 180 |
-
# Use send_from_directory for security (handles path joining and prevents traversal above the specified directory)
|
| 181 |
-
print(f"Attempting to send file: {filename} from directory: {file_dir}")
|
| 182 |
try:
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
|
| 189 |
if __name__ == '__main__':
|
|
@@ -193,4 +216,14 @@ if __name__ == '__main__':
|
|
| 193 |
print("Please ensure 'weights.pt' is in the application root directory.")
|
| 194 |
exit(1) # Exit if weights are missing
|
| 195 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
app.run(debug=True, host='0.0.0.0', port=7860) # Port 7860 is common for HF Spaces
|
|
|
|
| 5 |
import uuid
|
| 6 |
import pandas as pd # Added for CSV parsing
|
| 7 |
from werkzeug.utils import secure_filename # Added for security
|
| 8 |
+
import traceback
|
| 9 |
+
import sys
|
| 10 |
|
| 11 |
app = Flask(__name__)
|
| 12 |
# Use absolute paths for robustness within Docker/HF Spaces
|
|
|
|
| 22 |
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
|
| 23 |
RESULT_FOLDER.mkdir(parents=True, exist_ok=True)
|
| 24 |
|
| 25 |
+
# Global error handler to ensure JSON responses
|
| 26 |
+
@app.errorhandler(Exception)
|
| 27 |
+
def handle_exception(e):
|
| 28 |
+
# Log the exception
|
| 29 |
+
print(f"Unhandled exception: {str(e)}")
|
| 30 |
+
print(traceback.format_exc())
|
| 31 |
+
# Return JSON instead of HTML for HTTP errors
|
| 32 |
+
return jsonify({"error": "Server error", "log": str(e)}), 500
|
| 33 |
+
|
| 34 |
def allowed_file(filename):
|
| 35 |
return '.' in filename and \
|
| 36 |
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
|
|
|
| 41 |
|
| 42 |
@app.route('/process', methods=['POST'])
|
| 43 |
def process_images():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
try:
|
| 45 |
+
if 'files' not in request.files:
|
| 46 |
+
return jsonify({"error": "No file part"}), 400
|
| 47 |
+
|
| 48 |
+
files = request.files.getlist('files')
|
| 49 |
+
input_mode = request.form.get('input_mode', 'single') # Get from form data
|
| 50 |
+
confidence = request.form.get('confidence_threshold', '0.6') # Get from form data
|
| 51 |
+
|
| 52 |
+
if not files or files[0].filename == '':
|
| 53 |
+
return jsonify({"error": "No selected file"}), 400
|
| 54 |
+
|
| 55 |
+
# Create a unique job directory within results
|
| 56 |
+
job_id = str(uuid.uuid4())
|
| 57 |
+
job_input_dir = RESULT_FOLDER / job_id / 'input' # Save inputs within job dir
|
| 58 |
+
job_output_dir = RESULT_FOLDER / job_id / 'output' # Save outputs within job dir
|
| 59 |
+
job_input_dir.mkdir(parents=True, exist_ok=True)
|
| 60 |
+
job_output_dir.mkdir(parents=True, exist_ok=True)
|
| 61 |
+
|
| 62 |
+
saved_files = []
|
| 63 |
+
error_files = []
|
| 64 |
+
|
| 65 |
+
for file in files:
|
| 66 |
+
if file and allowed_file(file.filename):
|
| 67 |
+
filename = secure_filename(file.filename)
|
| 68 |
+
save_path = job_input_dir / filename
|
| 69 |
+
file.save(str(save_path))
|
| 70 |
+
saved_files.append(save_path)
|
| 71 |
+
elif file:
|
| 72 |
+
error_files.append(file.filename)
|
| 73 |
+
|
| 74 |
+
if not saved_files:
|
| 75 |
+
return jsonify({"error": f"No valid files uploaded. Invalid files: {error_files}"}), 400
|
| 76 |
+
|
| 77 |
+
# --- Prepare and Run nemaquant.py ---
|
| 78 |
+
# Determine input target for nemaquant.py
|
| 79 |
+
if input_mode == 'single' and len(saved_files) == 1:
|
| 80 |
+
input_target = str(saved_files[0])
|
| 81 |
+
img_mode_arg = 'file' # nemaquant uses file/dir, not single/directory
|
| 82 |
+
elif input_mode == 'directory' and len(saved_files) >= 1:
|
| 83 |
+
input_target = str(job_input_dir) # Pass the directory containing the images
|
| 84 |
+
img_mode_arg = 'dir'
|
| 85 |
+
else:
|
| 86 |
+
# Mismatch between mode and number of files
|
| 87 |
+
return jsonify({"error": f"Input mode '{input_mode}' requires {'1 file' if input_mode == 'single' else '1 or more files'}, but received {len(saved_files)}."}), 400
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
output_csv = job_output_dir / f"{job_id}_results.csv"
|
| 91 |
+
annotated_dir = job_output_dir # Save annotated images directly in job output dir
|
| 92 |
+
|
| 93 |
+
cmd = [
|
| 94 |
+
'python', str(APP_ROOT / 'nemaquant.py'),
|
| 95 |
+
'-i', input_target,
|
| 96 |
+
'-w', str(WEIGHTS_FILE), # Use absolute path
|
| 97 |
+
'-o', str(output_csv),
|
| 98 |
+
'-a', str(annotated_dir),
|
| 99 |
+
'--conf', confidence
|
| 100 |
+
]
|
| 101 |
+
|
| 102 |
+
# We don't need --key or XY mode for this web interface initially
|
| 103 |
+
|
| 104 |
+
try:
|
| 105 |
+
print(f"Running command: {' '.join(cmd)}") # Log the command
|
| 106 |
+
print(f"Input directory contents: {os.listdir(str(job_input_dir))}")
|
| 107 |
+
print(f"Weights file exists: {os.path.exists(str(WEIGHTS_FILE))}")
|
| 108 |
+
print(f"Weights file size: {os.path.getsize(str(WEIGHTS_FILE)) if os.path.exists(str(WEIGHTS_FILE)) else 'File not found'} bytes")
|
| 109 |
|
| 110 |
+
# Run the script, capture output and errors
|
| 111 |
+
# Timeout might be needed for long processes on shared infrastructure like HF Spaces
|
| 112 |
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=300) # 5 min timeout
|
| 113 |
+
status_log = f"NemaQuant Output:\n{result.stdout}\nNemaQuant Errors:\n{result.stderr}"
|
| 114 |
+
print(status_log) # Log script output
|
| 115 |
+
|
| 116 |
+
# Check output directory after command runs
|
| 117 |
+
print(f"Output directory exists: {os.path.exists(str(job_output_dir))}")
|
| 118 |
+
if os.path.exists(str(job_output_dir)):
|
| 119 |
+
print(f"Output directory contents: {os.listdir(str(job_output_dir))}")
|
| 120 |
+
|
| 121 |
+
# Check output file
|
| 122 |
+
print(f"Output CSV exists: {os.path.exists(str(output_csv))}")
|
| 123 |
+
|
| 124 |
+
# --- Parse Results ---
|
| 125 |
+
if not output_csv.exists():
|
| 126 |
+
raise FileNotFoundError(f"Output CSV not found at {output_csv}")
|
| 127 |
+
|
| 128 |
+
df = pd.read_csv(output_csv)
|
| 129 |
+
# Expect columns like 'filename', 'num_eggs' (based on nemaquant.py)
|
| 130 |
+
# Find corresponding annotated images
|
| 131 |
+
results_list = []
|
| 132 |
+
for index, row in df.iterrows():
|
| 133 |
+
original_filename = row.get('filename', '')
|
| 134 |
+
num_eggs = row.get('num_eggs', 'N/A')
|
| 135 |
+
# Construct expected annotated filename (based on nemaquant.py logic)
|
| 136 |
+
stem = Path(original_filename).stem
|
| 137 |
+
suffix = Path(original_filename).suffix
|
| 138 |
+
annotated_filename = f"{stem}_annotated{suffix}"
|
| 139 |
+
annotated_path = annotated_dir / annotated_filename
|
| 140 |
+
|
| 141 |
+
print(f"Looking for annotated file: {annotated_path}, exists: {annotated_path.exists()}")
|
| 142 |
+
|
| 143 |
+
results_list.append({
|
| 144 |
+
"filename": original_filename,
|
| 145 |
+
"num_eggs": num_eggs,
|
| 146 |
+
# Pass relative path within job dir for frontend URL construction
|
| 147 |
+
"annotated_filename": annotated_filename if annotated_path.exists() else None,
|
| 148 |
+
})
|
| 149 |
+
|
| 150 |
+
return jsonify({
|
| 151 |
+
"status": "success",
|
| 152 |
+
"job_id": job_id,
|
| 153 |
+
"results": results_list,
|
| 154 |
+
"log": status_log,
|
| 155 |
+
"error_files": error_files # Report files that were not processed
|
| 156 |
})
|
| 157 |
|
| 158 |
+
except subprocess.CalledProcessError as e:
|
| 159 |
+
error_message = f"Error running NemaQuant:\nExit Code: {e.returncode}\nSTDOUT:\n{e.stdout}\nSTDERR:\n{e.stderr}"
|
| 160 |
+
print(error_message)
|
| 161 |
+
return jsonify({"error": "Processing failed", "log": error_message}), 500
|
| 162 |
+
except subprocess.TimeoutExpired as e:
|
| 163 |
+
error_message = f"Error running NemaQuant: Process timed out after {e.timeout} seconds.\nSTDOUT:\n{e.stdout}\nSTDERR:\n{e.stderr}"
|
| 164 |
+
print(error_message)
|
| 165 |
+
return jsonify({"error": "Processing timed out", "log": error_message}), 500
|
| 166 |
+
except FileNotFoundError as e:
|
| 167 |
+
error_message = f"Error processing results: {e}"
|
| 168 |
+
print(error_message)
|
| 169 |
+
return jsonify({"error": "Could not find output file", "log": error_message}), 500
|
| 170 |
+
except Exception as e:
|
| 171 |
+
error_message = f"An unexpected error occurred: {str(e)}\n"
|
| 172 |
+
error_message += traceback.format_exc()
|
| 173 |
+
print(error_message)
|
| 174 |
+
return jsonify({"error": "An unexpected error occurred", "log": error_message}), 500
|
|
|
|
|
|
|
|
|
|
| 175 |
except Exception as e:
|
| 176 |
+
# High-level exception handler for the entire route
|
| 177 |
+
error_message = f"Global process error: {str(e)}\n{traceback.format_exc()}"
|
|
|
|
| 178 |
print(error_message)
|
| 179 |
+
return jsonify({"error": "Server error", "log": error_message}), 500
|
| 180 |
|
| 181 |
|
| 182 |
@app.route('/results/<job_id>/<path:filename>')
|
| 183 |
def download_file(job_id, filename):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
try:
|
| 185 |
+
# Construct the full path to the file within the job's output directory
|
| 186 |
+
# Use secure_filename on the incoming filename part for safety? Maybe not needed if we trust our generated paths.
|
| 187 |
+
# Crucially, validate job_id and filename to prevent directory traversal.
|
| 188 |
+
# A simple check: ensure job_id is a valid UUID format and filename doesn't contain '..'
|
| 189 |
+
try:
|
| 190 |
+
uuid.UUID(job_id, version=4) # Validate UUID format
|
| 191 |
+
except ValueError:
|
| 192 |
+
return jsonify({"error": "Invalid job ID format"}), 400
|
| 193 |
+
|
| 194 |
+
if '..' in filename or filename.startswith('/'):
|
| 195 |
+
return jsonify({"error": "Invalid filename"}), 400
|
| 196 |
+
|
| 197 |
+
file_dir = RESULT_FOLDER / job_id / 'output'
|
| 198 |
+
# Use send_from_directory for security (handles path joining and prevents traversal above the specified directory)
|
| 199 |
+
print(f"Attempting to send file: {filename} from directory: {file_dir}")
|
| 200 |
+
try:
|
| 201 |
+
return send_from_directory(str(file_dir), filename, as_attachment=False) # Display images inline
|
| 202 |
+
except FileNotFoundError:
|
| 203 |
+
print(f"File not found: {file_dir / filename}")
|
| 204 |
+
return jsonify({"error": "File not found"}), 404
|
| 205 |
+
except Exception as e:
|
| 206 |
+
# Catch-all exception handler
|
| 207 |
+
error_message = f"File serving error: {str(e)}"
|
| 208 |
+
print(error_message)
|
| 209 |
+
return jsonify({"error": "Server error", "log": error_message}), 500
|
| 210 |
|
| 211 |
|
| 212 |
if __name__ == '__main__':
|
|
|
|
| 216 |
print("Please ensure 'weights.pt' is in the application root directory.")
|
| 217 |
exit(1) # Exit if weights are missing
|
| 218 |
|
| 219 |
+
# Log startup information
|
| 220 |
+
print("----- NemaQuant Flask App Starting -----")
|
| 221 |
+
print(f"Working directory: {os.getcwd()}")
|
| 222 |
+
print(f"Python version: {sys.version}")
|
| 223 |
+
print(f"Weights file: {WEIGHTS_FILE}")
|
| 224 |
+
print(f"Weights file exists: {os.path.exists(str(WEIGHTS_FILE))}")
|
| 225 |
+
if os.path.exists(str(WEIGHTS_FILE)):
|
| 226 |
+
print(f"Weights file size: {os.path.getsize(str(WEIGHTS_FILE))} bytes")
|
| 227 |
+
print("---------------------------------------")
|
| 228 |
+
|
| 229 |
app.run(debug=True, host='0.0.0.0', port=7860) # Port 7860 is common for HF Spaces
|
static/script.js
CHANGED
|
@@ -83,6 +83,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 83 |
progress.value = 100;
|
| 84 |
progressText.textContent = '100%';
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
const data = await response.json();
|
| 87 |
|
| 88 |
if (response.ok) {
|
|
|
|
| 83 |
progress.value = 100;
|
| 84 |
progressText.textContent = '100%';
|
| 85 |
|
| 86 |
+
// First check if the response is valid
|
| 87 |
+
const contentType = response.headers.get('content-type');
|
| 88 |
+
if (!contentType || !contentType.includes('application/json')) {
|
| 89 |
+
// Handle non-JSON response
|
| 90 |
+
const textResponse = await response.text();
|
| 91 |
+
logStatus(`Error: Server returned non-JSON response: ${textResponse.substring(0, 200)}...`);
|
| 92 |
+
processingStatus.textContent = 'Error: Server returned invalid format';
|
| 93 |
+
throw new Error('Server returned non-JSON response');
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// Now we can safely parse JSON
|
| 97 |
const data = await response.json();
|
| 98 |
|
| 99 |
if (response.ok) {
|