Add api cred validation, cancel button, ffmpeg timelapse and date range
This commit is contained in:
parent
399b09960d
commit
23939cccdd
3 changed files with 392 additions and 53 deletions
98
main.py
98
main.py
|
|
@ -1,8 +1,9 @@
|
|||
import os
|
||||
import multiprocessing
|
||||
import threading
|
||||
from flask import Flask, request, render_template, jsonify
|
||||
from timelapse import process_faces, ProcessConfig
|
||||
import subprocess
|
||||
from flask import Flask, request, render_template, jsonify, redirect, url_for
|
||||
from timelapse import process_faces, ProcessConfig, validate_immich_connection
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
|
@ -20,6 +21,8 @@ AVAILABLE_CORES = multiprocessing.cpu_count()
|
|||
|
||||
# Global progress dictionary – only one job at a time is assumed here
|
||||
progress_info = {"completed": 0, "total": 0, "status": "idle"}
|
||||
# Global processing thread reference
|
||||
processing_thread = None
|
||||
|
||||
|
||||
def update_progress(current, total):
|
||||
|
|
@ -35,7 +38,8 @@ 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, max_workers):
|
||||
def background_process(person_id, padding_percent, resize_size, face_resolution_threshold, pose_threshold, max_workers,
|
||||
date_from, date_to, compile_video, framerate):
|
||||
"""
|
||||
Background process that creates a configuration object and calls process_faces.
|
||||
|
||||
|
|
@ -46,6 +50,10 @@ def background_process(person_id, padding_percent, resize_size, face_resolution_
|
|||
face_resolution_threshold (int): Minimum required face resolution.
|
||||
pose_threshold (float): Maximum allowed head pose deviation.
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
progress_info["status"] = "running"
|
||||
|
|
@ -65,11 +73,31 @@ def background_process(person_id, padding_percent, resize_size, face_resolution_
|
|||
face_detect_model_path=FACE_DETECT_MODEL,
|
||||
landmark_model_path=LANDMARK_MODEL
|
||||
)
|
||||
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)
|
||||
|
||||
# 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",
|
||||
output_video
|
||||
]
|
||||
try:
|
||||
subprocess.run(ffmpeg_command, check=True)
|
||||
progress_info["video_path"] = output_video
|
||||
progress_info["status"] = "video_done"
|
||||
except subprocess.CalledProcessError as e:
|
||||
progress_info["status"] = f"Video compilation failed: {e}"
|
||||
else:
|
||||
progress_info["status"] = "done"
|
||||
except Exception as e:
|
||||
progress_info["status"] = f"error: {e}"
|
||||
else:
|
||||
progress_info["status"] = "done"
|
||||
|
||||
|
||||
@app.route("/progress")
|
||||
|
|
@ -80,15 +108,45 @@ def progress():
|
|||
return jsonify(progress_info)
|
||||
|
||||
|
||||
@app.route("/check-connection")
|
||||
def check_connection():
|
||||
"""
|
||||
Endpoint to check the Immich server connection.
|
||||
"""
|
||||
is_valid, message = validate_immich_connection(API_KEY, BASE_URL)
|
||||
return jsonify({"valid": is_valid, "message": message})
|
||||
|
||||
|
||||
@app.route("/cancel", methods=["POST"])
|
||||
def cancel():
|
||||
"""
|
||||
Endpoint to cancel the current processing job.
|
||||
"""
|
||||
global processing_thread
|
||||
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:
|
||||
return jsonify({"success": False, "message": "No active processing to cancel."})
|
||||
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def index():
|
||||
"""
|
||||
Index route that displays the form and starts processing in a background thread on POST.
|
||||
"""
|
||||
global processing_thread
|
||||
|
||||
result = None
|
||||
error = None
|
||||
# Create max_workers_options as a list from 1 to AVAILABLE_CORES
|
||||
max_workers_options = list(range(1, AVAILABLE_CORES + 1))
|
||||
|
||||
# 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)))
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
person_id = request.form["person_id"]
|
||||
|
|
@ -96,23 +154,35 @@ def index():
|
|||
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)) # default is 1
|
||||
max_workers = int(request.form.get("max_workers", 1))
|
||||
|
||||
# Date ranges are optional
|
||||
date_from = request.form.get("date_from") or None
|
||||
date_to = request.form.get("date_to") or None
|
||||
|
||||
compile_video = request.form.get("compile_video") == "on"
|
||||
framerate = int(request.form.get("framerate", 24))
|
||||
|
||||
# Reset progress info before starting
|
||||
progress_info["completed"] = 0
|
||||
progress_info["total"] = 0
|
||||
progress_info["status"] = "idle"
|
||||
progress_info.pop("video_path", None)
|
||||
|
||||
# Start the processing in a background thread
|
||||
threading.Thread(
|
||||
processing_thread = threading.Thread(
|
||||
target=background_process,
|
||||
args=(person_id, padding_percent, resize_size, face_resolution_threshold, pose_threshold, max_workers)
|
||||
).start()
|
||||
args=(person_id, padding_percent, resize_size, face_resolution_threshold, pose_threshold, max_workers,
|
||||
date_from, date_to, compile_video, framerate)
|
||||
)
|
||||
processing_thread.start()
|
||||
result = "Processing started. Please wait and watch the progress bar below."
|
||||
except Exception as e:
|
||||
error = f"Error processing request: {e}"
|
||||
return render_template("index.html", result=result, error=error, max_workers_options=max_workers_options)
|
||||
|
||||
return render_template("index.html", result=result, error=error,
|
||||
max_workers_options=list(range(1, AVAILABLE_CORES + 1)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
|
|
@ -22,9 +22,26 @@
|
|||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
.connection-status {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
margin: 15px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.connected {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.disconnected {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
|
|
@ -32,6 +49,7 @@
|
|||
}
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="date"],
|
||||
select {
|
||||
width: calc(100% - 20px);
|
||||
padding: 8px 10px;
|
||||
|
|
@ -40,7 +58,16 @@
|
|||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
button {
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.checkbox-container label {
|
||||
margin-left: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.button-primary {
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
|
|
@ -51,9 +78,27 @@
|
|||
display: block;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
button:hover {
|
||||
.button-primary:hover {
|
||||
background: #218838;
|
||||
}
|
||||
.button-primary:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.button-danger {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin: 10px auto 0;
|
||||
}
|
||||
.button-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
.result, .error {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
|
|
@ -73,44 +118,114 @@
|
|||
width: 100%;
|
||||
height: 25px;
|
||||
}
|
||||
.section-header {
|
||||
margin-top: 25px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.flex-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
.flex-row > div {
|
||||
flex: 1;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Immich Selfie Timelapse Tool</h1>
|
||||
|
||||
<!-- Connection Status -->
|
||||
<div id="connectionStatus" class="connection-status">
|
||||
Checking Immich server connection...
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
<label>
|
||||
Person ID:
|
||||
<input type="text" name="person_id" required>
|
||||
</label>
|
||||
<label>
|
||||
Padding added around the face in percent:
|
||||
<input type="number" step="1" name="padding_percent" value="{{ request.form.padding_percent or 30 }}">
|
||||
</label>
|
||||
<label>
|
||||
Output image size:
|
||||
<input type="number" name="resize_size" value="{{ request.form.resize_size or 512 }}">
|
||||
</label>
|
||||
<label>
|
||||
Face resolution threshold:
|
||||
<input type="number" name="face_resolution_threshold" value="{{ request.form.face_resolution_threshold or 128 }}">
|
||||
</label>
|
||||
<label>
|
||||
Max orientation of the face (degrees):
|
||||
<input type="number" step="0.1" name="pose_threshold" value="{{ request.form.pose_threshold or 25 }}">
|
||||
</label>
|
||||
<label>
|
||||
Max Workers (CPU cores):
|
||||
<select name="max_workers">
|
||||
{% for i in max_workers_options %}
|
||||
<option value="{{ i }}" {% if request.form.max_workers|default('1')|int == i %}selected{% endif %}>{{ i }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Generate Timelapse</button>
|
||||
|
||||
<form method="post" id="timelapseForm">
|
||||
<div class="section-header">Person Selection</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Person ID:
|
||||
<input type="text" name="person_id" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="section-header">Date Range (Optional)</div>
|
||||
<div class="flex-row">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
From Date:
|
||||
<input type="date" name="date_from">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
To Date:
|
||||
<input type="date" name="date_to">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">Face Processing Settings</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Padding added around the face in percent:
|
||||
<input type="number" step="1" name="padding_percent" value="{{ request.form.padding_percent or 30 }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Output image size:
|
||||
<input type="number" name="resize_size" value="{{ request.form.resize_size or 512 }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Face resolution threshold:
|
||||
<input type="number" name="face_resolution_threshold" value="{{ request.form.face_resolution_threshold or 128 }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Max orientation of the face (degrees):
|
||||
<input type="number" step="0.1" name="pose_threshold" value="{{ request.form.pose_threshold or 25 }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Max Workers (CPU cores):
|
||||
<select name="max_workers">
|
||||
{% for i in max_workers_options %}
|
||||
<option value="{{ i }}" {% if request.form.max_workers|default('1')|int == i %}selected{% endif %}>{{ i }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="section-header">Video Output</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox-container">
|
||||
<input type="checkbox" id="compile_video" name="compile_video">
|
||||
<label for="compile_video">Compile timelapse into video</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="framerateGroup" style="display:none;">
|
||||
<label>
|
||||
Frames per second:
|
||||
<input type="number" name="framerate" value="24" min="1" max="60">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitButton" class="button-primary">Generate Timelapse</button>
|
||||
<button type="button" id="cancelButton" class="button-danger" style="display:none;">Cancel Processing</button>
|
||||
</form>
|
||||
{% if result %}
|
||||
<p class="result">{{ result }}</p>
|
||||
|
|
@ -119,26 +234,115 @@
|
|||
<div id="progressContainer">
|
||||
<progress id="progressBar" value="0" max="100"></progress>
|
||||
<p id="progressText">0%</p>
|
||||
<p id="videoResult" style="display:none;">
|
||||
<a id="videoLink" href="#" download>Download Video</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Check connection status when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkConnectionStatus();
|
||||
});
|
||||
|
||||
function checkConnectionStatus() {
|
||||
fetch('/check-connection')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const statusDiv = document.getElementById('connectionStatus');
|
||||
const submitButton = document.getElementById('submitButton');
|
||||
|
||||
if (data.valid) {
|
||||
statusDiv.className = 'connection-status connected';
|
||||
statusDiv.textContent = 'Connected to Immich server';
|
||||
submitButton.disabled = false;
|
||||
} else {
|
||||
statusDiv.className = 'connection-status disconnected';
|
||||
statusDiv.textContent = 'Error: ' + data.message;
|
||||
submitButton.disabled = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking connection:', error);
|
||||
const statusDiv = document.getElementById('connectionStatus');
|
||||
statusDiv.className = 'connection-status disconnected';
|
||||
statusDiv.textContent = 'Error checking Immich server connection';
|
||||
document.getElementById('submitButton').disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle framerate field visibility based on checkbox
|
||||
document.getElementById('compile_video').addEventListener('change', function() {
|
||||
document.getElementById('framerateGroup').style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Cancel button
|
||||
document.getElementById('cancelButton').addEventListener('click', function() {
|
||||
fetch('/cancel', {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('progressText').textContent = 'Processing cancelled.';
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
});
|
||||
|
||||
// Progress monitoring
|
||||
let isProcessing = false;
|
||||
function fetchProgress() {
|
||||
fetch('/progress')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const progressBar = document.getElementById("progressBar");
|
||||
const progressText = document.getElementById("progressText");
|
||||
if (data.total > 0) {
|
||||
const cancelButton = document.getElementById("cancelButton");
|
||||
const submitButton = document.getElementById("submitButton");
|
||||
const videoResult = document.getElementById("videoResult");
|
||||
const videoLink = document.getElementById("videoLink");
|
||||
|
||||
// Show/hide cancel button based on status
|
||||
if (data.status === "running") {
|
||||
isProcessing = true;
|
||||
cancelButton.style.display = "block";
|
||||
submitButton.disabled = true;
|
||||
} else {
|
||||
isProcessing = false;
|
||||
cancelButton.style.display = "none";
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
|
||||
// Handle status messages
|
||||
if (data.status === "cancelled") {
|
||||
progressText.textContent = "Processing cancelled.";
|
||||
} else if (data.status === "done") {
|
||||
progressText.textContent = "Processing complete!";
|
||||
} else if (data.status === "video_done") {
|
||||
progressText.textContent = "Video compilation complete!";
|
||||
videoResult.style.display = "block";
|
||||
videoLink.href = "/output/timelapse.mp4";
|
||||
videoLink.textContent = "Download Timelapse Video";
|
||||
} else if (data.status.startsWith("error:")) {
|
||||
progressText.textContent = data.status;
|
||||
} else if (data.total > 0) {
|
||||
progressBar.max = data.total;
|
||||
progressBar.value = data.completed;
|
||||
const percent = Math.floor((data.completed / data.total) * 100);
|
||||
progressText.textContent = percent + "%";
|
||||
progressText.textContent = percent + "% (" + data.completed + "/" + data.total + ")";
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Error fetching progress:', err));
|
||||
}
|
||||
|
||||
// Poll every 1 second
|
||||
setInterval(fetchProgress, 1000);
|
||||
|
||||
// Form submission
|
||||
document.getElementById('timelapseForm').addEventListener('submit', function() {
|
||||
document.getElementById("videoResult").style.display = "none";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
73
timelapse.py
73
timelapse.py
|
|
@ -55,6 +55,44 @@ class ProcessConfig:
|
|||
landmark_model_path: str = "shape_predictor_68_face_landmarks.dat"
|
||||
|
||||
|
||||
def validate_immich_connection(api_key, base_url):
|
||||
"""
|
||||
Validates that the provided Immich API key and base URL are working.
|
||||
|
||||
Args:
|
||||
api_key (str): API key for authentication.
|
||||
base_url (str): Base URL of the API.
|
||||
|
||||
Returns:
|
||||
tuple: (bool, str) - (is_valid, error_message)
|
||||
"""
|
||||
if not api_key or not base_url:
|
||||
return False, "API key and base URL are required."
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'x-api-key': api_key,
|
||||
}
|
||||
# Try a simple ping to the server via the user endpoint
|
||||
url = f"{base_url}/server/about"
|
||||
response = requests.get(url, headers=headers, timeout=5)
|
||||
|
||||
if response.status_code == 200:
|
||||
return True, "Connection successful."
|
||||
elif response.status_code == 401:
|
||||
return False, "Authentication failed. Invalid API key."
|
||||
else:
|
||||
return False, f"Server error: Status code {response.status_code}"
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False, "Connection error. Check the base URL."
|
||||
except requests.exceptions.Timeout:
|
||||
return False, "Connection timed out. Server might be down."
|
||||
except Exception as e:
|
||||
return False, f"Unexpected error: {str(e)}"
|
||||
|
||||
|
||||
def initialize_worker(face_detect_model_path, landmark_model_path):
|
||||
"""
|
||||
Initializes the face detector and predictor in each worker process.
|
||||
|
|
@ -64,7 +102,7 @@ def initialize_worker(face_detect_model_path, landmark_model_path):
|
|||
face_predictor = dlib.shape_predictor(landmark_model_path)
|
||||
|
||||
|
||||
def get_assets_with_person(api_key, base_url, person_id):
|
||||
def get_assets_with_person(api_key, base_url, person_id, date_from=None, date_to=None):
|
||||
"""
|
||||
Retrieve all image assets containing the specified person by querying the API.
|
||||
|
||||
|
|
@ -72,6 +110,8 @@ def get_assets_with_person(api_key, base_url, person_id):
|
|||
api_key (str): API key for authentication.
|
||||
base_url (str): Base URL of the API.
|
||||
person_id (str): ID of the person to search for.
|
||||
date_from (str, optional): Start date in ISO format (YYYY-MM-DD).
|
||||
date_to (str, optional): End date in ISO format (YYYY-MM-DD).
|
||||
|
||||
Returns:
|
||||
list: List of asset dictionaries.
|
||||
|
|
@ -93,6 +133,16 @@ def get_assets_with_person(api_key, base_url, person_id):
|
|||
"withPeople": True,
|
||||
"withStacked": True,
|
||||
}
|
||||
|
||||
# Add date filters if provided
|
||||
if date_from:
|
||||
payload["dateFilter"] = payload.get("dateFilter", {})
|
||||
payload["dateFilter"]["from"] = f"{date_from}T00:00:00.000Z"
|
||||
|
||||
if date_to:
|
||||
payload["dateFilter"] = payload.get("dateFilter", {})
|
||||
payload["dateFilter"]["to"] = f"{date_to}T23:59:59.999Z"
|
||||
|
||||
while payload["page"] is not None:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code != 200:
|
||||
|
|
@ -106,7 +156,6 @@ def get_assets_with_person(api_key, base_url, person_id):
|
|||
payload["page"] = data['assets'].get('nextPage')
|
||||
return all_assets
|
||||
|
||||
|
||||
def download_asset(api_key, base_url, asset_id):
|
||||
"""
|
||||
Downloads the original image asset from the API.
|
||||
|
|
@ -343,7 +392,7 @@ def process_asset_wrapper(args):
|
|||
return process_asset_worker(asset, config)
|
||||
|
||||
|
||||
def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None):
|
||||
def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None, date_from=None, date_to=None):
|
||||
"""
|
||||
Processes assets containing the person and saves aligned face images.
|
||||
|
||||
|
|
@ -354,12 +403,21 @@ def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None):
|
|||
config (ProcessConfig): Configuration parameters.
|
||||
max_workers (int): Number of worker processes.
|
||||
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.
|
||||
|
||||
Returns:
|
||||
list: A list of file paths of the saved images.
|
||||
"""
|
||||
os.makedirs(config.output_folder, exist_ok=True)
|
||||
assets = get_assets_with_person(config.api_key, config.base_url, config.person_id)
|
||||
|
||||
# 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.")
|
||||
return []
|
||||
|
||||
assets = get_assets_with_person(config.api_key, config.base_url, config.person_id, date_from, date_to)
|
||||
logger.info(f"Found {len(assets)} assets containing the person.")
|
||||
total_assets = len(assets)
|
||||
if progress_callback:
|
||||
|
|
@ -375,6 +433,13 @@ def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None):
|
|||
# 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
|
||||
|
||||
if result is not None:
|
||||
processed_files.append(result)
|
||||
if progress_callback:
|
||||
|
|
|
|||
Loading…
Reference in a new issue