GitHub Actions commited on
Commit
69c91ec
·
1 Parent(s): 07b1da9

deploy: sync from GitHub 7c94ae86e6a2671641dc0f6220fd590a4c9b64fc

Browse files
Files changed (2) hide show
  1. app.py +68 -19
  2. static/script.js +6 -1
app.py CHANGED
@@ -19,6 +19,7 @@ from torch import cuda
19
  from flask import Flask, Response, render_template, request, jsonify, send_file, session
20
  from multiprocessing.pool import Pool
21
  from multiprocessing import set_start_method
 
22
  from pathlib import Path
23
  from PIL import Image
24
  from datetime import datetime
@@ -98,6 +99,25 @@ def _load_session_meta(session_id):
98
  # Load model once at startup, use CUDA if available
99
  MODEL_DEVICE = 'cuda' if cuda.is_available() else 'cpu'
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  # need a global dict to hold async results objects
102
  # so you can check the progress of an abr
103
  # maybe there's a better way around this?
@@ -144,7 +164,7 @@ def upload_files():
144
  session['uuid_map_to_uuid_imgname'] = uuid_map_to_uuid_imgname
145
  # Persist to disk — cookie may be silently dropped if it exceeds ~4KB
146
  _save_session_meta(session_id, filename_map, uuid_map_to_uuid_imgname)
147
- return jsonify({'filename_map': filename_map, 'status': 'uploaded'})
148
 
149
  # /preview route for serving original uploaded image
150
  @app.route('/preview', methods=['POST'])
@@ -184,15 +204,12 @@ def preview_image():
184
  return jsonify({'error': str(e)}), 500
185
 
186
  # initializer for Pool to load model in each process
187
- # each worker will have its own model instance
188
  def init_worker(model_path):
189
  global model
190
  model = YOLO(model_path)
191
- if MODEL_DEVICE == 'cuda':
192
- model.to('cuda')
193
 
194
- # not sure if we need this decorator anymore?
195
- #@ThreadingLocked()
196
  def process_single_image(img_path, results_dir):
197
  global model
198
  uuid_base = img_path.stem
@@ -202,10 +219,33 @@ def process_single_image(img_path, results_dir):
202
  pickle.dump(results, pf)
203
  return uuid_base
204
 
 
 
 
 
 
 
 
 
 
 
205
  @app.route('/process', methods=['POST'])
206
  def start_processing():
207
  session_id = session['id']
 
 
 
 
 
208
  upload_dir_check = Path(app.config['UPLOAD_FOLDER']) / session_id
 
 
 
 
 
 
 
 
209
  print(f"DEBUG /process: session_id={session_id}, upload_dir={upload_dir_check}, exists={upload_dir_check.exists()}")
210
  print(f"DEBUG /process: /tmp/nemaquant/uploads contents={list(Path(app.config['UPLOAD_FOLDER']).iterdir()) if Path(app.config['UPLOAD_FOLDER']).exists() else 'UPLOAD_FOLDER missing'}")
