diff --git a/main.py b/main.py index 3bbb68a..abb0b72 100644 --- a/main.py +++ b/main.py @@ -8,9 +8,9 @@ from timelapse import process_faces, ProcessConfig, validate_immich_connection app = Flask(__name__) # Critical parameters provided via environment variables -API_KEY = os.environ.get("IMMICH_API_KEY", "") -BASE_URL = os.environ.get("IMMICH_BASE_URL", "") -OUTPUT_FOLDER = os.environ.get("OUTPUT_FOLDER", "output") +API_KEY = "4ryTBWjXRsaTUHMfOurAC1fcBktEgtoZvMR3lwZUluI" +BASE_URL = "http://192.168.1.94:2283/api" +OUTPUT_FOLDER = "output" # Model paths FACE_DETECT_MODEL = "mmod_human_face_detector.dat" @@ -23,6 +23,8 @@ AVAILABLE_CORES = multiprocessing.cpu_count() progress_info = {"completed": 0, "total": 0, "status": "idle"} # Global processing thread reference processing_thread = None +# Global flag to signal cancellation +cancel_requested = False def update_progress(current, total): @@ -55,6 +57,7 @@ def background_process(person_id, padding_percent, resize_size, face_resolution_ compile_video (bool): Whether to compile the images into a video. framerate (int): Frames per second for the output video. """ + global cancel_requested try: progress_info["status"] = "running" # Build the configuration object for processing @@ -73,8 +76,16 @@ def background_process(person_id, padding_percent, resize_size, face_resolution_ face_detect_model_path=FACE_DETECT_MODEL, landmark_model_path=LANDMARK_MODEL ) + + # Pass the cancel flag to the process_faces function processed_files = process_faces(config, max_workers=max_workers, progress_callback=update_progress, - date_from=date_from, date_to=date_to) + date_from=date_from, date_to=date_to, cancel_flag=lambda: cancel_requested) + + # Check if processing was cancelled + if cancel_requested: + progress_info["status"] = "cancelled" + cancel_requested = False + return # Compile video if requested and there are processed files if compile_video and processed_files: @@ -100,6 +111,21 @@ def background_process(person_id, padding_percent, resize_size, face_resolution_ progress_info["status"] = f"error: {e}" +def check_output_folder(): + """ + Checks if the output folder is empty. + + Returns: + tuple: (is_empty, file_count) - Boolean indicating if folder is empty and number of files + """ + if not os.path.exists(OUTPUT_FOLDER): + os.makedirs(OUTPUT_FOLDER, exist_ok=True) + return True, 0 + + files = [f for f in os.listdir(OUTPUT_FOLDER) if os.path.isfile(os.path.join(OUTPUT_FOLDER, f))] + return len(files) == 0, len(files) + + @app.route("/progress") def progress(): """ @@ -122,12 +148,16 @@ def cancel(): """ Endpoint to cancel the current processing job. """ - global processing_thread + global processing_thread, cancel_requested + + # Set the cancel flag + cancel_requested = True + if processing_thread and processing_thread.is_alive(): - # We can't truly kill a thread in Python, but we can signal it to stop progress_info["status"] = "cancelled" return jsonify({"success": True, "message": "Processing cancelled."}) else: + cancel_requested = False return jsonify({"success": False, "message": "No active processing to cancel."}) @@ -136,19 +166,29 @@ def index(): """ Index route that displays the form and starts processing in a background thread on POST. """ - global processing_thread + global processing_thread, cancel_requested result = None error = None + warning = None + + # Check if output folder is empty + is_empty, file_count = check_output_folder() + if not is_empty: + warning = f"Output folder is not empty. Contains {file_count} files. New images will be added to this folder." # Check if Immich server connection is valid is_valid, message = validate_immich_connection(API_KEY, BASE_URL) if not is_valid and request.method == "POST": error = f"Immich server connection error: {message}" - return render_template("index.html", error=error, max_workers_options=list(range(1, AVAILABLE_CORES + 1))) + return render_template("index.html", error=error, warning=warning, + max_workers_options=list(range(1, AVAILABLE_CORES + 1))) if request.method == "POST": try: + # Make sure any previous cancel request is cleared + cancel_requested = False + person_id = request.form["person_id"] padding_percent = float(request.form.get("padding_percent", 30)) / 100 resize_size = int(request.form.get("resize_size", 512)) @@ -180,7 +220,7 @@ def index(): except Exception as e: error = f"Error processing request: {e}" - return render_template("index.html", result=result, error=error, + return render_template("index.html", result=result, error=error, warning=warning, max_workers_options=list(range(1, AVAILABLE_CORES + 1))) diff --git a/templates/index.html b/templates/index.html index b950195..077b659 100644 --- a/templates/index.html +++ b/templates/index.html @@ -36,6 +36,14 @@ background-color: #f8d7da; color: #721c24; } + .warning { + background-color: #fff3cd; + color: #856404; + text-align: center; + padding: 10px; + margin: 15px 0; + border-radius: 4px; + } form { margin-top: 20px; } @@ -145,6 +153,10 @@ Checking Immich server connection... + {% if warning %} +
{{ error }}
{% endif %} diff --git a/timelapse.py b/timelapse.py index 666b8f0..4c7faa2 100644 --- a/timelapse.py +++ b/timelapse.py @@ -392,7 +392,10 @@ def process_asset_wrapper(args): return process_asset_worker(asset, config) -def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None, date_from=None, date_to=None): +# Replacing only the process_faces function in timelapse.py + +def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None, date_from=None, date_to=None, + cancel_flag=None): """ Processes assets containing the person and saves aligned face images. @@ -405,6 +408,7 @@ def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None, progress_callback (callable, optional): A callback function for progress updates. date_from (str, optional): Start date for filtering assets. date_to (str, optional): End date for filtering assets. + cancel_flag (callable, optional): A function that returns True if processing should be cancelled. Returns: list: A list of file paths of the saved images. @@ -412,8 +416,7 @@ def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None, os.makedirs(config.output_folder, exist_ok=True) # Check if we need to stop processing - if progress_callback and hasattr(progress_callback, "__self__") and getattr(progress_callback.__self__, "status", - "") == "cancelled": + if cancel_flag and cancel_flag(): logger.info("Processing was cancelled.") return [] @@ -432,17 +435,28 @@ def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None, initargs=initializer_args) as executor: # Pack arguments for each asset tasks = ((asset, config) for asset in assets) - for result in tqdm(executor.map(process_asset_wrapper, tasks), total=total_assets): - # Check if we need to stop processing - if progress_callback and hasattr(progress_callback, "__self__") and getattr(progress_callback.__self__, - "status", "") == "cancelled": - logger.info("Processing was cancelled.") - executor.shutdown(wait=False) - break + future_to_asset = {executor.submit(process_asset_wrapper, args): args[0] for args in + ((asset, config) for asset in assets)} + + for future in tqdm(concurrent.futures.as_completed(future_to_asset), total=total_assets): + # Check if we need to stop processing + if cancel_flag and cancel_flag(): + logger.info("Processing was cancelled.") + # Cancel all pending futures + for f in future_to_asset: + f.cancel() + executor.shutdown(wait=False) + return processed_files + + try: + result = future.result() + if result is not None: + processed_files.append(result) + except Exception as e: + logger.info(f"Asset processing failed: {e}") - if result is not None: - processed_files.append(result) if progress_callback: progress_callback(len(processed_files), total_assets) + logger.info(f"Finished processing. {len(processed_files)} images saved.") return processed_files