Skip to content

Commit f01567a

Browse files
committed
feat(image-to-video): add ping-pong zoom effect and loop duration parameters
1 parent e4fa4ad commit f01567a

3 files changed

Lines changed: 27 additions & 4 deletions

File tree

docs/image/convert/image_to_video.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ The request body must be in JSON format and should include the following paramet
2525
| `length` | number | No | The desired length of the video in seconds (default: 5). |
2626
| `frame_rate`| integer| No | The frame rate of the output video (default: 30). |
2727
| `zoom_speed`| number | No | The speed of the zoom effect (0-100, default: 3). |
28+
| `zoom_effect`| string | No | The type of zoom effect: "linear" (default), "ping-pong". |
29+
| `zoom_loop_duration`| number | No | Duration in seconds for a full zoom loop (for ping-pong). |
2830
| `webhook_url`| string| No | The URL to receive a webhook notification upon completion. |
2931
| `id` | string | No | An optional identifier for the request. |
3032

@@ -38,6 +40,8 @@ The `validate_payload` decorator in the `routes.v1.image.convert.image_to_video`
3840
"length": {"type": "number", "minimum": 1, "maximum": 60},
3941
"frame_rate": {"type": "integer", "minimum": 15, "maximum": 60},
4042
"zoom_speed": {"type": "number", "minimum": 0, "maximum": 100},
43+
"zoom_effect": {"type": "string", "enum": ["linear", "ping-pong", "loop"]},
44+
"zoom_loop_duration": {"type": "number", "minimum": 5, "maximum": 400},
4145
"webhook_url": {"type": "string", "format": "uri"},
4246
"id": {"type": "string"}
4347
},
@@ -54,6 +58,8 @@ The `validate_payload` decorator in the `routes.v1.image.convert.image_to_video`
5458
"length": 10,
5559
"frame_rate": 24,
5660
"zoom_speed": 5,
61+
"zoom_effect": "ping-pong",
62+
"zoom_loop_duration": 5,
5763
"webhook_url": "https://example.com/webhook",
5864
"id": "request-123"
5965
}

routes/v1/image/convert/image_to_video.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
"length": {"type": "number", "minimum": 0.1, "maximum": 400},
3737
"frame_rate": {"type": "integer", "minimum": 15, "maximum": 60},
3838
"zoom_speed": {"type": "number", "minimum": 0, "maximum": 100},
39+
"zoom_effect": {"type": "string", "enum": ["linear", "ping-pong", "loop"]},
40+
"zoom_loop_duration": {"type": "number", "minimum": 5, "maximum": 400},
3941
"webhook_url": {"type": "string", "format": "uri"},
4042
"id": {"type": "string"}
4143
},
@@ -48,6 +50,8 @@ def image_to_video(job_id, data):
4850
length = data.get('length', 5)
4951
frame_rate = data.get('frame_rate', 30)
5052
zoom_speed = data.get('zoom_speed', 3) / 100
53+
zoom_effect = data.get('zoom_effect', 'linear')
54+
zoom_loop_duration = data.get('zoom_loop_duration')
5155
webhook_url = data.get('webhook_url')
5256
id = data.get('id')
5357

@@ -56,7 +60,7 @@ def image_to_video(job_id, data):
5660
try:
5761
# Process image to video conversion
5862
output_filename = process_image_to_video(
59-
image_url, length, frame_rate, zoom_speed, job_id, webhook_url
63+
image_url, length, frame_rate, zoom_speed, job_id, webhook_url, zoom_effect, zoom_loop_duration
6064
)
6165

6266
# Upload the resulting file using the unified upload_file() method

services/v1/image/convert/image_to_video.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from config import LOCAL_STORAGE_PATH
2525
logger = logging.getLogger(__name__)
2626

27-
def process_image_to_video(image_url, length, frame_rate, zoom_speed, job_id, webhook_url=None):
27+
def process_image_to_video(image_url, length, frame_rate, zoom_speed, job_id, webhook_url=None, zoom_effect="linear", zoom_loop_duration=None):
2828
try:
2929
# Download the image file
3030
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
5252

5353
logger.info(f"Using scale dimensions: {scale_dims}, output dimensions: {output_dims}")
5454
logger.info(f"Video length: {length}s, Frame rate: {frame_rate}fps, Total frames: {total_frames}")
55-
logger.info(f"Zoom speed: {zoom_speed}/s, Final zoom factor: {zoom_factor}")
55+
logger.info(f"Zoom speed: {zoom_speed}/s, Final zoom factor: {zoom_factor}, Effect: {zoom_effect}, Loop Duration: {zoom_loop_duration}")
5656

5757
# Prepare FFmpeg command with fps filter to ensure correct frame rate
58+
if zoom_effect == "ping-pong":
59+
# Using triangle wave for Ping-Pong Zoom (In -> Out) with optional loop frequency
60+
loop_period = zoom_loop_duration if zoom_loop_duration else length
61+
loop_frames = int(loop_period * frame_rate)
62+
if loop_frames < 1: loop_frames = total_frames
63+
64+
# Formula: 1 + MaxCorrection * (1 - abs(2 * mod(on, loop_frames)/loop_frames - 1))
65+
# mod(on, loop_frames) cycles 0 -> loop_frames-1
66+
expression = f"1+({zoom_speed}*{length})*(1-abs(2*mod(on,{loop_frames})/{loop_frames}-1))"
67+
else: # linear default
68+
# Standard linear zoom in
69+
expression = f"min(1+({zoom_speed}*{length})*on/{total_frames}, {zoom_factor})"
70+
5871
cmd = [
5972
'ffmpeg', '-framerate', str(frame_rate), '-loop', '1', '-i', image_path,
60-
'-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}",
73+
'-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}",
6174
'-c:v', 'libx264', '-r', str(frame_rate), '-t', str(length), '-pix_fmt', 'yuv420p', output_path
6275
]
6376

0 commit comments

Comments
 (0)