211
  job_state = {
@@ -226,21 +266,22 @@ def start_processing():
226
 
227
  try:
228
  if MODEL_DEVICE == 'cuda':
229
- n_proc = 1
 
 
 
 
 
 
 
 
230
  else:
231
  n_proc = os.cpu_count()
232
- # Initialize job state
233
- job_state = {
234
- "status": "starting",
235
- "progress": 0,
236
- "started": True
237
- }
238
- session['job_state'] = job_state
239
- pool = Pool(processes=n_proc,
240
- initializer=init_worker,
241
- initargs=(str(WEIGHTS_FILE),))
242
- async_results[session_id] = pool.starmap_async(process_single_image, arg_list)
243
- pool.close()
244
 
245
  # Update job state after process launch
246
  job_state["status"] = "processing"
@@ -258,8 +299,16 @@ def start_processing():
258
  @app.route('/progress')
259
  def get_progress():
260
  session_id = session['id']
 
 
 
 
 
261
  try:
262
  job_state = session.get('job_state')
 
 
 
263
  if not job_state:
264
  print("/progress: No job_state found in session.")
265
  return jsonify({"status": "error", "error": "No job state"}), 404
 
19
  from flask import Flask, Response, render_template, request, jsonify, send_file, session
20
  from multiprocessing.pool import Pool
21
  from multiprocessing import set_start_method
22
+ from concurrent.futures import ThreadPoolExecutor
23
  from pathlib import Path
24
  from PIL import Image
25
  from datetime import datetime
 
99
  # Load model once at startup, use CUDA if available
100
  MODEL_DEVICE = 'cuda' if cuda.is_available() else 'cpu'
101
 
102
+ # For GPU: load the model globally at startup so threads can reuse it without
103
+ # re-initialising CUDA (forked Pool workers cannot re-init CUDA in the child).
104
+ # For CPU: model is loaded per-worker in init_worker() instead.
105
+ _gpu_model = None
106
+ if MODEL_DEVICE == 'cuda':
107
+ _gpu_model = YOLO(str(WEIGHTS_FILE))
108
+ _gpu_model.to('cuda')
109
+ print(f'GPU model loaded at startup on {MODEL_DEVICE}')
110
+
111
+ # Wrapper so GPU futures (concurrent.futures.Future) expose the same
112
+ # .ready() interface as multiprocessing AsyncResult.
113
+ class _FutureWrapper:
114
+ def __init__(self, future):
115
+ self._f = future
116
+ def ready(self):
117
+ return self._f.done()
118
+ def get(self):
119
+ return self._f.result()
120
+
121
  # need a global dict to hold async results objects
122
  # so you can check the progress of an abr
123
  # maybe there's a better way around this?
 
164
  session['uuid_map_to_uuid_imgname'] = uuid_map_to_uuid_imgname
165
  # Persist to disk — cookie may be silently dropped if it exceeds ~4KB
166
  _save_session_meta(session_id, filename_map, uuid_map_to_uuid_imgname)
167
+ return jsonify({'filename_map': filename_map, 'session_id': session_id, 'status': 'uploaded'})
168
 
169
  # /preview route for serving original uploaded image
170
  @app.route('/preview', methods=['POST'])
 
204
  return jsonify({'error': str(e)}), 500
205
 
206
  # initializer for Pool to load model in each process
207
+ # each worker will have its own model instance (CPU only)
208
  def init_worker(model_path):
209
  global model
210
  model = YOLO(model_path)
 
 
211
 
212
+ # CPU pool worker uses per-worker model loaded by init_worker()
 
213
  def process_single_image(img_path, results_dir):
214
  global model
215
  uuid_base = img_path.stem
 
219
  pickle.dump(results, pf)
220
  return uuid_base
221
 
222
+ # GPU thread worker — reuses the global _gpu_model loaded at startup
223
+ def process_single_image_thread(img_path, results_dir):
224
+ global _gpu_model
225
+ uuid_base = img_path.stem
226
+ pickle_path = results_dir / f"{uuid_base}.pkl"
227
+ results = detect_in_image(_gpu_model, str(img_path))
228
+ with open(pickle_path, 'wb') as pf:
229
+ pickle.dump(results, pf)
230
+ return uuid_base
231
+
232
  @app.route('/process', methods=['POST'])
233
  def start_processing():
234
  session_id = session['id']
235
+ # The client echoes back the session_id it received from /uploads.
236
+ # On HF Spaces the session cookie can be missing on subsequent requests
237
+ # (HTTPS proxy / SameSite), so we fall back to the client-supplied id
238
+ # when the cookie-based id doesn't have an upload directory.
239
+ client_session_id = request.form.get('session_id', '')
240
  upload_dir_check = Path(app.config['UPLOAD_FOLDER']) / session_id
241
+ if not upload_dir_check.exists() and client_session_id:
242
+ fallback_dir = Path(app.config['UPLOAD_FOLDER']) / client_session_id
243
+ if fallback_dir.exists():
244
+ print(f"DEBUG /process: cookie session {session_id} has no upload dir; "
245
+ f"using client-supplied session {client_session_id}")
246
+ session_id = client_session_id
247
+ session['id'] = session_id
248
+ upload_dir_check = fallback_dir
249
  print(f"DEBUG /process: session_id={session_id}, upload_dir={upload_dir_check}, exists={upload_dir_check.exists()}")
250
  print(f"DEBUG /process: /tmp/nemaquant/uploads contents={list(Path(app.config['UPLOAD_FOLDER']).iterdir()) if Path(app.config['UPLOAD_FOLDER']).exists() else 'UPLOAD_FOLDER missing'}")
251
  job_state = {
 
266
 
267
  try:
268
  if MODEL_DEVICE == 'cuda':
269
+ # GPU: run in a single thread so CUDA is never re-initialised in a
270
+ # forked subprocess (Pool uses fork by default, which breaks CUDA).
271
+ def _gpu_task():
272
+ for img_path, res_dir in arg_list:
273
+ process_single_image_thread(img_path, res_dir)
274
+ executor = ThreadPoolExecutor(max_workers=1)
275
+ future = executor.submit(_gpu_task)
276
+ executor.shutdown(wait=False)
277
+ async_results[session_id] = _FutureWrapper(future)
278
  else:
279
  n_proc = os.cpu_count()
280
+ pool = Pool(processes=n_proc,
281
+ initializer=init_worker,
282
+ initargs=(str(WEIGHTS_FILE),))
283
+ async_results[session_id] = pool.starmap_async(process_single_image, arg_list)
284
+ pool.close()
 
 
 
 
 
 
 
285
 
286
  # Update job state after process launch
287
  job_state["status"] = "processing"
 
299
  @app.route('/progress')
300
  def get_progress():
301
  session_id = session['id']
302
+ # Accept client-supplied session_id as fallback (cookie may be missing on HF Spaces)
303
+ client_session_id = request.args.get('session_id', '')
304
+ if client_session_id and session_id not in async_results and client_session_id in async_results:
305
+ session_id = client_session_id
306
+ session['id'] = session_id
307
  try:
308
  job_state = session.get('job_state')
309
+ # If session lost job_state but we have an async_result, reconstruct from disk
310
+ if not job_state and session_id in async_results:
311
+ job_state = {'status': 'processing', 'progress': 0, 'sessionId': session_id}
312
  if not job_state:
313
  print("/progress: No job_state found in session.")
314
  return jsonify({"status": "error", "error": "No job state"}), 404
static/script.js CHANGED
@@ -33,6 +33,7 @@ document.addEventListener('DOMContentLoaded', () => {
33
  let currentJobId = null;
34
  let currentZoomLevel = 1;
35
  let filenameMap = {};
 
36
  const MAX_ZOOM = 3;
37
  const MIN_ZOOM = 0.5;
38
  let progressInterval = null; // Interval timer for polling
@@ -253,6 +254,7 @@ document.addEventListener('DOMContentLoaded', () => {
253
  const data = await response.json();
254
  logStatus('Files uploaded successfully.');
255
  filenameMap = data.filename_map || {};
 
256
 
257
  // Update results table with filenames and View buttons
258
  resultsTableBody.innerHTML = '';
@@ -358,6 +360,9 @@ document.addEventListener('DOMContentLoaded', () => {
358
  }
359
  formData.append('input_mode', mode);
360
  formData.append('confidence_threshold', confidenceSlider.value);
 
 
 
361
 
362
  try {
363
  const response = await fetch('/process', {
@@ -541,7 +546,7 @@ document.addEventListener('DOMContentLoaded', () => {
541
 
542
  progressInterval = setInterval(async () => {
543
  try {
544
- const response = await fetch('/progress', { credentials: 'include' });
545
  if (!response.ok) {
546
  let errorText = `Progress check failed: ${response.status}`;
547
  try {
 
33
  let currentJobId = null;
34
  let currentZoomLevel = 1;
35
  let filenameMap = {};
36
+ let uploadSessionId = ''; // echoed back to /process as cookie-independent fallback
37
  const MAX_ZOOM = 3;
38
  const MIN_ZOOM = 0.5;
39
  let progressInterval = null; // Interval timer for polling
 
254
  const data = await response.json();
255
  logStatus('Files uploaded successfully.');
256
  filenameMap = data.filename_map || {};
257
+ uploadSessionId = data.session_id || '';
258
 
259
  // Update results table with filenames and View buttons
260
  resultsTableBody.innerHTML = '';
 
360
  }
361
  formData.append('input_mode', mode);
362
  formData.append('confidence_threshold', confidenceSlider.value);
363
+ // Send back the session_id from /uploads so the server can recover the
364
+ // correct upload directory when the session cookie is missing (HF Spaces).
365
+ if (uploadSessionId) formData.append('session_id', uploadSessionId);
366
 
367
  try {
368
  const response = await fetch('/process', {
 
546
 
547
  progressInterval = setInterval(async () => {
548
  try {
549
+ const response = await fetch(`/progress?session_id=${encodeURIComponent(uploadSessionId)}`, { credentials: 'include' });
550
  if (!response.ok) {
551
  let errorText = `Progress check failed: ${response.status}`;
552
  try {