Add warning if files present in output folder, fix cancellation button
This commit is contained in:
parent
23939cccdd
commit
e7075dbec7
3 changed files with 87 additions and 21 deletions
58
main.py
58
main.py
|
|
@ -8,9 +8,9 @@ from timelapse import process_faces, ProcessConfig, validate_immich_connection
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Critical parameters provided via environment variables
|
# Critical parameters provided via environment variables
|
||||||
API_KEY = os.environ.get("IMMICH_API_KEY", "")
|
API_KEY = "4ryTBWjXRsaTUHMfOurAC1fcBktEgtoZvMR3lwZUluI"
|
||||||
BASE_URL = os.environ.get("IMMICH_BASE_URL", "")
|
BASE_URL = "http://192.168.1.94:2283/api"
|
||||||
OUTPUT_FOLDER = os.environ.get("OUTPUT_FOLDER", "output")
|
OUTPUT_FOLDER = "output"
|
||||||
|
|
||||||
# Model paths
|
# Model paths
|
||||||
FACE_DETECT_MODEL = "mmod_human_face_detector.dat"
|
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"}
|
progress_info = {"completed": 0, "total": 0, "status": "idle"}
|
||||||
# Global processing thread reference
|
# Global processing thread reference
|
||||||
processing_thread = None
|
processing_thread = None
|
||||||
|
# Global flag to signal cancellation
|
||||||
|
cancel_requested = False
|
||||||
|
|
||||||
|
|
||||||
def update_progress(current, total):
|
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.
|
compile_video (bool): Whether to compile the images into a video.
|
||||||
framerate (int): Frames per second for the output video.
|
framerate (int): Frames per second for the output video.
|
||||||
"""
|
"""
|
||||||
|
global cancel_requested
|
||||||
try:
|
try:
|
||||||
progress_info["status"] = "running"
|
progress_info["status"] = "running"
|
||||||
# Build the configuration object for processing
|
# 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,
|
face_detect_model_path=FACE_DETECT_MODEL,
|
||||||
landmark_model_path=LANDMARK_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,
|
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
|
# Compile video if requested and there are processed files
|
||||||
if compile_video and 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}"
|
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")
|
@app.route("/progress")
|
||||||
def progress():
|
def progress():
|
||||||
"""
|
"""
|
||||||
|
|
@ -122,12 +148,16 @@ def cancel():
|
||||||
"""
|
"""
|
||||||
Endpoint to cancel the current processing job.
|
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():
|
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"
|
progress_info["status"] = "cancelled"
|
||||||
return jsonify({"success": True, "message": "Processing cancelled."})
|
return jsonify({"success": True, "message": "Processing cancelled."})
|
||||||
else:
|
else:
|
||||||
|
cancel_requested = False
|
||||||
return jsonify({"success": False, "message": "No active processing to cancel."})
|
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.
|
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
|
result = None
|
||||||
error = 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
|
# Check if Immich server connection is valid
|
||||||
is_valid, message = validate_immich_connection(API_KEY, BASE_URL)
|
is_valid, message = validate_immich_connection(API_KEY, BASE_URL)
|
||||||
if not is_valid and request.method == "POST":
|
if not is_valid and request.method == "POST":
|
||||||
error = f"Immich server connection error: {message}"
|
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":
|
if request.method == "POST":
|
||||||
try:
|
try:
|
||||||
|
# Make sure any previous cancel request is cleared
|
||||||
|
cancel_requested = False
|
||||||
|
|
||||||
person_id = request.form["person_id"]
|
person_id = request.form["person_id"]
|
||||||
padding_percent = float(request.form.get("padding_percent", 30)) / 100
|
padding_percent = float(request.form.get("padding_percent", 30)) / 100
|
||||||
resize_size = int(request.form.get("resize_size", 512))
|
resize_size = int(request.form.get("resize_size", 512))
|
||||||
|
|
@ -180,7 +220,7 @@ def index():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error = f"Error processing request: {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)))
|
max_workers_options=list(range(1, AVAILABLE_CORES + 1)))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,14 @@
|
||||||
background-color: #f8d7da;
|
background-color: #f8d7da;
|
||||||
color: #721c24;
|
color: #721c24;
|
||||||
}
|
}
|
||||||
|
.warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
form {
|
form {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +153,10 @@
|
||||||
Checking Immich server connection...
|
Checking Immich server connection...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if warning %}
|
||||||
|
<div class="warning">{{ warning }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<p class="error">{{ error }}</p>
|
<p class="error">{{ error }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
38
timelapse.py
38
timelapse.py
|
|
@ -392,7 +392,10 @@ def process_asset_wrapper(args):
|
||||||
return process_asset_worker(asset, config)
|
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.
|
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.
|
progress_callback (callable, optional): A callback function for progress updates.
|
||||||
date_from (str, optional): Start date for filtering assets.
|
date_from (str, optional): Start date for filtering assets.
|
||||||
date_to (str, optional): End 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:
|
Returns:
|
||||||
list: A list of file paths of the saved images.
|
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)
|
os.makedirs(config.output_folder, exist_ok=True)
|
||||||
|
|
||||||
# Check if we need to stop processing
|
# Check if we need to stop processing
|
||||||
if progress_callback and hasattr(progress_callback, "__self__") and getattr(progress_callback.__self__, "status",
|
if cancel_flag and cancel_flag():
|
||||||
"") == "cancelled":
|
|
||||||
logger.info("Processing was cancelled.")
|
logger.info("Processing was cancelled.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -432,17 +435,28 @@ def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None,
|
||||||
initargs=initializer_args) as executor:
|
initargs=initializer_args) as executor:
|
||||||
# Pack arguments for each asset
|
# Pack arguments for each asset
|
||||||
tasks = ((asset, config) for asset in assets)
|
tasks = ((asset, config) for asset in assets)
|
||||||
for result in tqdm(executor.map(process_asset_wrapper, tasks), total=total_assets):
|
future_to_asset = {executor.submit(process_asset_wrapper, args): args[0] for args in
|
||||||
# Check if we need to stop processing
|
((asset, config) for asset in assets)}
|
||||||
if progress_callback and hasattr(progress_callback, "__self__") and getattr(progress_callback.__self__,
|
|
||||||
"status", "") == "cancelled":
|
for future in tqdm(concurrent.futures.as_completed(future_to_asset), total=total_assets):
|
||||||
logger.info("Processing was cancelled.")
|
# Check if we need to stop processing
|
||||||
executor.shutdown(wait=False)
|
if cancel_flag and cancel_flag():
|
||||||
break
|
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:
|
if progress_callback:
|
||||||
progress_callback(len(processed_files), total_assets)
|
progress_callback(len(processed_files), total_assets)
|
||||||
|
|
||||||
logger.info(f"Finished processing. {len(processed_files)} images saved.")
|
logger.info(f"Finished processing. {len(processed_files)} images saved.")
|
||||||
return processed_files
|
return processed_files
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue