Working on ffmpeg call
This commit is contained in:
parent
22351cf31e
commit
225728d624
3 changed files with 137 additions and 12 deletions
101
compile_timelapse.py
Normal file
101
compile_timelapse.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from typing import Callable, List
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def compile_timelapse(
|
||||||
|
image_folder: str,
|
||||||
|
output_path: str,
|
||||||
|
framerate: int,
|
||||||
|
update_progress: Callable[[int, int], None]
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Compile a timelapse video from a folder of images using ffmpeg.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_folder: Path to folder containing timestamped JPEG images
|
||||||
|
output_path: Path where the output video should be saved
|
||||||
|
framerate: Desired frames per second
|
||||||
|
update_progress: Callback function to report progress (current: int, total: int)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get list of image files and sort by timestamp
|
||||||
|
image_files = sorted([
|
||||||
|
f for f in os.listdir(image_folder)
|
||||||
|
if f.lower().endswith(('.jpg', '.jpeg'))
|
||||||
|
])
|
||||||
|
|
||||||
|
if not image_files:
|
||||||
|
logger.error("No image files found in folder")
|
||||||
|
update_progress(0, 0)
|
||||||
|
return False
|
||||||
|
|
||||||
|
total_frames = len(image_files)
|
||||||
|
logger.info(f"Found {total_frames} images to process")
|
||||||
|
|
||||||
|
# Create ffmpeg input file list
|
||||||
|
input_file = os.path.join(image_folder, "input.txt")
|
||||||
|
frame_duration = 1.0 / framerate # Calculate duration based on requested framerate
|
||||||
|
|
||||||
|
with open(input_file, "w") as f:
|
||||||
|
for img in image_files:
|
||||||
|
f.write(f"file '{os.path.join(image_folder, img)}'\n")
|
||||||
|
f.write(f"duration {frame_duration}\n")
|
||||||
|
|
||||||
|
# Build ffmpeg command
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-f", "concat",
|
||||||
|
"-safe", "0",
|
||||||
|
"-i", input_file,
|
||||||
|
"-framerate", str(framerate),
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-y", # Overwrite output file if it exists
|
||||||
|
output_path
|
||||||
|
]
|
||||||
|
|
||||||
|
# Run ffmpeg with progress reporting
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
universal_newlines=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Monitor progress
|
||||||
|
frame_count = 0
|
||||||
|
for line in process.stderr:
|
||||||
|
if "frame=" in line:
|
||||||
|
try:
|
||||||
|
# Extract frame number from ffmpeg output
|
||||||
|
frame = int(line.split("frame=")[1].split()[0])
|
||||||
|
frame_count = min(frame, total_frames) # Ensure we don't exceed total
|
||||||
|
update_progress(frame_count, total_frames)
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Wait for process to complete
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
# Clean up temporary file
|
||||||
|
os.remove(input_file)
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
update_progress(total_frames, total_frames)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"ffmpeg failed with return code {process.returncode}")
|
||||||
|
update_progress(0, total_frames)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error compiling timelapse: {str(e)}")
|
||||||
|
update_progress(0, total_frames if 'total_frames' in locals() else 0)
|
||||||
|
return False
|
||||||
44
main.py
44
main.py
|
|
@ -8,6 +8,7 @@ from typing import Callable, Dict, List, Tuple
|
||||||
from flask import Flask, jsonify, render_template, request
|
from flask import Flask, jsonify, render_template, request
|
||||||
from image_processing import process_faces
|
from image_processing import process_faces
|
||||||
from immich_api import validate_immich_connection
|
from immich_api import validate_immich_connection
|
||||||
|
from compile_timelapse import compile_timelapse
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -74,24 +75,40 @@ def check_output_folder() -> Tuple[bool, int]:
|
||||||
def background_process(
|
def background_process(
|
||||||
max_workers: int = 1,
|
max_workers: int = 1,
|
||||||
progress_callback: Callable = None,
|
progress_callback: Callable = None,
|
||||||
cancel_flag: Callable = None
|
cancel_flag: Callable = None,
|
||||||
|
compile_video: bool = False,
|
||||||
|
framerate: int = 15
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""Process faces in the background.
|
"""Process faces in the background and optionally compile a timelapse video.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
max_workers: Number of worker processes for face processing
|
||||||
progress_callback: Optional callback for progress updates
|
progress_callback: Optional callback for progress updates
|
||||||
cancel_flag: Optional function to check for cancellation
|
cancel_flag: Optional function to check for cancellation
|
||||||
|
compile_video: Whether to compile a timelapse video after processing
|
||||||
Returns:
|
framerate: Frames per second for the output video
|
||||||
List of processed file paths
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return process_faces(
|
# Process faces
|
||||||
|
process_faces(
|
||||||
config=config,
|
config=config,
|
||||||
max_workers=max_workers,
|
max_workers=max_workers,
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
cancel_flag=cancel_flag
|
cancel_flag=cancel_flag
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if compile_video and not cancel_flag():
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(0, 1) # Reset progress for video compilation
|
||||||
|
|
||||||
|
progress_info["status"] = "compiling_video"
|
||||||
|
video_output_path = os.path.join(config.output_folder, "timelapse.mp4")
|
||||||
|
success = compile_timelapse(
|
||||||
|
image_folder=config.output_folder,
|
||||||
|
output_path=video_output_path,
|
||||||
|
framerate=framerate,
|
||||||
|
update_progress=progress_callback
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in background process: {str(e)}")
|
logger.error(f"Error in background process: {str(e)}")
|
||||||
|
|
@ -124,7 +141,7 @@ def index() -> str:
|
||||||
"""Handle the main page and processing requests."""
|
"""Handle the main page and processing requests."""
|
||||||
global processing_thread, cancel_requested
|
global processing_thread, cancel_requested
|
||||||
|
|
||||||
result = None
|
message = None
|
||||||
error = None
|
error = None
|
||||||
warning = None
|
warning = None
|
||||||
|
|
||||||
|
|
@ -166,16 +183,23 @@ def index() -> str:
|
||||||
# Start processing
|
# Start processing
|
||||||
processing_thread = threading.Thread(
|
processing_thread = threading.Thread(
|
||||||
target=background_process,
|
target=background_process,
|
||||||
args=(max_workers, update_progress, lambda: cancel_requested)
|
kwargs={
|
||||||
|
"max_workers": max_workers,
|
||||||
|
"progress_callback": update_progress,
|
||||||
|
"cancel_flag": lambda: cancel_requested,
|
||||||
|
"compile_video": compile_video,
|
||||||
|
"framerate": framerate
|
||||||
|
}
|
||||||
)
|
)
|
||||||
processing_thread.start()
|
processing_thread.start()
|
||||||
result = "Processing started. Please wait and watch the progress bar below."
|
|
||||||
|
message = "Processing started. Please wait and watch the progress bar below."
|
||||||
|
|
||||||
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",
|
return render_template("index.html",
|
||||||
result=result,
|
message=message,
|
||||||
error=error,
|
error=error,
|
||||||
warning=warning,
|
warning=warning,
|
||||||
max_workers_options=list(range(1, AVAILABLE_CORES + 1)))
|
max_workers_options=list(range(1, AVAILABLE_CORES + 1)))
|
||||||
|
|
|
||||||
|
|
@ -230,8 +230,8 @@
|
||||||
<button type="submit" id="submitButton" class="button-primary">Generate Timelapse</button>
|
<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>
|
<button type="button" id="cancelButton" class="button-danger" style="display:none;">Cancel Processing</button>
|
||||||
</form>
|
</form>
|
||||||
{% if result %}
|
{% if message %}
|
||||||
<p class="result">{{ result }}</p>
|
<p class="result">{{ message }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Progress Bar -->
|
<!-- Progress Bar -->
|
||||||
<div id="progressContainer">
|
<div id="progressContainer">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue