Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/image/convert/image_to_video.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand All @@ -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"}
},
Expand All @@ -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"
}
Expand Down
6 changes: 5 additions & 1 deletion routes/v1/image/convert/image_to_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
},
Expand All @@ -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')

Expand All @@ -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
Expand Down
19 changes: 16 additions & 3 deletions services/v1/image/convert/image_to_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
]

Expand Down