mirror of
https://github.com/vondas-network/videobeaux.git
synced 2025-12-11 18:35:01 +01:00
added commands to example for newer programs
This commit is contained in:
@@ -1,91 +1,302 @@
|
|||||||
|
# from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
|
||||||
|
# from pathlib import Path
|
||||||
|
# import sys
|
||||||
|
# import math
|
||||||
|
|
||||||
|
# # Video dimension presets (unchanged)
|
||||||
|
# VIDEO_PRESETS = {
|
||||||
|
# "sd": (320, 240), "720hd": (640, 360), "1080hd": (960, 540), "widescreen": (320, 180),
|
||||||
|
# "portrait1080": (1080, 1620), "480p": (640, 480), "576p": (720, 576), "720p": (1280, 720),
|
||||||
|
# "1080p": (1920, 1080), "1440p": (2560, 1440), "4k": (3840, 2160), "8k": (7680, 4320),
|
||||||
|
# "vga": (640, 480), "qvga": (320, 240), "wvga": (800, 480), "svga": (800, 600),
|
||||||
|
# "xga": (1024, 768), "wxga": (1280, 800), "sxga": (1280, 1024), "uxga": (1600, 1200),
|
||||||
|
# "wuxga": (1920, 1200), "qwxga": (2048, 1152), "qhd": (2560, 1440), "wqxga": (2560, 1600),
|
||||||
|
# "5k": (5120, 2880), "portrait720": (720, 1280), "portrait4k": (2160, 3840),
|
||||||
|
# "square1080": (1080, 1080), "square720": (720, 720), "cinema4k": (4096, 2160),
|
||||||
|
# "ultrawide1080": (2560, 1080), "ultrawide1440": (3440, 1440),
|
||||||
|
# "instagram_feed": (1080, 1080), "instagram_reels": (1080, 1920),
|
||||||
|
# "instagram_stories": (1080, 1920), "tiktok_video": (1080, 1920),
|
||||||
|
# "youtube_standard": (1920, 1080), "youtube_shorts": (1080, 1920),
|
||||||
|
# "facebook_feed": (1080, 1080), "facebook_stories": (1080, 1920),
|
||||||
|
# "twitter_video": (1280, 720), "twitter_square": (1080, 1080),
|
||||||
|
# "linkedin_video": (1920, 1080), "linkedin_square": (1080, 1080),
|
||||||
|
# "snapchat_video": (1080, 1920), "pinterest_video": (1080, 1920),
|
||||||
|
# "pinterest_square": (1000, 1000)
|
||||||
|
# }
|
||||||
|
|
||||||
|
# def register_arguments(parser):
|
||||||
|
# parser.description = (
|
||||||
|
# "Converts an input video to a specified preset dimension. "
|
||||||
|
# "Choose how to handle aspect ratio: pad, fit, fill-crop, or stretch. "
|
||||||
|
# "Use --portrait-full to force a 9:16 full-frame vertical output."
|
||||||
|
# )
|
||||||
|
# parser.add_argument(
|
||||||
|
# "--output-format", required=True, type=str,
|
||||||
|
# help="Format to convert output into (e.g., mp4, mov). Output can be a filename without extension."
|
||||||
|
# )
|
||||||
|
# parser.add_argument(
|
||||||
|
# "--preset", required=True, type=str, choices=VIDEO_PRESETS.keys(),
|
||||||
|
# help="Preset dimension (e.g., 1080p, instagram_reels)."
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # Flexible aspect behavior. If provided, overrides --translate.
|
||||||
|
# parser.add_argument(
|
||||||
|
# "--mode",
|
||||||
|
# choices=["pad", "fit", "fill", "fill_crop", "stretch"],
|
||||||
|
# help=(
|
||||||
|
# "Aspect behavior: "
|
||||||
|
# "'pad' (letter/pillarbox to exact size), "
|
||||||
|
# "'fit' (scale to fit within box, no pad), "
|
||||||
|
# "'fill'/'fill_crop' (cover box, center-crop to exact size), "
|
||||||
|
# "'stretch' (ignore AR, stretch to exact size). "
|
||||||
|
# "If omitted, falls back to --translate (no→pad, yes→stretch)."
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # Back-compat with existing calls
|
||||||
|
# parser.add_argument(
|
||||||
|
# "--translate", type=str, choices=["yes", "no"], default="no",
|
||||||
|
# help="(Deprecated in favor of --mode) 'yes' = stretch; 'no' = pad."
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # NEW: Force a true full-height Portrait 9:16 canvas (no borders).
|
||||||
|
# parser.add_argument(
|
||||||
|
# "--portrait-full", action="store_true",
|
||||||
|
# help=(
|
||||||
|
# "Force output to 9:16 portrait and fill the frame (center-crop). "
|
||||||
|
# "Uses the larger side of the preset as the output HEIGHT, and sets WIDTH = round(HEIGHT*9/16). "
|
||||||
|
# "Example: 1080p -> 1080x1920, 4k -> 2160x3840."
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
# # NOTE: input/output/force provided by top-level CLI.
|
||||||
|
|
||||||
|
# def _resolve_output_path(output: Path, fmt: str) -> Path:
|
||||||
|
# suffix = output.suffix.lower().lstrip(".")
|
||||||
|
# if suffix == fmt.lower():
|
||||||
|
# return output
|
||||||
|
# return output.with_suffix(f".{fmt}")
|
||||||
|
|
||||||
|
# def _even(x: int) -> int:
|
||||||
|
# return x if x % 2 == 0 else x - 1
|
||||||
|
|
||||||
|
# def _portrait_full_dims(w: int, h: int) -> tuple[int, int]:
|
||||||
|
# """
|
||||||
|
# Use the larger side of the preset as HEIGHT, set WIDTH for exact 9:16.
|
||||||
|
# Ensures even dimensions (some encoders require mod2).
|
||||||
|
# """
|
||||||
|
# H = max(w, h)
|
||||||
|
# W = int(round(H * 9 / 16))
|
||||||
|
# return _even(W), _even(H)
|
||||||
|
|
||||||
|
# def _vf_for_mode(mode: str, w: int, h: int) -> str:
|
||||||
|
# """
|
||||||
|
# - pad: keep AR, add borders to reach exact WxH
|
||||||
|
# - fit: keep AR, scale to fit within WxH (no padding)
|
||||||
|
# - fill_crop: keep AR, scale to cover WxH then crop center to exact WxH
|
||||||
|
# - stretch: ignore AR, force exact WxH
|
||||||
|
# """
|
||||||
|
# if mode == "stretch":
|
||||||
|
# return f"scale={w}:{h}:force_original_aspect_ratio=disable,setsar=1,setdar={w}/{h}"
|
||||||
|
|
||||||
|
# if mode == "fit":
|
||||||
|
# return f"scale={w}:{h}:force_original_aspect_ratio=decrease,setsar=1"
|
||||||
|
|
||||||
|
# if mode in ("fill", "fill_crop"):
|
||||||
|
# return (
|
||||||
|
# f"scale={w}:{h}:force_original_aspect_ratio=increase,"
|
||||||
|
# f"crop={w}:{h}:(in_w-out_w)/2:(in_h-out_h)/2,setsar=1,setdar={w}/{h}"
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # default = pad
|
||||||
|
# return (
|
||||||
|
# f"scale={w}:{h}:force_original_aspect_ratio=decrease,"
|
||||||
|
# f"pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,setsar=1,setdar={w}/{h}"
|
||||||
|
# )
|
||||||
|
|
||||||
|
# def run(args):
|
||||||
|
# output_path = Path(args.output)
|
||||||
|
# clean_output = _resolve_output_path(output_path, args.output_format)
|
||||||
|
|
||||||
|
# # Ensure destination folder exists
|
||||||
|
# clean_output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# if clean_output.exists() and not getattr(args, "force", False):
|
||||||
|
# print(f"❌ {clean_output} already exists. Use --force to overwrite.")
|
||||||
|
# sys.exit(1)
|
||||||
|
|
||||||
|
# if args.preset not in VIDEO_PRESETS:
|
||||||
|
# print(f"❌ Invalid preset: {args.preset}. Available presets: {', '.join(VIDEO_PRESETS.keys())}")
|
||||||
|
# sys.exit(1)
|
||||||
|
|
||||||
|
# target_width, target_height = VIDEO_PRESETS[args.preset]
|
||||||
|
|
||||||
|
# # If full portrait requested, override canvas to strict 9:16 using preset's larger side.
|
||||||
|
# if getattr(args, "portrait_full", False):
|
||||||
|
# target_width, target_height = _portrait_full_dims(target_width, target_height)
|
||||||
|
# # Force a fill/crop behavior to guarantee full-frame portrait (no borders).
|
||||||
|
# chosen_mode = "fill_crop"
|
||||||
|
# else:
|
||||||
|
# # Choose behavior: --mode overrides legacy --translate
|
||||||
|
# chosen_mode = args.mode if args.mode else ("stretch" if args.translate == "yes" else "pad")
|
||||||
|
|
||||||
|
# vf = _vf_for_mode(chosen_mode, target_width, target_height)
|
||||||
|
|
||||||
|
# command = [
|
||||||
|
# "ffmpeg",
|
||||||
|
# "-i", str(args.input),
|
||||||
|
|
||||||
|
# # Map robustly: succeed even if the source has no audio
|
||||||
|
# "-map", "0:v:0",
|
||||||
|
# "-map", "0:a:0?", # optional first audio track
|
||||||
|
|
||||||
|
# "-vf", vf,
|
||||||
|
# "-c:v", "libx264",
|
||||||
|
# "-b:v", "5000k",
|
||||||
|
# "-pix_fmt", "yuv420p",
|
||||||
|
# "-preset", "medium",
|
||||||
|
|
||||||
|
# "-c:a", "aac",
|
||||||
|
# "-b:a", "160k",
|
||||||
|
# "-ar", "48000",
|
||||||
|
# "-movflags", "+faststart",
|
||||||
|
|
||||||
|
# str(clean_output),
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# if getattr(args, "force", False):
|
||||||
|
# command = command[:1] + ["-y"] + command[1:]
|
||||||
|
|
||||||
|
# run_ffmpeg_with_progress(command, args.input, clean_output)
|
||||||
|
|
||||||
from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
|
from videobeaux.utils.ffmpeg_operations import run_ffmpeg_with_progress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Video dimension presets
|
# Video dimension presets (unchanged)
|
||||||
VIDEO_PRESETS = {
|
VIDEO_PRESETS = {
|
||||||
"sd": (320, 240), # Standard Definition (4:3)
|
"sd": (320, 240), "720hd": (640, 360), "1080hd": (960, 540), "widescreen": (320, 180),
|
||||||
"720hd": (640, 360), # 720p HD reduced (16:9)
|
"portrait1080": (1080, 1620), "480p": (640, 480), "576p": (720, 576), "720p": (1280, 720),
|
||||||
"1080hd": (960, 540), # 1080p HD reduced (16:9)
|
"1080p": (1920, 1080), "1440p": (2560, 1440), "4k": (3840, 2160), "8k": (7680, 4320),
|
||||||
"widescreen": (320, 180), # Widescreen low-res (16:9)
|
"vga": (640, 480), "qvga": (320, 240), "wvga": (800, 480), "svga": (800, 600),
|
||||||
"portrait1080": (1080, 1620), # Portrait 1080p (9:13.5)
|
"xga": (1024, 768), "wxga": (1280, 800), "sxga": (1280, 1024), "uxga": (1600, 1200),
|
||||||
"480p": (640, 480), # Standard Definition (4:3)
|
"wuxga": (1920, 1200), "qwxga": (2048, 1152), "qhd": (2560, 1440), "wqxga": (2560, 1600),
|
||||||
"576p": (720, 576), # PAL Standard Definition (4:3)
|
"5k": (5120, 2880), "portrait720": (720, 1280), "portrait4k": (2160, 3840),
|
||||||
"720p": (1280, 720), # HD (16:9)
|
"square1080": (1080, 1080), "square720": (720, 720), "cinema4k": (4096, 2160),
|
||||||
"1080p": (1920, 1080), # Full HD (16:9)
|
"ultrawide1080": (2560, 1080), "ultrawide1440": (3440, 1440),
|
||||||
"1440p": (2560, 1440), # QHD/2K (16:9)
|
"instagram_feed": (1080, 1080), "instagram_reels": (1080, 1920),
|
||||||
"4k": (3840, 2160), # 4K UHD (16:9)
|
"instagram_stories": (1080, 1920), "tiktok_video": (1080, 1920),
|
||||||
"8k": (7680, 4320), # 8K UHD (16:9)
|
"youtube_standard": (1920, 1080), "youtube_shorts": (1080, 1920),
|
||||||
"vga": (640, 480), # VGA (4:3)
|
"facebook_feed": (1080, 1080), "facebook_stories": (1080, 1920),
|
||||||
"qvga": (320, 240), # Quarter VGA (4:3)
|
"twitter_video": (1280, 720), "twitter_square": (1080, 1080),
|
||||||
"wvga": (800, 480), # Wide VGA (5:3)
|
"linkedin_video": (1920, 1080), "linkedin_square": (1080, 1080),
|
||||||
"svga": (800, 600), # Super VGA (4:3)
|
"snapchat_video": (1080, 1920), "pinterest_video": (1080, 1920),
|
||||||
"xga": (1024, 768), # Extended Graphics Array (4:3)
|
"pinterest_square": (1000, 1000)
|
||||||
"wxga": (1280, 800), # Wide XGA (16:10)
|
|
||||||
"sxga": (1280, 1024), # Super XGA (5:4)
|
|
||||||
"uxga": (1600, 1200), # Ultra XGA (4:3)
|
|
||||||
"wuxga": (1920, 1200), # Widescreen Ultra XGA (16:10)
|
|
||||||
"qwxga": (2048, 1152), # Quad Wide XGA (16:9)
|
|
||||||
"qhd": (2560, 1440), # Quad HD (16:9)
|
|
||||||
"wqxga": (2560, 1600), # Wide Quad XGA (16:10)
|
|
||||||
"5k": (5120, 2880), # 5K (16:9)
|
|
||||||
"portrait720": (720, 1280), # Portrait 720p (9:16)
|
|
||||||
"portrait4k": (2160, 3840), # Portrait 4K (9:16)
|
|
||||||
"square1080": (1080, 1080), # Square 1080p (1:1)
|
|
||||||
"square720": (720, 720), # Square 720p (1:1)
|
|
||||||
"cinema4k": (4096, 2160), # 4K DCI (Digital Cinema, ~17:9)
|
|
||||||
"ultrawide1080": (2560, 1080), # Ultrawide 1080p (21:9)
|
|
||||||
"ultrawide1440": (3440, 1440), # Ultrawide 1440p (21:9)
|
|
||||||
"instagram_feed": (1080, 1080), # Instagram square video (1:1)
|
|
||||||
"instagram_reels": (1080, 1920), # Instagram Reels/TikTok (9:16)
|
|
||||||
"instagram_stories": (1080, 1920), # Instagram Stories (9:16)
|
|
||||||
"tiktok_video": (1080, 1920), # TikTok standard video (9:16)
|
|
||||||
"youtube_standard": (1920, 1080), # YouTube standard video (16:9)
|
|
||||||
"youtube_shorts": (1080, 1920), # YouTube Shorts (9:16)
|
|
||||||
"facebook_feed": (1080, 1080), # Facebook in-feed video (1:1 recommended)
|
|
||||||
"facebook_stories": (1080, 1920), # Facebook Stories (9:16)
|
|
||||||
"twitter_video": (1280, 720), # Twitter/X video (16:9, recommended)
|
|
||||||
"twitter_square": (1080, 1080), # Twitter/X square video (1:1)
|
|
||||||
"linkedin_video": (1920, 1080), # LinkedIn video (16:9, recommended)
|
|
||||||
"linkedin_square": (1080, 1080), # LinkedIn square video (1:1)
|
|
||||||
"snapchat_video": (1080, 1920), # Snapchat video (9:16)
|
|
||||||
"pinterest_video": (1080, 1920), # Pinterest video (9:16)
|
|
||||||
"pinterest_square": (1000, 1000) # Pinterest square video (1:1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def register_arguments(parser):
|
def register_arguments(parser):
|
||||||
parser.description = ("Converts an input video to a specified preset dimension. "
|
parser.description = (
|
||||||
"Optionally stretch to fit dimensions or maintain aspect ratio with padding.")
|
"Converts an input video to a specified preset dimension. "
|
||||||
parser.add_argument(
|
"Choose how to handle aspect ratio: pad, fit, fill-crop, or stretch. "
|
||||||
"--output-format", required=True, type=str,
|
"Use --portrait-full to force a 9:16 full-frame vertical output."
|
||||||
help="Format to convert output into (e.g., mp4, mov). Output can be a filename without extension."
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument("--output-format", required=True, type=str,
|
||||||
"--preset", required=True, type=str, choices=VIDEO_PRESETS.keys(),
|
help="Format for output file extension (e.g., mp4, mov).")
|
||||||
help="Preset dimension (e.g., 1080p, instagram_reels)."
|
parser.add_argument("--preset", required=True, type=str, choices=VIDEO_PRESETS.keys(),
|
||||||
)
|
help="Preset dimension (e.g., 1080p, instagram_reels).")
|
||||||
parser.add_argument(
|
|
||||||
"--translate", type=str, choices=["yes", "no"], default="no",
|
# Flexible aspect behavior (overrides --translate when provided)
|
||||||
help="Stretch to fit dimensions exactly ('yes') or maintain aspect ratio with padding ('no')."
|
parser.add_argument("--mode",
|
||||||
)
|
choices=["pad", "fit", "fill", "fill_crop", "stretch"],
|
||||||
# NOTE: input/output/force provided by top-level CLI.
|
help="pad: keep AR + borders; fit: keep AR, no pad; "
|
||||||
|
"fill/fill_crop: cover + center-crop; stretch: ignore AR.")
|
||||||
|
|
||||||
|
# Back-compat
|
||||||
|
parser.add_argument("--translate", type=str, choices=["yes", "no"], default="no",
|
||||||
|
help="(Deprecated) yes=stretch, no=pad if --mode not set.")
|
||||||
|
|
||||||
|
# NEW: crop/pad alignment & cosmetics
|
||||||
|
parser.add_argument("--anchor",
|
||||||
|
choices=["center", "top", "bottom", "left", "right",
|
||||||
|
"top_left", "top_right", "bottom_left", "bottom_right"],
|
||||||
|
default="center",
|
||||||
|
help="Where to bias the crop or padding.")
|
||||||
|
parser.add_argument("--pad-color", default="#000000",
|
||||||
|
help="Padding color for --mode pad. Hex like #000000, #FFFFFF.")
|
||||||
|
parser.add_argument("--scale-flags",
|
||||||
|
choices=["lanczos", "bicubic", "bilinear", "neighbor"],
|
||||||
|
default="lanczos",
|
||||||
|
help="Scaler kernel for the scale filter.")
|
||||||
|
|
||||||
|
# 9:16 portrait override (no borders)
|
||||||
|
parser.add_argument("--portrait-full", action="store_true",
|
||||||
|
help="Force 9:16 portrait output and fill (cover + crop).")
|
||||||
|
|
||||||
def _resolve_output_path(output: Path, fmt: str) -> Path:
|
def _resolve_output_path(output: Path, fmt: str) -> Path:
|
||||||
|
sfx = output.suffix.lower().lstrip(".")
|
||||||
|
return output if sfx == fmt.lower() else output.with_suffix(f".{fmt}")
|
||||||
|
|
||||||
|
def _even(x: int) -> int:
|
||||||
|
return x if x % 2 == 0 else x - 1
|
||||||
|
|
||||||
|
def _portrait_full_dims(w: int, h: int) -> tuple[int, int]:
|
||||||
|
# Use the larger side of the preset as HEIGHT; WIDTH = round(HEIGHT*9/16)
|
||||||
|
H = max(w, h)
|
||||||
|
W = int(round(H * 9 / 16))
|
||||||
|
return _even(W), _even(H)
|
||||||
|
|
||||||
|
def _color_hex_to_ffmpeg(color: str) -> str:
|
||||||
|
c = color.strip()
|
||||||
|
if c.startswith("#"):
|
||||||
|
c = c[1:]
|
||||||
|
# Accept 3/6 hex; normalize to 6
|
||||||
|
if len(c) == 3:
|
||||||
|
c = "".join(ch*2 for ch in c)
|
||||||
|
return f"0x{c}"
|
||||||
|
|
||||||
|
def _anchor_offsets(anchor: str, axis_len: str) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
If user supplied an extension already and it matches fmt, keep it.
|
Return (x_expr, y_expr) for pad or crop offsets based on anchor.
|
||||||
If they supplied a different extension, replace it with fmt.
|
axis_len is 'pad' or 'crop' context indicating how to compute expressions.
|
||||||
If no extension, append fmt.
|
|
||||||
"""
|
"""
|
||||||
suffix = output.suffix.lower().lstrip(".")
|
# Center expressions
|
||||||
if suffix == fmt.lower():
|
cx = "(ow-iw)/2" if axis_len == "pad" else "(in_w-out_w)/2"
|
||||||
return output
|
cy = "(oh-ih)/2" if axis_len == "pad" else "(in_h-out_h)/2"
|
||||||
return output.with_suffix(f".{fmt}")
|
|
||||||
|
if anchor == "center": return cx, cy
|
||||||
|
if anchor == "top": return cx, ( "0" if axis_len == "pad" else "0")
|
||||||
|
if anchor == "bottom": return cx, ( "(oh-ih)" if axis_len == "pad" else "(in_h-out_h)" )
|
||||||
|
if anchor == "left": return ( "0" if axis_len == "pad" else "0"), cy
|
||||||
|
if anchor == "right": return ( "(ow-iw)" if axis_len == "pad" else "(in_w-out_w)"), cy
|
||||||
|
if anchor == "top_left": return ( "0" if axis_len == "pad" else "0"), ( "0" if axis_len == "pad" else "0")
|
||||||
|
if anchor == "top_right": return ( "(ow-iw)" if axis_len == "pad" else "(in_w-out_w)"), ( "0" if axis_len == "pad" else "0")
|
||||||
|
if anchor == "bottom_left": return ( "0" if axis_len == "pad" else "0"), ( "(oh-ih)" if axis_len == "pad" else "(in_h-out_h)")
|
||||||
|
if anchor == "bottom_right": return ( "(ow-iw)" if axis_len == "pad" else "(in_w-out_w)"), ( "(oh-ih)" if axis_len == "pad" else "(in_h-out_h)")
|
||||||
|
return cx, cy
|
||||||
|
|
||||||
|
def _vf_for_mode(mode: str, w: int, h: int, anchor: str, pad_color_hex: str, scale_flags: str) -> str:
|
||||||
|
color = _color_hex_to_ffmpeg(pad_color_hex)
|
||||||
|
|
||||||
|
if mode == "stretch":
|
||||||
|
return f"scale={w}:{h}:flags={scale_flags}:force_original_aspect_ratio=disable,setsar=1,setdar={w}/{h}"
|
||||||
|
|
||||||
|
if mode == "fit":
|
||||||
|
# Keep AR, fit into WxH; no padding
|
||||||
|
return f"scale={w}:{h}:flags={scale_flags}:force_original_aspect_ratio=decrease,setsar=1"
|
||||||
|
|
||||||
|
if mode in ("fill", "fill_crop"):
|
||||||
|
# Cover, then crop based on anchor
|
||||||
|
x, y = _anchor_offsets(anchor, "crop")
|
||||||
|
return (f"scale={w}:{h}:flags={scale_flags}:force_original_aspect_ratio=increase,"
|
||||||
|
f"crop={w}:{h}:{x}:{y},setsar=1,setdar={w}/{h}")
|
||||||
|
|
||||||
|
# default = pad
|
||||||
|
x, y = _anchor_offsets(anchor, "pad")
|
||||||
|
return (f"scale={w}:{h}:flags={scale_flags}:force_original_aspect_ratio=decrease,"
|
||||||
|
f"pad={w}:{h}:{x}:{y}:{color},setsar=1,setdar={w}/{h}")
|
||||||
|
|
||||||
def run(args):
|
def run(args):
|
||||||
output_path = Path(args.output)
|
output_path = Path(args.output)
|
||||||
clean_output = _resolve_output_path(output_path, args.output_format)
|
clean_output = _resolve_output_path(output_path, args.output_format)
|
||||||
|
|
||||||
# Ensure destination folder exists
|
|
||||||
clean_output.parent.mkdir(parents=True, exist_ok=True)
|
clean_output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if clean_output.exists() and not getattr(args, "force", False):
|
if clean_output.exists() and not getattr(args, "force", False):
|
||||||
@@ -98,18 +309,13 @@ def run(args):
|
|||||||
|
|
||||||
target_width, target_height = VIDEO_PRESETS[args.preset]
|
target_width, target_height = VIDEO_PRESETS[args.preset]
|
||||||
|
|
||||||
# Filter graph (keep your translate semantics)
|
if getattr(args, "portrait_full", False):
|
||||||
if args.translate == "yes":
|
target_width, target_height = _portrait_full_dims(target_width, target_height)
|
||||||
vf = (
|
chosen_mode = "fill_crop" # guarantee full-frame portrait
|
||||||
f"scale={target_width}:{target_height}:force_original_aspect_ratio=disable,"
|
|
||||||
f"setsar=1,setdar={target_width}/{target_height}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
vf = (
|
chosen_mode = args.mode if args.mode else ("stretch" if args.translate == "yes" else "pad")
|
||||||
f"scale={target_width}:{target_height}:force_original_aspect_ratio=decrease,"
|
|
||||||
f"pad={target_width}:{target_height}:(ow-iw)/2:(oh-ih)/2,"
|
vf = _vf_for_mode(chosen_mode, target_width, target_height, args.anchor, args.pad_color, args.scale_flags)
|
||||||
f"setsar=1,setdar={target_width}/{target_height}"
|
|
||||||
)
|
|
||||||
|
|
||||||
command = [
|
command = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
@@ -117,7 +323,7 @@ def run(args):
|
|||||||
|
|
||||||
# Map robustly: succeed even if the source has no audio
|
# Map robustly: succeed even if the source has no audio
|
||||||
"-map", "0:v:0",
|
"-map", "0:v:0",
|
||||||
"-map", "0:a?:0",
|
"-map", "0:a:0?", # optional first audio track
|
||||||
|
|
||||||
"-vf", vf,
|
"-vf", vf,
|
||||||
"-c:v", "libx264",
|
"-c:v", "libx264",
|
||||||
@@ -128,7 +334,6 @@ def run(args):
|
|||||||
"-c:a", "aac",
|
"-c:a", "aac",
|
||||||
"-b:a", "160k",
|
"-b:a", "160k",
|
||||||
"-ar", "48000",
|
"-ar", "48000",
|
||||||
|
|
||||||
"-movflags", "+faststart",
|
"-movflags", "+faststart",
|
||||||
|
|
||||||
str(clean_output),
|
str(clean_output),
|
||||||
@@ -137,4 +342,4 @@ def run(args):
|
|||||||
if getattr(args, "force", False):
|
if getattr(args, "force", False):
|
||||||
command = command[:1] + ["-y"] + command[1:]
|
command = command[:1] + ["-y"] + command[1:]
|
||||||
|
|
||||||
run_ffmpeg_with_progress(command, args.input, clean_output)
|
run_ffmpeg_with_progress(command, args.input, clean_output)
|
||||||
|
|||||||
Reference in New Issue
Block a user