diff --git a/main.py b/main.py index fc209ce..8d30d98 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,8 @@ import subprocess from flask import Flask, request, render_template, jsonify, redirect, url_for from timelapse import process_faces, ProcessConfig, validate_immich_connection import logging +import uuid +from datetime import datetime app = Flask(__name__) @@ -46,98 +48,50 @@ def update_progress(current, total): progress_info["status"] = "running" if current < total else "done" -def background_process(person_id, padding_percent, resize_size, face_resolution_threshold, pose_threshold, - left_eye_pos, - max_workers, date_from, date_to, compile_video, framerate): +def background_process(person_id, resize_size, face_resolution_threshold, pose_threshold, + left_eye_pos, output_folder, api_key, base_url, progress_callback=None, cancel_flag=None): """ - Background process that creates a configuration object and calls process_faces. + Background process to handle face alignment and timelapse creation. Args: - person_id (str): The target person ID. - padding_percent (float): Padding percentage for face cropping. - resize_size (int): Desired width and height for the aligned face image. - face_resolution_threshold (int): Minimum required face resolution. + person_id (str): ID of the person to process. + resize_size (int): Size to resize the output images to. + face_resolution_threshold (int): Minimum face resolution threshold. pose_threshold (float): Maximum allowed head pose deviation. - left_eye_pos (tuple): The desired relative position of the left eye. - max_workers (int): Number of concurrent worker processes. - date_from (str): Start date for asset filtering. - date_to (str): End date for asset filtering. - compile_video (bool): Whether to compile the images into a video. - framerate (int): Frames per second for the output video. + left_eye_pos (tuple): Desired position of the left eye in the output. + output_folder (str): Folder to save the output images. + api_key (str): API key for authentication. + base_url (str): Base URL of the API. + progress_callback (callable, optional): Callback for progress updates. + cancel_flag (callable, optional): Function to check if process should be cancelled. """ - global cancel_requested try: - progress_info["status"] = "running" - # Build the configuration object for processing config = ProcessConfig( - api_key=API_KEY, - base_url=BASE_URL, + api_key=api_key, + base_url=base_url, person_id=person_id, - output_folder=OUTPUT_FOLDER, - padding_percent=padding_percent, + output_folder=output_folder, resize_width=resize_size, resize_height=resize_size, min_face_width=face_resolution_threshold, min_face_height=face_resolution_threshold, pose_threshold=pose_threshold, - left_eye_pos=left_eye_pos, - landmark_model_path=LANDMARK_MODEL + left_eye_pos=left_eye_pos ) - processed_files = process_faces(config, max_workers=max_workers, progress_callback=update_progress, - date_from=date_from, date_to=date_to, cancel_flag=lambda: cancel_requested) + # Process the faces + processed_files = process_faces( + config=config, + max_workers=1, + progress_callback=progress_callback, + cancel_flag=cancel_flag + ) - # 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: - output_video = os.path.join(OUTPUT_FOLDER, "timelapse.mp4") - ffmpeg_command = [ - "ffmpeg", "-y", - "-framerate", str(framerate), - "-pattern_type", "glob", - "-i", os.path.join(OUTPUT_FOLDER, "*.jpg"), - "-c:v", "libx264", - "-pix_fmt", "yuv420p", - "-progress", "pipe:1", - output_video - ] - try: - progress_info["completed"] = 0 - progress_info["total"] = 100 - progress_info["status"] = "video_compiling" - - process = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - universal_newlines=True) - for line in process.stdout: - line = line.strip() - if line: - if line.startswith("frame="): - try: - parts = line.split("=") - frame = int(parts[1].strip()) - progress_info["completed"] = min(frame, progress_info["total"]) - except Exception: - pass - if "progress=end" in line: - progress_info["completed"] = progress_info["total"] - break - process.wait() - if process.returncode == 0: - progress_info["status"] = "video_done" - else: - progress_info["status"] = "Video compilation failed." - except subprocess.CalledProcessError as e: - progress_info["status"] = f"Video compilation failed: {e}" - else: - progress_info["status"] = "done" + return processed_files except Exception as e: - progress_info["status"] = f"error: {e}" + logger.error(f"Error in background process: {str(e)}") + raise def check_output_folder(): @@ -190,6 +144,51 @@ def cancel(): return jsonify({"success": False, "message": "No active processing to cancel."}) +@app.route("/process", methods=["POST"]) +def process(): + """Handle the processing request.""" + try: + person_id = request.form.get("person_id") + if not person_id: + return jsonify({"error": "Person ID is required"}), 400 + + resize_size = int(request.form.get("resize_size", 512)) + face_resolution_threshold = int(request.form.get("face_resolution_threshold", 128)) + pose_threshold = float(request.form.get("pose_threshold", 25)) + left_eye_x = float(request.form.get("left_eye_x", 0.4)) + left_eye_y = float(request.form.get("left_eye_y", 0.4)) + left_eye_pos = (left_eye_x, left_eye_y) + + # Create output folder with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output_folder = os.path.join("output", f"timelapse_{timestamp}") + os.makedirs(output_folder, exist_ok=True) + + # Start background process + process_id = str(uuid.uuid4()) + process_info = { + "status": "running", + "start_time": datetime.now().isoformat(), + "output_folder": output_folder + } + active_processes[process_id] = process_info + + # Start the background process + process = multiprocessing.Process( + target=background_process, + args=(person_id, resize_size, face_resolution_threshold, pose_threshold, + left_eye_pos, output_folder, API_KEY, BASE_URL) + ) + process.start() + active_processes[process_id]["process"] = process + + return jsonify({"process_id": process_id}) + + except Exception as e: + logger.error(f"Error in process route: {str(e)}") + return jsonify({"error": str(e)}), 500 + + @app.route("/", methods=["GET", "POST"]) def index(): """ @@ -219,12 +218,12 @@ def index(): 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)) face_resolution_threshold = int(request.form.get("face_resolution_threshold", 128)) pose_threshold = float(request.form.get("pose_threshold", 25)) - max_workers = int(request.form.get("max_workers", 1)) - left_eye_pos = (0.4, 0.4) + left_eye_x = float(request.form.get("left_eye_x", 0.4)) + left_eye_y = float(request.form.get("left_eye_y", 0.4)) + left_eye_pos = (left_eye_x, left_eye_y) # Date ranges are optional date_from = request.form.get("date_from") or None @@ -242,8 +241,8 @@ def index(): # Start the processing in a background thread processing_thread = threading.Thread( target=background_process, - args=(person_id, padding_percent, resize_size, face_resolution_threshold, pose_threshold, - left_eye_pos, max_workers, date_from, date_to, compile_video, framerate) + args=(person_id, resize_size, face_resolution_threshold, pose_threshold, + left_eye_pos, OUTPUT_FOLDER, API_KEY, BASE_URL, update_progress, lambda: cancel_requested) ) processing_thread.start() result = "Processing started. Please wait and watch the progress bar below." diff --git a/templates/index.html b/templates/index.html index 077b659..8507dbe 100644 --- a/templates/index.html +++ b/templates/index.html @@ -188,28 +188,19 @@
Face Processing Settings
- + + + Width and height of the output images in pixels.
- + + + Minimum width/height of detected faces in pixels.
- -
-
- + + + Maximum allowed head pose deviation in degrees.