From dbea9f53b06e6d7042ebec25d4bf900a2a2a9309 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Fri, 2 May 2025 21:52:33 +0530 Subject: [PATCH 01/13] Update image_to_video.py --- routes/v1/image/convert/image_to_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/v1/image/convert/image_to_video.py b/routes/v1/image/convert/image_to_video.py index a9dd226b..490cfc59 100644 --- a/routes/v1/image/convert/image_to_video.py +++ b/routes/v1/image/convert/image_to_video.py @@ -33,7 +33,7 @@ "type": "object", "properties": { "image_url": {"type": "string", "format": "uri"}, - "length": {"type": "number", "minimum": 0.1, "maximum": 60}, + "length": {"type": "number", "minimum": 0.1, "maximum": 300}, "frame_rate": {"type": "integer", "minimum": 15, "maximum": 60}, "zoom_speed": {"type": "number", "minimum": 0, "maximum": 100}, "webhook_url": {"type": "string", "format": "uri"}, From 2f62ac3775e5dbcfc686722dec0ebf37ad1af5d3 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Fri, 2 May 2025 21:54:22 +0530 Subject: [PATCH 02/13] Update image_to_video.py --- routes/v1/image/convert/image_to_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/v1/image/convert/image_to_video.py b/routes/v1/image/convert/image_to_video.py index a9dd226b..490cfc59 100644 --- a/routes/v1/image/convert/image_to_video.py +++ b/routes/v1/image/convert/image_to_video.py @@ -33,7 +33,7 @@ "type": "object", "properties": { "image_url": {"type": "string", "format": "uri"}, - "length": {"type": "number", "minimum": 0.1, "maximum": 60}, + "length": {"type": "number", "minimum": 0.1, "maximum": 300}, "frame_rate": {"type": "integer", "minimum": 15, "maximum": 60}, "zoom_speed": {"type": "number", "minimum": 0, "maximum": 100}, "webhook_url": {"type": "string", "format": "uri"}, From b394ac959e800dbc1b1c92e1e1a2fc461525f07c Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Fri, 2 May 2025 21:55:33 +0530 Subject: [PATCH 03/13] Update image_to_video.py --- routes/image_to_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/image_to_video.py b/routes/image_to_video.py index 5a223720..700fcc74 100644 --- a/routes/image_to_video.py +++ b/routes/image_to_video.py @@ -32,7 +32,7 @@ "type": "object", "properties": { "image_url": {"type": "string", "format": "uri"}, - "length": {"type": "number", "minimum": 1, "maximum": 60}, + "length": {"type": "number", "minimum": 1, "maximum": 300}, "frame_rate": {"type": "integer", "minimum": 15, "maximum": 60}, "zoom_speed": {"type": "number", "minimum": 0, "maximum": 100}, "webhook_url": {"type": "string", "format": "uri"}, From 91a90bec0f2946bff446dca7c06ae552a93bc5c5 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Sun, 4 May 2025 00:33:48 +0530 Subject: [PATCH 04/13] feat: Add orientation parameter to image-to-video endpoint --- routes/v1/image/convert/image_to_video.py | 7 +++-- services/v1/image/convert/image_to_video.py | 34 +++++++++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/routes/v1/image/convert/image_to_video.py b/routes/v1/image/convert/image_to_video.py index 490cfc59..b266050c 100644 --- a/routes/v1/image/convert/image_to_video.py +++ b/routes/v1/image/convert/image_to_video.py @@ -33,9 +33,10 @@ "type": "object", "properties": { "image_url": {"type": "string", "format": "uri"}, - "length": {"type": "number", "minimum": 0.1, "maximum": 300}, + "length": {"type": "number", "minimum": 0.1, "maximum": 60}, "frame_rate": {"type": "integer", "minimum": 15, "maximum": 60}, "zoom_speed": {"type": "number", "minimum": 0, "maximum": 100}, + "orientation": {"type": "string", "enum": ["landscape", "portrait"]}, "webhook_url": {"type": "string", "format": "uri"}, "id": {"type": "string"} }, @@ -48,6 +49,7 @@ 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 + orientation = data.get('orientation') webhook_url = data.get('webhook_url') id = data.get('id') @@ -56,7 +58,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, orientation ) # Upload the resulting file using the unified upload_file() method @@ -71,3 +73,4 @@ def image_to_video(job_id, data): except Exception as e: logger.error(f"Job {job_id}: Error processing image to video: {str(e)}", exc_info=True) return str(e), "/v1/image/convert/video", 500 + diff --git a/services/v1/image/convert/image_to_video.py b/services/v1/image/convert/image_to_video.py index 7bce0481..52afdb5f 100644 --- a/services/v1/image/convert/image_to_video.py +++ b/services/v1/image/convert/image_to_video.py @@ -24,27 +24,37 @@ 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, orientation=None): try: # Download the image file image_path = download_file(image_url, LOCAL_STORAGE_PATH) logger.info(f"Downloaded image to {image_path}") - # Get image dimensions using Pillow - with Image.open(image_path) as img: - width, height = img.size - logger.info(f"Original image dimensions: {width}x{height}") - # Prepare the output path output_path = os.path.join(LOCAL_STORAGE_PATH, f"{job_id}.mp4") - # Determine orientation and set appropriate dimensions - if width > height: - scale_dims = "7680:4320" - output_dims = "1920x1080" - else: + # Determine orientation and set appropriate dimensions based on parameter or default to landscape + if orientation == 'portrait': scale_dims = "4320:7680" output_dims = "1080x1920" + logger.info(f"Using specified portrait orientation.") + else: + # Default to landscape if orientation is not specified or invalid + if orientation != 'landscape' and orientation is not None: + logger.warning(f"Invalid orientation specified: '{orientation}'. Defaulting to landscape.") + else: + logger.info(f"Using landscape orientation (specified or default).") + orientation = 'landscape' # Ensure orientation variable is set for logging/potential future use + scale_dims = "7680:4320" + output_dims = "1920x1080" + + # Get image dimensions using Pillow (for logging/potential future use, not for dimension setting) + try: + with Image.open(image_path) as img: + width, height = img.size + logger.info(f"Original image dimensions: {width}x{height}") + except Exception as img_err: + logger.warning(f"Could not read image dimensions: {img_err}") # Calculate total frames and zoom factor total_frames = int(length * frame_rate) @@ -77,4 +87,4 @@ def process_image_to_video(image_url, length, frame_rate, zoom_speed, job_id, we return output_path except Exception as e: logger.error(f"Error in process_image_to_video: {str(e)}", exc_info=True) - raise \ No newline at end of file + raise From 0b3d9ff90f335eb359ebabee308ff9c9124e3a0d Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Sun, 4 May 2025 22:50:10 +0530 Subject: [PATCH 05/13] docs: Update image-to-video endpoint for orientation parameter --- docs/image/convert/image_to_video.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/image/convert/image_to_video.md b/docs/image/convert/image_to_video.md index 376be039..a9624b02 100644 --- a/docs/image/convert/image_to_video.md +++ b/docs/image/convert/image_to_video.md @@ -25,6 +25,7 @@ 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). | +| `orientation`| string | No | Desired output video orientation: "landscape" (1920x1080) or "portrait" (1080x1920). Defaults to landscape if omitted or invalid. | | `webhook_url`| string| No | The URL to receive a webhook notification upon completion. | | `id` | string | No | An optional identifier for the request. | @@ -35,9 +36,10 @@ The `validate_payload` decorator in the `routes.v1.image.convert.image_to_video` "type": "object", "properties": { "image_url": {"type": "string", "format": "uri"}, - "length": {"type": "number", "minimum": 1, "maximum": 60}, + "length": {"type": "number", "minimum": 0.1, "maximum": 60}, "frame_rate": {"type": "integer", "minimum": 15, "maximum": 60}, "zoom_speed": {"type": "number", "minimum": 0, "maximum": 100}, + "orientation": {"type": "string", "enum": ["landscape", "portrait"]}, "webhook_url": {"type": "string", "format": "uri"}, "id": {"type": "string"} }, @@ -54,6 +56,7 @@ The `validate_payload` decorator in the `routes.v1.image.convert.image_to_video` "length": 10, "frame_rate": 24, "zoom_speed": 5, + "orientation": "landscape", "webhook_url": "https://example.com/webhook", "id": "request-123" } @@ -63,7 +66,7 @@ The `validate_payload` decorator in the `routes.v1.image.convert.image_to_video` curl -X POST \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ - -d '{"image_url": "https://example.com/image.jpg", "length": 10, "frame_rate": 24, "zoom_speed": 5, "webhook_url": "https://example.com/webhook", "id": "request-123"}' \ + -d '{"image_url": "https://example.com/image.jpg", "length": 10, "frame_rate": 24, "zoom_speed": 5, "orientation": "landscape", "webhook_url": "https://example.com/webhook", "id": "request-123"}' \ http://your-api-endpoint/v1/image/convert/video ``` @@ -139,9 +142,10 @@ The endpoint handles the following types of errors: ## 6. Usage Notes - The `image_url` parameter must be a valid URL pointing to an image file. -- The `length` parameter specifies the duration of the output video in seconds and must be between 1 and 60. +- The `length` parameter specifies the duration of the output video in seconds and must be between 0.1 and 60. - The `frame_rate` parameter specifies the frame rate of the output video and must be between 15 and 60. - The `zoom_speed` parameter controls the speed of the zoom effect and must be between 0 and 100. +- The `orientation` parameter (optional) forces the output to "landscape" (1920x1080) or "portrait" (1080x1920). Defaults to landscape. - The `webhook_url` parameter is optional and can be used to receive a notification when the conversion is complete. - The `id` parameter is optional and can be used to identify the request. @@ -156,4 +160,4 @@ The endpoint handles the following types of errors: - Validate the `image_url` parameter before sending the request to ensure it points to a valid and accessible image file. - Use the `webhook_url` parameter to receive notifications about the completion of the conversion process, rather than polling the API repeatedly. - Provide the `id` parameter to easily identify and track the request in logs or notifications. -- Consider setting the `bypass_queue` parameter to `True` for time-sensitive requests to bypass the queue and process the request immediately. \ No newline at end of file +- Consider setting the `bypass_queue` parameter to `True` for time-sensitive requests to bypass the queue and process the request immediately. From 2104c57d0cf7887f0f0b4877b7c053d053a88863 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Sun, 4 May 2025 23:06:28 +0530 Subject: [PATCH 06/13] feat: Add image effects endpoint (zoom_in_out, pan_rlr) --- app.py | 18 ++- docs/image/effects/image_effects_video.md | 100 +++++++++++++++ .../v1/image/effects/image_effects_video.py | 72 +++++++++++ .../v1/image/effects/image_effects_video.py | 114 ++++++++++++++++++ 4 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 docs/image/effects/image_effects_video.md create mode 100644 routes/v1/image/effects/image_effects_video.py create mode 100644 services/v1/image/effects/image_effects_video.py diff --git a/app.py b/app.py index 2a196f82..e7822795 100644 --- a/app.py +++ b/app.py @@ -237,6 +237,7 @@ def wrapper(*args, **kwargs): from routes.v1.media.metadata import v1_media_metadata_bp from routes.v1.toolkit.job_status import v1_toolkit_job_status_bp from routes.v1.toolkit.jobs_status import v1_toolkit_jobs_status_bp + from routes.v1.image.effects.image_effects_video import v1_image_effects_video_bp app.register_blueprint(v1_ffmpeg_compose_bp) app.register_blueprint(v1_media_transcribe_bp) @@ -265,10 +266,21 @@ def wrapper(*args, **kwargs): app.register_blueprint(v1_media_metadata_bp) app.register_blueprint(v1_toolkit_job_status_bp) app.register_blueprint(v1_toolkit_jobs_status_bp) + app.register_blueprint(v1_image_effects_video_bp) # Register new blueprint return app -app = create_app() -if __name__ == '__main__': - app.run(host='0.0.0.0', port=8080) \ No newline at end of file + +if __name__ == "__main__": + # Use environment variables or defaults for Gunicorn settings + bind_ip = os.environ.get('GUNICORN_BIND_IP', '0.0.0.0') + bind_port = os.environ.get('GUNICORN_BIND_PORT', '8080') + workers = os.environ.get('GUNICORN_WORKERS', '4') # Default to 4 workers + timeout = os.environ.get('GUNICORN_TIMEOUT', '300') # Default to 300 seconds timeout + + # Command to run Gunicorn + gunicorn_command = f"gunicorn -w {workers} -b {bind_ip}:{bind_port} -t {timeout} app:create_app()" + + # Execute the Gunicorn command + os.system(gunicorn_command) diff --git a/docs/image/effects/image_effects_video.md b/docs/image/effects/image_effects_video.md new file mode 100644 index 00000000..2e0a410f --- /dev/null +++ b/docs/image/effects/image_effects_video.md @@ -0,0 +1,100 @@ +# Image Effects to Video Conversion + +## 1. Overview + +The `/v1/image/effects/video` endpoint converts an image into a video file with specific dynamic effects like zoom-in-out or panning. It's registered in `app.py` under the `v1_image_effects_video_bp` blueprint from `routes.v1.image.effects.image_effects_video`. + +## 2. Endpoint + +**URL Path:** `/v1/image/effects/video` +**HTTP Method:** `POST` + +## 3. Request + +### Headers + +- `x-api-key` (required): The API key for authentication. + +### Body Parameters + +The request body must be in JSON format: + +| Parameter | Type | Required | Description | +|---------------|---------|----------|--------------------------------------------------------------------------------------------------------| +| `image_url` | string | Yes | The URL of the image to be converted. | +| `effect_type` | string | Yes | The desired effect: "zoom_in_out" or "pan_rlr" (pan right-left-right). | +| `length` | number | No | Desired video length in seconds (default: 5, max: 120). | +| `frame_rate` | integer | No | Frame rate of the output video (default: 30, min: 15, max: 60). | +| `orientation` | string | No | Desired output video orientation: "landscape" (1920x1080) or "portrait" (1080x1920). Default: landscape. | +| `webhook_url` | string | No | URL to receive a webhook notification upon completion. | +| `id` | string | No | Optional identifier for the request. | + +The `validate_payload` decorator enforces this JSON schema: + +```json +{ + "type": "object", + "properties": { + "image_url": {"type": "string", "format": "uri"}, + "effect_type": {"type": "string", "enum": ["zoom_in_out", "pan_rlr"]}, + "length": {"type": "number", "minimum": 0.1, "maximum": 120}, + "frame_rate": {"type": "integer", "minimum": 15, "maximum": 60}, + "orientation": {"type": "string", "enum": ["landscape", "portrait"]}, + "webhook_url": {"type": "string", "format": "uri"}, + "id": {"type": "string"} + }, + "required": ["image_url", "effect_type"], + "additionalProperties": False +} +``` + +### Example Request (Zoom In/Out) + +```json +{ + "image_url": "https://example.com/image.jpg", + "effect_type": "zoom_in_out", + "length": 10, + "orientation": "landscape", + "id": "request-zoom" +} +``` + +### Example Request (Pan Right-Left-Right) + +```json +{ + "image_url": "https://example.com/image.jpg", + "effect_type": "pan_rlr", + "length": 15, + "orientation": "portrait", + "id": "request-pan" +} +``` + +## 4. Response + +Success and error responses follow the same structure as other endpoints (e.g., `/v1/image/convert/video`), returning the cloud storage URL in the `response` field on success (200 OK), or appropriate error codes (400, 429, 500). + +## 5. Error Handling + +- Handles missing/invalid parameters (400). +- Handles queue length exceeded (429). +- Handles exceptions during processing (500). + +## 6. Usage Notes + +- `effect_type` is required and determines the animation applied. +- `zoom_in_out`: Zooms in towards the center for the first half of the `length`, then zooms out for the second half. +- `pan_rlr`: Pans the view across the image from right-to-left for the first half, then left-to-right for the second half. No zoom is applied. +- The maximum `length` is increased to 120 seconds for this endpoint. + +## 7. Common Issues + +- Providing an invalid `image_url`. +- Forgetting the required `effect_type` parameter. + +## 8. Best Practices + +- Use `webhook_url` for long processing times. +- Specify `orientation` if a specific output aspect ratio is needed. diff --git a/routes/v1/image/effects/image_effects_video.py b/routes/v1/image/effects/image_effects_video.py new file mode 100644 index 00000000..d8269f89 --- /dev/null +++ b/routes/v1/image/effects/image_effects_video.py @@ -0,0 +1,72 @@ +# Copyright (c) 2025 Stephen G. Pope +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from flask import Blueprint +from app_utils import * +import logging +from services.v1.image.effects.image_effects_video import process_image_effects_video +from services.authentication import authenticate +from services.cloud_storage import upload_file + +v1_image_effects_video_bp = Blueprint('v1_image_effects_video', __name__) +logger = logging.getLogger(__name__) + +@v1_image_effects_video_bp.route('/v1/image/effects/video', methods=['POST']) +@authenticate +@validate_payload({ + "type": "object", + "properties": { + "image_url": {"type": "string", "format": "uri"}, + "effect_type": {"type": "string", "enum": ["zoom_in_out", "pan_rlr"]}, + "length": {"type": "number", "minimum": 0.1, "maximum": 120}, # Increased max length + "frame_rate": {"type": "integer", "minimum": 15, "maximum": 60}, + "orientation": {"type": "string", "enum": ["landscape", "portrait"]}, + "webhook_url": {"type": "string", "format": "uri"}, + "id": {"type": "string"} + }, + "required": ["image_url", "effect_type"], # effect_type is required + "additionalProperties": False +}) +@queue_task_wrapper(bypass_queue=False) +def image_effects_video(job_id, data): + image_url = data.get('image_url') + effect_type = data.get('effect_type') + length = data.get('length', 5) + frame_rate = data.get('frame_rate', 30) + orientation = data.get('orientation') + webhook_url = data.get('webhook_url') + id = data.get('id') + + logger.info(f"Job {job_id}: Received image effects video request for {image_url} with effect {effect_type}") + + try: + # Process image to video conversion with effects + output_filename = process_image_effects_video( + image_url, length, frame_rate, effect_type, job_id, webhook_url, orientation + ) + + # Upload the resulting file using the unified upload_file() method + cloud_url = upload_file(output_filename) + + # Log the successful upload + logger.info(f"Job {job_id}: Converted effects video uploaded to cloud storage: {cloud_url}") + + # Return the cloud URL for the uploaded file + return cloud_url, "/v1/image/effects/video", 200 + + except Exception as e: + logger.error(f"Job {job_id}: Error processing image effects video: {str(e)}", exc_info=True) + return str(e), "/v1/image/effects/video", 500 diff --git a/services/v1/image/effects/image_effects_video.py b/services/v1/image/effects/image_effects_video.py new file mode 100644 index 00000000..25a837b8 --- /dev/null +++ b/services/v1/image/effects/image_effects_video.py @@ -0,0 +1,114 @@ +# Copyright (c) 2025 Stephen G. Pope +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import subprocess +import logging +from services.file_management import download_file +from PIL import Image +from config import LOCAL_STORAGE_PATH +logger = logging.getLogger(__name__) + +def process_image_effects_video(image_url, length, frame_rate, effect_type, job_id, webhook_url=None, orientation=None): + """ + Generates a video from an image with specific effects like zoom-in-out or panning. + """ + try: + # Download the image file + image_path = download_file(image_url, LOCAL_STORAGE_PATH) + logger.info(f"Job {job_id}: Downloaded image for effects video to {image_path}") + + # Prepare the output path + output_path = os.path.join(LOCAL_STORAGE_PATH, f"{job_id}.mp4") + + # Determine output dimensions based on orientation or default to landscape + if orientation == 'portrait': + scale_dims = "4320:7680" # High-res intermediate for portrait + output_dims = "1080x1920" # Final output dimensions + ow, oh = 1080, 1920 + iw, ih = 4320, 7680 # Input dimensions *to zoompan filter* after initial scale + logger.info(f"Job {job_id}: Using specified portrait orientation.") + else: + # Default to landscape + if orientation != 'landscape' and orientation is not None: + logger.warning(f"Job {job_id}: Invalid orientation specified: '{orientation}'. Defaulting to landscape.") + orientation = 'landscape' + scale_dims = "7680:4320" # High-res intermediate for landscape + output_dims = "1920x1080" # Final output dimensions + ow, oh = 1920, 1080 + iw, ih = 7680, 4320 # Input dimensions *to zoompan filter* after initial scale + logger.info(f"Job {job_id}: Using landscape orientation (specified or default).") + + # Calculate total frames and midpoint + total_frames = int(length * frame_rate) + mid_frame = total_frames / 2 + + # Build the zoompan filter string based on effect_type + vf_filter = "" + if effect_type == "zoom_in_out": + max_zoom = 1.5 # Define how much to zoom in + # Expression: Zoom in linearly to max_zoom until mid_frame, then zoom out linearly back to 1 + zoom_expr = f"if(lt(on,{mid_frame}), 1+(({max_zoom}-1)*on/{mid_frame}), {max_zoom}-(({max_zoom}-1)*(on-{mid_frame})/{mid_frame}))" + vf_filter = f"scale={scale_dims}:force_original_aspect_ratio=decrease,pad={iw}:{ih}:(ow-iw)/2:(oh-ih)/2,zoompan=z='{zoom_expr}':d={total_frames}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s={output_dims},fps={frame_rate}" + logger.info(f"Job {job_id}: Using zoom_in_out effect.") + + elif effect_type == "pan_rlr": + # Ensure zoom is 1 (no zoom for panning) + # Expression: Pan x from right (iw-ow) to left (0) until mid_frame, then pan x from left (0) to right (iw-ow) + pan_x_expr = f"if(lt(on,{mid_frame}), ({iw}-{ow})*(1-on/{mid_frame}), ({iw}-{ow})*((on-{mid_frame})/{mid_frame}))" + vf_filter = f"scale={scale_dims}:force_original_aspect_ratio=decrease,pad={iw}:{ih}:(ow-iw)/2:(oh-ih)/2,zoompan=z=1:d={total_frames}:x='{pan_x_expr}':y='(ih-{oh})/2':s={output_dims},fps={frame_rate}" + logger.info(f"Job {job_id}: Using pan_rlr (right-left-right) effect.") + else: + # Default or fallback: Simple zoom-in (copied from original function) + zoom_speed_default = 0.03 # Default zoom speed if needed + zoom_factor = 1 + (zoom_speed_default * length) + vf_filter = f"scale={scale_dims}:force_original_aspect_ratio=decrease,pad={iw}:{ih}:(ow-iw)/2:(oh-ih)/2,zoompan=z='min(1+({zoom_speed_default}*on/{frame_rate}),{zoom_factor})':d={total_frames}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s={output_dims},fps={frame_rate}" + logger.warning(f"Job {job_id}: Unknown effect_type '{effect_type}'. Defaulting to simple zoom-in.") + + + # Prepare FFmpeg command + cmd = [ + 'ffmpeg', '-framerate', str(frame_rate), '-loop', '1', '-i', image_path, + '-vf', vf_filter, + '-c:v', 'libx264', '-r', str(frame_rate), '-t', str(length), '-pix_fmt', 'yuv420p', output_path + ] + + logger.info(f"Job {job_id}: Running FFmpeg command: {' '.join(cmd)}") + + # Run FFmpeg command + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + logger.error(f"Job {job_id}: FFmpeg command failed. Error: {result.stderr}") + # Clean up input file even on error + if os.path.exists(image_path): + os.remove(image_path) + raise subprocess.CalledProcessError(result.returncode, cmd, result.stdout, result.stderr) + + logger.info(f"Job {job_id}: Video created successfully: {output_path}") + + # Clean up input file + os.remove(image_path) + + return output_path + except Exception as e: + logger.error(f"Job {job_id}: Error in process_image_effects_video: {str(e)}", exc_info=True) + # Clean up potentially downloaded input file on error + if 'image_path' in locals() and os.path.exists(image_path): + try: + os.remove(image_path) + except OSError as rm_err: + logger.error(f"Job {job_id}: Error cleaning up input file {image_path}: {rm_err}") + raise From 810836dff9b428aeade9f367b6450ab30c8f3956 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Mon, 5 May 2025 14:43:07 +0530 Subject: [PATCH 07/13] docs: Add image effects endpoint to main README --- README.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4467429c..8abfb7e5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ +``` + ![Original Logo Symbol](https://github.com/user-attachments/assets/75173cf4-2502-4710-998b-6b81740ae1bd) # No-Code Architects Toolkit API @@ -57,6 +59,9 @@ Each endpoint is supported by robust payload validation and detailed API documen - **[`/v1/image/convert/video`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/image/convert/image_to_video.md)** - Transforms a static image into a video with custom duration and zoom effects. +- **[`/v1/image/effects/video`](https://github.com/fahimanwer/no-code-architects-toolkit/blob/main/docs/image/effects/image_effects_video.md)** + - Creates a video from an image with dynamic effects like zoom-in-out or panning. + ### Media - **[`/v1/media/convert`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/media/convert/media_convert.md)** @@ -255,7 +260,7 @@ You need to use the "webhook_url" (for any request that exceeds 1 min) in your A If you use the webhook_url, there is no limit to the processing length. -- [Digital Ocean App Platform Installation Guide](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/cloud-installation/do.md) - Deploy the API on Digital Ocean App Platform +- Digital Ocean App Platform Installation Guide - Deploy the API on Digital Ocean App Platform ### Google Cloud RUN Platform @@ -265,7 +270,7 @@ However this is one of the cheapest options with great performance because you'r Outside of that you are not charged. -#### Imporatnt: Requests exceeding 5+ minutes can be problemactic +#### Imporatnt: Requests exceeding 5+ minutes can be problemactic GCP Run will terminate long rununing processes, which can happen when processing larger files (whether you use the webhook_url or not). @@ -273,7 +278,7 @@ However, when your processing times are consistant lower than 5 minutes (e.g. yo They also have a GPU option that might be usable for better performance (untested). -- [Google Cloud RUN Platform (GCP) Installation Guide](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/cloud-installation/gcp.md) - Deploy the API on Google Cloud Run +- Google Cloud RUN Platform (GCP) Installation Guide - Deploy the API on Google Cloud Run ### General Docker Instructions @@ -281,17 +286,17 @@ You can use these instructions to deploy the NCA Toolkit to any linux server (on You can more easily control performance and cost this way, but requires more technical skill to get up and running (not much though). -- [General Docker Compose Installation Guide](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docker-compose.md) +- General Docker Compose Installation Guide ## Testing the API -1. Install the **[Postman Template](https://bit.ly/49Gkhl)** on your computer +1. Install the **Postman Template** on your computer 2. Import the API example requests from the template 3. Configure your environment variables in Postman: - `base_url`: Your deployed API URL - `x-api-key`: Your API key configured during installation 4. Use the example requests to validate that the API is functioning correctly -5. Use the **[NCA Toolkit API GPT](https://bit.ly/4feDDk4)** to explore additional features +5. Use the **NCA Toolkit API GPT** to explore additional features --- @@ -317,8 +322,8 @@ Thank you for your contributions! Get courses, community, support daily calls and more. -Join the **[No-Code Architects Community](https://www.skool.com/no-code-architects)** today! +Join the **No-Code Architects Community** today! ## License -This project is licensed under the [GNU General Public License v2.0 (GPL-2.0)](LICENSE). +This project is licensed under the GNU General Public License v2.0 (GPL-2.0). From 04729480d2c03f9d194baaf48db1d6e305e3b6b5 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Mon, 5 May 2025 14:47:21 +0530 Subject: [PATCH 08/13] docs: Add example payloads to README endpoints list --- README.md | 252 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 241 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8abfb7e5..170bf8b0 100644 --- a/README.md +++ b/README.md @@ -43,86 +43,314 @@ Each endpoint is supported by robust payload validation and detailed API documen - **[`/v1/audio/concatenate`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/audio/concatenate.md)** - Combines multiple audio files into a single audio file. + - Example Payload: + ```json + { + "audio_urls": [ + { "audio_url": "https://example.com/audio1.mp3" }, + { "audio_url": "https://example.com/audio2.mp3" } + ], + "webhook_url": "https://your-webhook-endpoint.com/callback", + "id": "custom-request-id-123" + } + ``` ### Code - **[`/v1/code/execute/python`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/code/execute/execute_python.md)** - Executes Python code remotely and returns the execution results. + - Example Payload: + ```json + { + "code": "print('Hello, World!')", + "timeout": 10, + "webhook_url": "https://example.com/webhook", + "id": "unique-request-id" + } + ``` ### FFmpeg - **[`/v1/ffmpeg/compose`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/ffmpeg/ffmpeg_compose.md)** - Provides a flexible interface to FFmpeg for complex media processing operations. + - Example Payload (Note: See documentation for details on filter structure): + ```json + { + "inputs": [ + { "file_url": "https://example.com/video1.mp4" } + ], + "filters": [ + { "filter": "hflip" } + ], + "outputs": [ + { + "options": [ + { "option": "-c:v", "argument": "libx264" }, + { "option": "-crf", "argument": 23 } + ] + } + ], + "webhook_url": "https://example.com/webhook", + "id": "unique-request-id" + } + ``` ### Image - **[`/v1/image/convert/video`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/image/convert/image_to_video.md)** - Transforms a static image into a video with custom duration and zoom effects. + - Example Payload: + ```json + { + "image_url": "https://example.com/image.jpg", + "length": 10, + "frame_rate": 24, + "zoom_speed": 5, + "orientation": "landscape", + "webhook_url": "https://example.com/webhook", + "id": "request-123" + } + ``` - **[`/v1/image/effects/video`](https://github.com/fahimanwer/no-code-architects-toolkit/blob/main/docs/image/effects/image_effects_video.md)** - Creates a video from an image with dynamic effects like zoom-in-out or panning. + - Example Payload (Zoom In/Out): + ```json + { + "image_url": "https://example.com/image.jpg", + "effect_type": "zoom_in_out", + "length": 10, + "orientation": "landscape", + "id": "request-zoom" + } + ``` ### Media - **[`/v1/media/convert`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/media/convert/media_convert.md)** - Converts media files from one format to another with customizable codec options. + - Example Payload: + ```json + { + "media_url": "https://example.com/video.mp4", + "format": "avi", + "video_codec": "libx264", + "audio_codec": "aac", + "webhook_url": "https://example.com/webhook", + "id": "unique-request-id" + } + ``` - **[`/v1/media/convert/mp3`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/media/convert/media_to_mp3.md)** - Converts various media formats specifically to MP3 audio. + - Example Payload: + ```json + { + "media_url": "https://example.com/video.mp4", + "webhook_url": "https://example.com/webhook", + "id": "unique-request-id", + "bitrate": "192k" + } + ``` - **[`/v1/BETA/media/download`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/media/download.md)** - Downloads media content from various online sources using yt-dlp. + - Example Payload: + ```json + { + "media_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "webhook_url": "https://example.com/webhook", + "id": "custom-request-123", + "format": { + "quality": "best", + "resolution": "720p" + }, + "audio": { + "extract": true, + "format": "mp3" + } + } + ``` - **[`/v1/media/feedback`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/media/feedback.md)** - - Provides a web interface for collecting and displaying feedback on media content. + - Provides a web interface for collecting and displaying feedback on media content. (GET endpoint, no JSON payload) - **[`/v1/media/transcribe`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/media/media_transcribe.md)** - Transcribes or translates audio/video content from a provided media URL. + - Example Payload: + ```json + { + "media_url": "https://example.com/media/file.mp3", + "task": "transcribe", + "include_text": true, + "include_srt": true, + "response_type": "cloud", + "webhook_url": "https://your-webhook.com/callback", + "id": "custom-job-123" + } + ``` - **[`/v1/media/silence`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/media/silence.md)** - Detects silence intervals in a given media file. + - Example Payload: + ```json + { + "media_url": "https://example.com/audio.mp3", + "start": "00:00:10.0", + "end": "00:01:00.0", + "duration": 0.5, + "webhook_url": "https://example.com/webhook", + "id": "unique-request-id" + } + ``` - **[`/v1/media/metadata`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/media/metadata.md)** - Extracts comprehensive metadata from media files including format, codecs, resolution, and bitrates. + - Example Payload: + ```json + { + "media_url": "https://example.com/media.mp4", + "webhook_url": "https://example.com/webhook", + "id": "custom-id" + } + ``` ### S3 - **[`/v1/s3/upload`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/s3/upload.md)** - Uploads files to Amazon S3 storage by streaming directly from a URL. + - Example Payload: + ```json + { + "file_url": "https://example.com/path/to/file.mp4", + "filename": "custom-name.mp4", + "public": true + } + ``` ### Toolkit - **[`/v1/toolkit/authenticate`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/toolkit/authenticate.md)** - - Provides a simple authentication mechanism to validate API keys. + - Provides a simple authentication mechanism to validate API keys. (GET endpoint, uses `X-API-Key` header, no JSON payload) - **[`/v1/toolkit/test`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/toolkit/test.md)** - - Verifies that the NCA Toolkit API is properly installed and functioning. + - Verifies that the NCA Toolkit API is properly installed and functioning. (GET endpoint, uses `X-API-Key` header, no JSON payload) - **[`/v1/toolkit/job/status`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/toolkit/job_status.md)** - Retrieves the status of a specific job by its ID. + - Example Payload: + ```json + { + "job_id": "e6d7f3c0-9c9f-4b8a-b7c3-f0e3c9f6b9d7" + } + ``` - **[`/v1/toolkit/jobs/status`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/toolkit/jobs_status.md)** - Retrieves the status of all jobs within a specified time range. + - Example Payload (Optional): + ```json + { + "since_seconds": 3600 + } + ``` ### Video - **[`/v1/video/caption`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/video/caption_video.md)** - Adds customizable captions to videos with various styling options. + - Example Payload (Auto-transcribe): + ```json + { + "video_url": "https://example.com/video.mp4", + "settings": { + "style": "karaoke", + "line_color": "#FFFFFF", + "word_color": "#FFFF00" + }, + "webhook_url": "https://example.com/webhook" + } + ``` - **[`/v1/video/concatenate`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/video/concatenate.md)** - Combines multiple videos into a single continuous video file. + - Example Payload: + ```json + { + "video_urls": [ + {"video_url": "https://example.com/video1.mp4"}, + {"video_url": "https://example.com/video2.mp4"} + ], + "webhook_url": "https://example.com/webhook", + "id": "request-123" + } + ``` - **[`/v1/video/thumbnail`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/video/thumbnail.md)** - Extracts a thumbnail image from a specific timestamp in a video. + - Example Payload: + ```json + { + "video_url": "https://example.com/video.mp4", + "second": 30, + "webhook_url": "https://your-service.com/webhook", + "id": "custom-request-123" + } + ``` - **[`/v1/video/cut`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/video/cut.md)** - Cuts specified segments from a video file with optional encoding settings. + - Example Payload: + ```json + { + "video_url": "https://example.com/video.mp4", + "cuts": [ + { + "start": "00:00:10.000", + "end": "00:00:20.000" + }, + { + "start": "00:00:30.000", + "end": "00:00:40.000" + } + ], + "webhook_url": "https://example.com/webhook", + "id": "unique-request-id" + } + ``` - **[`/v1/video/split`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/video/split.md)** - Splits a video into multiple segments based on specified start and end times. + - Example Payload: + ```json + { + "video_url": "https://example.com/video.mp4", + "splits": [ + { + "start": "00:00:10.000", + "end": "00:00:20.000" + }, + { + "start": "00:00:30.000", + "end": "00:00:40.000" + } + ], + "webhook_url": "https://example.com/webhook", + "id": "unique-request-id" + } + ``` - **[`/v1/video/trim`](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/video/trim.md)** - Trims a video by keeping only the content between specified start and end times. + - Example Payload: + ```json + { + "video_url": "https://example.com/video.mp4", + "start": "00:01:00", + "end": "00:03:00", + "webhook_url": "https://example.com/webhook", + "id": "unique-request-id" + } + ``` --- @@ -260,7 +488,7 @@ You need to use the "webhook_url" (for any request that exceeds 1 min) in your A If you use the webhook_url, there is no limit to the processing length. -- Digital Ocean App Platform Installation Guide - Deploy the API on Digital Ocean App Platform +- [Digital Ocean App Platform Installation Guide](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/cloud-installation/do.md) - Deploy the API on Digital Ocean App Platform ### Google Cloud RUN Platform @@ -270,7 +498,7 @@ However this is one of the cheapest options with great performance because you'r Outside of that you are not charged. -#### Imporatnt: Requests exceeding 5+ minutes can be problemactic +#### Imporatnt: Requests exceeding 5+ minutes can be problemactic GCP Run will terminate long rununing processes, which can happen when processing larger files (whether you use the webhook_url or not). @@ -278,7 +506,7 @@ However, when your processing times are consistant lower than 5 minutes (e.g. yo They also have a GPU option that might be usable for better performance (untested). -- Google Cloud RUN Platform (GCP) Installation Guide - Deploy the API on Google Cloud Run +- [Google Cloud RUN Platform (GCP) Installation Guide](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docs/cloud-installation/gcp.md) - Deploy the API on Google Cloud Run ### General Docker Instructions @@ -286,17 +514,17 @@ You can use these instructions to deploy the NCA Toolkit to any linux server (on You can more easily control performance and cost this way, but requires more technical skill to get up and running (not much though). -- General Docker Compose Installation Guide +- [General Docker Compose Installation Guide](https://github.com/stephengpope/no-code-architects-toolkit/blob/main/docker-compose.md) ## Testing the API -1. Install the **Postman Template** on your computer +1. Install the **[Postman Template](https://bit.ly/49Gkhl)** on your computer 2. Import the API example requests from the template 3. Configure your environment variables in Postman: - `base_url`: Your deployed API URL - `x-api-key`: Your API key configured during installation 4. Use the example requests to validate that the API is functioning correctly -5. Use the **NCA Toolkit API GPT** to explore additional features +5. Use the **[NCA Toolkit API GPT](https://bit.ly/4feDDk4)** to explore additional features --- @@ -322,8 +550,10 @@ Thank you for your contributions! Get courses, community, support daily calls and more. -Join the **No-Code Architects Community** today! +Join the **[No-Code Architects Community](https://www.skool.com/no-code-architects)** today! ## License -This project is licensed under the GNU General Public License v2.0 (GPL-2.0). +This project is licensed under the [GNU General Public License v2.0 (GPL-2.0)](LICENSE). + +``` \ No newline at end of file From 62011f88f5ba95ace33a631954207e9e57593f39 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Mon, 5 May 2025 14:48:21 +0530 Subject: [PATCH 09/13] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 170bf8b0..55644748 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ - -``` - ![Original Logo Symbol](https://github.com/user-attachments/assets/75173cf4-2502-4710-998b-6b81740ae1bd) # No-Code Architects Toolkit API @@ -556,4 +553,3 @@ Join the **[No-Code Architects Community](https://www.skool.com/no-code-architec This project is licensed under the [GNU General Public License v2.0 (GPL-2.0)](LICENSE). -``` \ No newline at end of file From 506dac06d664dfaf845cc3dcdb23a54b7cffb2f2 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Wed, 7 May 2025 02:39:51 +0530 Subject: [PATCH 10/13] Fix: Correct SyntaxError in get_scenes service --- services/v1/video/get_scenes.py | 190 ++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 services/v1/video/get_scenes.py diff --git a/services/v1/video/get_scenes.py b/services/v1/video/get_scenes.py new file mode 100644 index 00000000..8f2c3b84 --- /dev/null +++ b/services/v1/video/get_scenes.py @@ -0,0 +1,190 @@ +# Copyright (c) 2025 Stephen G. Pope +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import subprocess +import logging +import re +from services.file_management import download_file +from services.cloud_storage import upload_file +from config import LOCAL_STORAGE_PATH + +logger = logging.getLogger(__name__) + +def is_youtube_url(url): + """Check if the URL is a YouTube URL.""" + # Explicitly double backslashes for escapes in regex + youtube_regex = ( + r'(https?://)?(www\.)?' # Optional http/https and www. + r'(youtube|youtu|youtube-nocookie)\.(com|be)/' # Domain + r'(watch\?v=|embed/|v/|.+\?v=|live/)?' # Optional path parts + r'([^&=%\?]{11})' # Video ID (11 chars excluding & = % ?) + ) + return re.match(youtube_regex, url) is not None + +def download_youtube_video(url, output_template, job_id): + """Downloads video (no audio) from YouTube using yt-dlp.""" + output_path = output_template.format(job_id=job_id, timestamp="video", ext="mp4") # Predictable name + # -f 'bv*[ext=mp4]': best video-only stream with mp4 extension + # --no-audio: Explicitly try to avoid audio download/muxing if separate + # Using a temporary template first to get the final name + temp_output_template = os.path.join(LOCAL_STORAGE_PATH, f"{job_id}_temp.%(ext)s") + final_output_path = os.path.join(LOCAL_STORAGE_PATH, f"{job_id}_video.mp4") + + cmd = [ + 'yt-dlp', + '-f', 'bv*[ext=mp4]', # Best video-only mp4 + '--no-audio', # Attempt to ensure no audio track included + '--output', temp_output_template, + url + ] + logger.info(f"Job {job_id}: Running yt-dlp command: {' '.join(cmd)}") + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + logger.info(f"Job {job_id}: yt-dlp stdout: {result.stdout}") + logger.info(f"Job {job_id}: yt-dlp stderr: {result.stderr}") + + # Find the actually downloaded file (yt-dlp might adjust extension) + downloaded_file = None + for f in os.listdir(LOCAL_STORAGE_PATH): + if f.startswith(f"{job_id}_temp"): + downloaded_file = os.path.join(LOCAL_STORAGE_PATH, f) + break + + if not downloaded_file or not os.path.exists(downloaded_file): + raise Exception("yt-dlp completed but output file not found.") + + # Rename to predictable final path + os.rename(downloaded_file, final_output_path) + logger.info(f"Job {job_id}: YouTube video downloaded and renamed to {final_output_path}") + return final_output_path + + except subprocess.CalledProcessError as e: + logger.error(f"Job {job_id}: yt-dlp command failed. Error: {e.stderr}") + raise Exception(f"yt-dlp failed: {e.stderr}") + except Exception as e: + logger.error(f"Job {job_id}: Error during YouTube download: {str(e)}") + raise + +def process_get_scenes(video_url, timestamps, job_id): + """ + Downloads a video (handles YouTube URLs) and extracts frames at specified timestamps. + Uploads frames to cloud storage and returns their URLs. + """ + local_video_path = None + generated_files = [] + scene_urls = [] + + output_template = os.path.join(LOCAL_STORAGE_PATH, f"{job_id}_scene_{{timestamp}}.jpg") + + try: + # --- 1. Download Video --- + if is_youtube_url(video_url): + logger.info(f"Job {job_id}: Detected YouTube URL. Downloading with yt-dlp...") + local_video_path = download_youtube_video(video_url, output_template, job_id) + else: + logger.info(f"Job {job_id}: Downloading standard URL...") + local_video_path = download_file(video_url, LOCAL_STORAGE_PATH) + # Rename downloaded file for consistency if needed (optional) + base, ext = os.path.splitext(local_video_path) + consistent_path = os.path.join(LOCAL_STORAGE_PATH, f"{job_id}_video{ext}") + if local_video_path != consistent_path: + os.rename(local_video_path, consistent_path) + local_video_path = consistent_path + logger.info(f"Job {job_id}: Video downloaded to {local_video_path}") + + if not local_video_path or not os.path.exists(local_video_path): + raise Exception("Video download failed or file not found.") + + # --- 2. Extract Frames --- + unique_timestamps = sorted(list(set(timestamps))) # Ensure uniqueness and order + + for ts in unique_timestamps: + if not isinstance(ts, (int, float)) or ts < 0: + logger.warning(f"Job {job_id}: Skipping invalid timestamp {ts}") + continue + + # Ensure timestamp string is safe for filenames + ts_str = str(ts).replace('.', '_') + scene_filename = output_template.format(timestamp=ts_str) + generated_files.append(scene_filename) + + # Use -ss before -i for faster seeking + cmd = [ + 'ffmpeg', + '-ss', str(ts), + '-i', local_video_path, + '-vframes', '1', + '-q:v', '2', # Good quality JPEG + scene_filename + ] + logger.info(f"Job {job_id}: Extracting frame at {ts}s: {' '.join(cmd)}") + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + # Log error but try to continue with other timestamps + logger.error(f"Job {job_id}: Failed to extract frame at {ts}s. Error: {e.stderr}") + # Remove potentially incomplete file + if os.path.exists(scene_filename): + os.remove(scene_filename) + generated_files.remove(scene_filename) # Don't try to upload it + + # --- 3. Upload Frames --- + logger.info(f"Job {job_id}: Uploading {len(generated_files)} extracted scenes...") + for scene_file in generated_files: + if os.path.exists(scene_file): + try: + cloud_url = upload_file(scene_file) + # Extract timestamp safely from filename + try: + base_name = os.path.basename(scene_file) + # Split by the last occurrence of '_scene_' before the extension + parts = base_name.rsplit('_scene_', 1) + if len(parts) == 2: + ts_part = parts[1].replace('.jpg', '') + original_ts = float(ts_part.replace('_', '.')) + scene_urls.append({"timestamp": original_ts, "url": cloud_url}) + else: + logger.warning(f"Job {job_id}: Could not parse timestamp from filename {scene_file}") + except Exception as parse_err: + logger.error(f"Job {job_id}: Error parsing timestamp from {scene_file}: {parse_err}") + except Exception as upload_err: + logger.error(f"Job {job_id}: Failed to upload scene {scene_file}. Error: {upload_err}") + # Optionally collect failed uploads info? For now, just log. + + logger.info(f"Job {job_id}: Scene extraction and upload complete.") + return scene_urls + + except Exception as e: + logger.error(f"Job {job_id}: Error in process_get_scenes: {str(e)}", exc_info=True) + raise # Re-raise the exception to be caught by the route handler + + finally: + # --- 4. Cleanup --- + logger.info(f"Job {job_id}: Cleaning up temporary files...") + if local_video_path and os.path.exists(local_video_path): + try: + os.remove(local_video_path) + logger.debug(f"Job {job_id}: Removed video file {local_video_path}") + except OSError as e: + logger.error(f"Job {job_id}: Error removing video file {local_video_path}: {e}") + for scene_file in generated_files: + if os.path.exists(scene_file): + try: + os.remove(scene_file) + logger.debug(f"Job {job_id}: Removed scene file {scene_file}") + except OSError as e: + logger.error(f"Job {job_id}: Error removing scene file {scene_file}: {e}") From 2ce9a07005ddd7a7ae64444acd7fcf7059d6f891 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Wed, 7 May 2025 02:42:15 +0530 Subject: [PATCH 11/13] Update README.md --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 55644748..0949d757 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,17 @@ Each endpoint is supported by robust payload validation and detailed API documen "id": "unique-request-id" } ``` + - **[`/v1/video/get-scenes`](https://github.com/fahimanwer/no-code-architects-toolkit/blob/main/docs/video/get_scenes.md)** + - Extracts frames (thumbnails) from multiple specified timestamps in a video. + - Example Payload: + ```json + { + "video_url": "https://example.com/my_video.mp4", + "timestamps": [5, 15.5, 25], + "id": "local-video-scenes", + "webhook_url": "https://example.com/webhook" + } + ``` --- From 6fd6fe08f4fbd9aadff13db88022e386c86d0641 Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Wed, 7 May 2025 03:00:59 +0530 Subject: [PATCH 12/13] Update Dockerfile to include yt-dlp installation --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 77dbc757..b5c2e262 100644 --- a/Dockerfile +++ b/Dockerfile @@ -165,7 +165,8 @@ COPY requirements.txt . RUN pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir -r requirements.txt && \ pip install openai-whisper && \ - pip install jsonschema + pip install jsonschema && \ + pip install yt-dlp # Create the appuser RUN useradd -m appuser @@ -197,4 +198,4 @@ gunicorn --bind 0.0.0.0:8080 \ chmod +x /app/run_gunicorn.sh # Run the shell script -CMD ["/app/run_gunicorn.sh"] \ No newline at end of file +CMD ["/app/run_gunicorn.sh"] From 50a307caf8cab9d66497f6f989954155e9ce3a4a Mon Sep 17 00:00:00 2001 From: fahimanwer <36186398+fahimanwer@users.noreply.github.com> Date: Wed, 7 May 2025 05:03:56 +0530 Subject: [PATCH 13/13] Docs: Add Development Workflow & Disk Space Management section --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0949d757..7cc12513 100644 --- a/README.md +++ b/README.md @@ -348,7 +348,8 @@ Each endpoint is supported by robust payload validation and detailed API documen "id": "unique-request-id" } ``` - - **[`/v1/video/get-scenes`](https://github.com/fahimanwer/no-code-architects-toolkit/blob/main/docs/video/get_scenes.md)** + +- **[`/v1/video/get-scenes`](https://github.com/fahimanwer/no-code-architects-toolkit/blob/main/docs/video/get_scenes.md)** - Extracts frames (thumbnails) from multiple specified timestamps in a video. - Example Payload: ```json @@ -536,6 +537,82 @@ You can more easily control performance and cost this way, but requires more tec --- +## Development Workflow & Disk Space Management + +Developing new features or fixing bugs typically involves building and testing Docker images. Over time, this can lead to an accumulation of old images and build cache, consuming significant disk space. Here's a recommended workflow and tips for managing disk usage: + +### Managing Docker Disk Space + +It's good practice to periodically clean up unused Docker resources: + +* **Prune unused images, containers, and networks:** + ```bash + docker system prune -a + ``` + (Use with caution: this will remove all stopped containers, all networks not used by at least one container, all dangling images, and all dangling build cache.) + +* **List images to identify old/large ones:** + ```bash + docker images + ``` + +* **Remove specific images by ID or tag:** + ```bash + docker rmi + ``` + +* **Prune build cache:** + ```bash + docker builder prune + ``` + +### Feature Development Workflow + +1. **Create a new branch:** Start by creating a new branch in your local Git repository for the feature or fix (e.g., `git checkout -b feature/my-new-endpoint`). +2. **Implement changes:** Make your code changes (e.g., add new service logic, routes, update `app.py`). +3. **Update Documentation:** Create or update any relevant documentation files in the `/docs` directory and ensure the main `README.md` is updated to reflect new endpoints or significant changes in behavior, including example payloads. +4. **Local Testing:** + * **Build a local Docker image:** + ```bash + docker build -t nca-toolkit-dev . + ``` + (Use a temporary tag like `nca-toolkit-dev` to avoid conflicting with official tags). + * **Run the local image:** + ```bash + docker run -d -p 8080:8080 --name nca-toolkit-test -e API_KEY=your_test_key [other_env_vars_as_needed] nca-toolkit-dev + ``` + * Thoroughly test the new feature using Postman or cURL. Check container logs (`docker logs nca-toolkit-test -f`) for errors. + * Stop and remove the test container: `docker stop nca-toolkit-test && docker rm nca-toolkit-test`. +5. **Commit Changes:** Once satisfied, commit your changes to your feature branch with clear commit messages. +6. **Push to GitHub:** + * Push your feature branch to your fork on GitHub. + * Create a Pull Request against the `main` branch of the `fahimanwer/no-code-architects-toolkit` repository (or the appropriate upstream repository if contributing to `stephengpope/no-code-architects-toolkit`). +7. **Update Docker Hub (After PR Merge - Optional for Personal Use):** + * Once your changes are merged into the `main` branch of your primary repository (e.g., `fahimanwer/no-code-architects-toolkit`), you can build the official image and push it to your Docker Hub. + * Ensure your local `main` branch is up to date: `git checkout main && git pull origin main`. + * **Build the image:** + ```bash + docker build -t yourdockerhubusername/repositoryname:latest -t yourdockerhubusername/repositoryname:version_tag . + # Example: + # docker build -t fahimanwer18/nca-toolkit-fahim:latest -t fahimanwer18/nca-toolkit-fahim:0.2.0 . + ``` + * **Log in to Docker Hub:** + ```bash + docker login + ``` + * **Push the image:** + ```bash + docker push yourdockerhubusername/repositoryname:latest + docker push yourdockerhubusername/repositoryname:version_tag + # Example: + # docker push fahimanwer18/nca-toolkit-fahim:latest + # docker push fahimanwer18/nca-toolkit-fahim:0.2.0 + ``` + +By following these steps, you can keep your development environment clean and ensure that your GitHub and Docker Hub repositories stay synchronized with tested features. + +--- + ## Contributing To the NCA Toolkit API We welcome contributions from the public! If you'd like to contribute to this project, please follow these steps: @@ -563,4 +640,3 @@ Join the **[No-Code Architects Community](https://www.skool.com/no-code-architec ## License This project is licensed under the [GNU General Public License v2.0 (GPL-2.0)](LICENSE). -