From 225728d62444d56ef453936423c93a648cd2410a Mon Sep 17 00:00:00 2001 From: Arnaud_Cayrol Date: Sat, 12 Apr 2025 20:40:42 +0200 Subject: [PATCH] Working on ffmpeg call --- compile_timelapse.py | 101 +++++++++++++++++++++++++++++++++++++++++++ main.py | 44 ++++++++++++++----- templates/index.html | 4 +- 3 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 compile_timelapse.py diff --git a/compile_timelapse.py b/compile_timelapse.py new file mode 100644 index 0000000..00c3cbe --- /dev/null +++ b/compile_timelapse.py @@ -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 \ No newline at end of file diff --git a/main.py b/main.py index d862e14..bc6d078 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ from typing import Callable, Dict, List, Tuple from flask import Flask, jsonify, render_template, request from image_processing import process_faces from immich_api import validate_immich_connection +from compile_timelapse import compile_timelapse # Configure logging logger = logging.getLogger(__name__) @@ -74,24 +75,40 @@ def check_output_folder() -> Tuple[bool, int]: def background_process( max_workers: int = 1, progress_callback: Callable = None, - cancel_flag: Callable = None + cancel_flag: Callable = None, + compile_video: bool = False, + framerate: int = 15 ) -> List[str]: - """Process faces in the background. + """Process faces in the background and optionally compile a timelapse video. Args: + max_workers: Number of worker processes for face processing progress_callback: Optional callback for progress updates cancel_flag: Optional function to check for cancellation - - Returns: - List of processed file paths + compile_video: Whether to compile a timelapse video after processing + framerate: Frames per second for the output video """ try: - return process_faces( + # Process faces + process_faces( config=config, max_workers=max_workers, progress_callback=progress_callback, 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: logger.error(f"Error in background process: {str(e)}") @@ -124,7 +141,7 @@ def index() -> str: """Handle the main page and processing requests.""" global processing_thread, cancel_requested - result = None + message = None error = None warning = None @@ -166,16 +183,23 @@ def index() -> str: # Start processing processing_thread = threading.Thread( 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() - 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: error = f"Error processing request: {e}" return render_template("index.html", - result=result, + message=message, error=error, warning=warning, max_workers_options=list(range(1, AVAILABLE_CORES + 1))) diff --git a/templates/index.html b/templates/index.html index eb58771..e4d75b5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -230,8 +230,8 @@ - {% if result %} -

{{ result }}

+ {% if message %} +

{{ message }}

{% endif %}