diff --git a/docs/image/convert/image_to_video.md b/docs/image/convert/image_to_video.md index 419a1151..ebcbeb08 100644 --- a/docs/image/convert/image_to_video.md +++ b/docs/image/convert/image_to_video.md @@ -25,6 +25,8 @@ The request body must be in JSON format and should include the following paramet | `length` | number | No | The desired length of the video in seconds (default: 5). | | `frame_rate`| integer| No | The frame rate of the output video (default: 30). | | `zoom_speed`| number | No | The speed of the zoom effect (0-100, default: 3). | +| `zoom_effect`| string | No | The type of zoom effect: "linear" (default), "ping-pong". | +| `zoom_loop_duration`| number | No | Duration in seconds for a full zoom loop (for ping-pong). | | `webhook_url`| string| No | The URL to receive a webhook notification upon completion. | | `id` | string | No | An optional identifier for the request. | @@ -38,6 +40,8 @@ The `validate_payload` decorator in the `routes.v1.image.convert.image_to_video` "length": {"type": "number", "minimum": 1, "maximum": 60}, "frame_rate": {"type": "integer", "minimum": 15, "maximum": 60}, "zoom_speed": {"type": "number", "minimum": 0, "maximum": 100}, + "zoom_effect": {"type": "string", "enum": ["linear", "ping-pong", "loop"]}, + "zoom_loop_duration": {"type": "number", "minimum": 5, "maximum": 400}, "webhook_url": {"type": "string", "format": "uri"}, "id": {"type": "string"} }, @@ -54,6 +58,8 @@ The `validate_payload` decorator in the `routes.v1.image.convert.image_to_video` "length": 10, "frame_rate": 24, "zoom_speed": 5, + "zoom_effect": "ping-pong", + "zoom_loop_duration": 5, "webhook_url": "https://example.com/webhook", "id": "request-123" } diff --git a/routes/v1/image/convert/image_to_video.py b/routes/v1/image/convert/image_to_video.py index 85fe42da..3e3907a6 100644 --- a/routes/v1/image/convert/image_to_video.py +++ b/routes/v1/image/convert/image_to_video.py @@ -36,6 +36,8 @@ "length": {"type": "number", "minimum": 0.1, "maximum": 400}, "frame_rate": {"type": "integer", "minimum": 15, "maximum": 60}, "zoom_speed": {"type": "number", "minimum": 0, "maximum": 100}, + "zoom_effect": {"type": "string", "enum": ["linear", "ping-pong", "loop"]}, + "zoom_loop_duration": {"type": "number", "minimum": 5, "maximum": 400}, "webhook_url": {"type": "string", "format": "uri"}, "id": {"type": "string"} }, @@ -48,6 +50,8 @@ def image_to_video(job_id, data): length = data.get('length', 5) frame_rate = data.get('frame_rate', 30) zoom_speed = data.get('zoom_speed', 3) / 100 + zoom_effect = data.get('zoom_effect', 'linear') + zoom_loop_duration = data.get('zoom_loop_duration') webhook_url = data.get('webhook_url') id = data.get('id') @@ -56,7 +60,7 @@ def image_to_video(job_id, data): try: # Process image to video conversion output_filename = process_image_to_video( - image_url, length, frame_rate, zoom_speed, job_id, webhook_url + image_url, length, frame_rate, zoom_speed, job_id, webhook_url, zoom_effect, zoom_loop_duration ) # Upload the resulting file using the unified upload_file() method diff --git a/services/v1/image/convert/image_to_video.py b/services/v1/image/convert/image_to_video.py index 7bce0481..4d714f1b 100644 --- a/services/v1/image/convert/image_to_video.py +++ b/services/v1/image/convert/image_to_video.py @@ -24,7 +24,7 @@ from config import LOCAL_STORAGE_PATH logger = logging.getLogger(__name__) -def process_image_to_video(image_url, length, frame_rate, zoom_speed, job_id, webhook_url=None): +def process_image_to_video(image_url, length, frame_rate, zoom_speed, job_id, webhook_url=None, zoom_effect="linear", zoom_loop_duration=None): try: # Download the image file image_path = download_file(image_url, LOCAL_STORAGE_PATH) @@ -52,12 +52,25 @@ def process_image_to_video(image_url, length, frame_rate, zoom_speed, job_id, we logger.info(f"Using scale dimensions: {scale_dims}, output dimensions: {output_dims}") logger.info(f"Video length: {length}s, Frame rate: {frame_rate}fps, Total frames: {total_frames}") - logger.info(f"Zoom speed: {zoom_speed}/s, Final zoom factor: {zoom_factor}") + logger.info(f"Zoom speed: {zoom_speed}/s, Final zoom factor: {zoom_factor}, Effect: {zoom_effect}, Loop Duration: {zoom_loop_duration}") # Prepare FFmpeg command with fps filter to ensure correct frame rate + if zoom_effect == "ping-pong": + # Using triangle wave for Ping-Pong Zoom (In -> Out) with optional loop frequency + loop_period = zoom_loop_duration if zoom_loop_duration else length + loop_frames = int(loop_period * frame_rate) + if loop_frames < 1: loop_frames = total_frames + + # Formula: 1 + MaxCorrection * (1 - abs(2 * mod(on, loop_frames)/loop_frames - 1)) + # mod(on, loop_frames) cycles 0 -> loop_frames-1 + expression = f"1+({zoom_speed}*{length})*(1-abs(2*mod(on,{loop_frames})/{loop_frames}-1))" + else: # linear default + # Standard linear zoom in + expression = f"min(1+({zoom_speed}*{length})*on/{total_frames}, {zoom_factor})" + cmd = [ 'ffmpeg', '-framerate', str(frame_rate), '-loop', '1', '-i', image_path, - '-vf', f"scale={scale_dims},zoompan=z='min(1+({zoom_speed}*{length})*on/{total_frames}, {zoom_factor})':d={total_frames}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s={output_dims},fps={frame_rate}", + '-vf', f"scale={scale_dims},zoompan=z='{expression}':d={total_frames}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s={output_dims},fps={frame_rate}", '-c:v', 'libx264', '-r', str(frame_rate), '-t', str(length), '-pix_fmt', 'yuv420p', output_path ]