Add warning if files present in output folder, fix cancellation button

This commit is contained in:
Arnaud_Cayrol 2025-04-07 20:53:42 +02:00
parent 23939cccdd
commit e7075dbec7
3 changed files with 87 additions and 21 deletions

58
main.py
View file

@ -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)))

View file

@ -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...
</div>
{% if warning %}
<div class="warning">{{ warning }}</div>
{% endif %}
{% if error %}
<p class="error">{{ error }}</p>
{% endif %}

View file

@ -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