resize on download
Browse files- src/automation.py +9 -6
- src/utils.py +81 -1
src/automation.py
CHANGED
|
@@ -304,24 +304,24 @@ class ContentAutomation:
|
|
| 304 |
if assets.get("hook_video") and assets["hook_video"].get("video_url"):
|
| 305 |
hook_url = assets["hook_video"]["video_url"]
|
| 306 |
download_tasks.append(
|
| 307 |
-
self._download_with_fallback(hook_url, "hook_video.mp4", assets["hook_video"], "local_path")
|
| 308 |
)
|
| 309 |
# VEO library videos
|
| 310 |
if assets["hook_video"].get("veo_video_data") and assets["hook_video"].get("veo_video_data").get("video_url"):
|
| 311 |
veo_hook_url = assets["hook_video"]["veo_video_data"]["video_url"]
|
| 312 |
download_tasks.append(
|
| 313 |
-
self._download_with_fallback(veo_hook_url, "veo_hook_url.mp4", assets["hook_video"]["veo_video_data"], "local_path")
|
| 314 |
)
|
| 315 |
|
| 316 |
# Download library videos
|
| 317 |
for i, video in enumerate(assets.get("selected_videos", [])):
|
| 318 |
if video.get("url"):
|
| 319 |
download_tasks.append(
|
| 320 |
-
self._download_with_fallback(video["url"], f"library_video_{i}.mp4", video, "local_path")
|
| 321 |
)
|
| 322 |
if video.get("alternate_url"):
|
| 323 |
download_tasks.append(
|
| 324 |
-
self._download_with_fallback(video["alternate_url"], f"library_all_video_alternate_url_{i}.mp4", video, "alternate_url_local_path")
|
| 325 |
)
|
| 326 |
|
| 327 |
# Download library videos
|
|
@@ -343,12 +343,15 @@ class ContentAutomation:
|
|
| 343 |
# Verify all required assets have local_path
|
| 344 |
self._verify_assets_downloaded(assets)
|
| 345 |
|
| 346 |
-
async def _download_with_fallback(self, url: str, filename: str, target_dict: Dict, key: str = "local_path"):
|
| 347 |
"""Download file with fallback to ensure local_path is always set"""
|
| 348 |
try:
|
| 349 |
local_path = await self.api_clients.download_file(url, filename)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
target_dict[key] = local_path
|
| 351 |
-
await self.api_clients.upload_to_temp_gcs(local_path)
|
| 352 |
logger.info(f"✓ Downloaded {filename}")
|
| 353 |
return local_path
|
| 354 |
except Exception as e:
|
|
|
|
| 304 |
if assets.get("hook_video") and assets["hook_video"].get("video_url"):
|
| 305 |
hook_url = assets["hook_video"]["video_url"]
|
| 306 |
download_tasks.append(
|
| 307 |
+
self._download_with_fallback(hook_url, "hook_video.mp4", assets["hook_video"], "local_path", resize=True)
|
| 308 |
)
|
| 309 |
# VEO library videos
|
| 310 |
if assets["hook_video"].get("veo_video_data") and assets["hook_video"].get("veo_video_data").get("video_url"):
|
| 311 |
veo_hook_url = assets["hook_video"]["veo_video_data"]["video_url"]
|
| 312 |
download_tasks.append(
|
| 313 |
+
self._download_with_fallback(veo_hook_url, "veo_hook_url.mp4", assets["hook_video"]["veo_video_data"], "local_path", resize=True, remove_black_padding=True)
|
| 314 |
)
|
| 315 |
|
| 316 |
# Download library videos
|
| 317 |
for i, video in enumerate(assets.get("selected_videos", [])):
|
| 318 |
if video.get("url"):
|
| 319 |
download_tasks.append(
|
| 320 |
+
self._download_with_fallback(video["url"], f"library_video_{i}.mp4", video, "local_path", resize=True)
|
| 321 |
)
|
| 322 |
if video.get("alternate_url"):
|
| 323 |
download_tasks.append(
|
| 324 |
+
self._download_with_fallback(video["alternate_url"], f"library_all_video_alternate_url_{i}.mp4", video, "alternate_url_local_path", resize=True)
|
| 325 |
)
|
| 326 |
|
| 327 |
# Download library videos
|
|
|
|
| 343 |
# Verify all required assets have local_path
|
| 344 |
self._verify_assets_downloaded(assets)
|
| 345 |
|
| 346 |
+
async def _download_with_fallback(self, url: str, filename: str, target_dict: Dict, key: str = "local_path", resize: bool = False, remove_black_padding: bool = False):
|
| 347 |
"""Download file with fallback to ensure local_path is always set"""
|
| 348 |
try:
|
| 349 |
local_path = await self.api_clients.download_file(url, filename)
|
| 350 |
+
if remove_black_padding:
|
| 351 |
+
utils.remove_black_padding(local_path, overwrite=True)
|
| 352 |
+
if resize:
|
| 353 |
+
utils.resize_video(local_path, overwrite=True)
|
| 354 |
target_dict[key] = local_path
|
|
|
|
| 355 |
logger.info(f"✓ Downloaded {filename}")
|
| 356 |
return local_path
|
| 357 |
except Exception as e:
|
src/utils.py
CHANGED
|
@@ -12,6 +12,8 @@ import imagehash
|
|
| 12 |
import subprocess
|
| 13 |
import os
|
| 14 |
import uuid
|
|
|
|
|
|
|
| 15 |
|
| 16 |
class ColoredFormatter(logging.Formatter):
|
| 17 |
"""Custom formatter with colors for terminal output"""
|
|
@@ -640,4 +642,82 @@ def interpolate_video(input_path: str, target_duration: float = 4.0, fps: int =
|
|
| 640 |
]
|
| 641 |
|
| 642 |
subprocess.run(cmd, check=True)
|
| 643 |
-
return output_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
import subprocess
|
| 13 |
import os
|
| 14 |
import uuid
|
| 15 |
+
import re
|
| 16 |
+
import shutil
|
| 17 |
|
| 18 |
class ColoredFormatter(logging.Formatter):
|
| 19 |
"""Custom formatter with colors for terminal output"""
|
|
|
|
| 642 |
]
|
| 643 |
|
| 644 |
subprocess.run(cmd, check=True)
|
| 645 |
+
return output_path
|
| 646 |
+
|
| 647 |
+
def resize_video(input_path: str, target_width: int = 1080, target_height: int = 1920, overwrite: bool = False) -> str:
|
| 648 |
+
"""
|
| 649 |
+
Resize a video to the given resolution (default 1080x1920) using FFmpeg.
|
| 650 |
+
If overwrite=True, replaces the original file safely after successful conversion.
|
| 651 |
+
"""
|
| 652 |
+
if not os.path.exists(input_path):
|
| 653 |
+
raise FileNotFoundError(f"Input video not found: {input_path}")
|
| 654 |
+
|
| 655 |
+
temp_output = os.path.join("/tmp", f"{uuid.uuid4().hex}.mp4")
|
| 656 |
+
|
| 657 |
+
# FFmpeg resize command (output goes to /tmp first)
|
| 658 |
+
cmd = [
|
| 659 |
+
"ffmpeg", "-y", "-i", input_path,
|
| 660 |
+
"-vf", f"scale={target_width}:{target_height}",
|
| 661 |
+
"-c:v", "libx264", "-crf", "18", "-preset", "slow",
|
| 662 |
+
"-c:a", "copy",
|
| 663 |
+
temp_output
|
| 664 |
+
]
|
| 665 |
+
|
| 666 |
+
# Run FFmpeg process
|
| 667 |
+
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 668 |
+
if result.returncode != 0:
|
| 669 |
+
raise RuntimeError(f"FFmpeg failed:\n{result.stderr.decode('utf-8', errors='ignore')}")
|
| 670 |
+
|
| 671 |
+
# Overwrite original safely if requested
|
| 672 |
+
if overwrite:
|
| 673 |
+
shutil.move(temp_output, input_path)
|
| 674 |
+
return input_path
|
| 675 |
+
|
| 676 |
+
return temp_output
|
| 677 |
+
|
| 678 |
+
def remove_black_padding(input_path: str, overwrite: bool = False) -> str:
|
| 679 |
+
"""
|
| 680 |
+
Automatically detect and remove black padding (crop only) using FFmpeg.
|
| 681 |
+
Saves to /tmp with a unique UUID filename unless overwrite=True.
|
| 682 |
+
|
| 683 |
+
Args:
|
| 684 |
+
input_path (str): Path to the input video.
|
| 685 |
+
overwrite (bool): If True, safely replace the original file.
|
| 686 |
+
|
| 687 |
+
Returns:
|
| 688 |
+
str: Path to the cropped video.
|
| 689 |
+
"""
|
| 690 |
+
if not os.path.exists(input_path):
|
| 691 |
+
raise FileNotFoundError(f"Input video not found: {input_path}")
|
| 692 |
+
|
| 693 |
+
# Step 1: Detect crop parameters using cropdetect
|
| 694 |
+
detect_cmd = [
|
| 695 |
+
"ffmpeg", "-i", input_path, "-vf", "cropdetect=24:16:0",
|
| 696 |
+
"-frames:v", "500", "-f", "null", "-"
|
| 697 |
+
]
|
| 698 |
+
result = subprocess.run(detect_cmd, stderr=subprocess.PIPE, text=True)
|
| 699 |
+
matches = re.findall(r"crop=\S+", result.stderr)
|
| 700 |
+
|
| 701 |
+
if not matches:
|
| 702 |
+
raise RuntimeError("Could not detect any crop region — video may not have black bars.")
|
| 703 |
+
|
| 704 |
+
# Get most frequent crop value
|
| 705 |
+
crop_value = max(set(matches), key=matches.count)
|
| 706 |
+
logger.info(f"Detected crop: {crop_value}")
|
| 707 |
+
|
| 708 |
+
# Step 2: Create temp output file
|
| 709 |
+
tmp_output = os.path.join("/tmp", f"{uuid.uuid4().hex}_cropped.mp4")
|
| 710 |
+
|
| 711 |
+
# Step 3: Run FFmpeg crop command
|
| 712 |
+
crop_cmd = ["ffmpeg", "-y", "-i", input_path, "-vf", crop_value, "-c:a", "copy", tmp_output]
|
| 713 |
+
crop_proc = subprocess.run(crop_cmd, stderr=subprocess.PIPE, text=True)
|
| 714 |
+
|
| 715 |
+
if crop_proc.returncode != 0:
|
| 716 |
+
raise RuntimeError(f"FFmpeg crop failed:\n{crop_proc.stderr}")
|
| 717 |
+
|
| 718 |
+
# Step 4: Handle overwrite safely
|
| 719 |
+
if overwrite:
|
| 720 |
+
shutil.move(tmp_output, input_path)
|
| 721 |
+
return input_path
|
| 722 |
+
|
| 723 |
+
return tmp_output
|