mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-24 15:38:10 +02:00
fix: optimize video stabilization with adaptive zoom and single-pass encoding
This commit is contained in:
1 parent
d7377914e7
commit
15397974e0
1 file changed
+54
-89
+54
-89
@@ -433,94 +433,48 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
stats["tts_duration"] = time.time() - start_step
|
stats["tts_duration"] = time.time() - start_step
|
||||||
start_step = time.time()
|
start_step = time.time()
|
||||||
|
|
||||||
# 3.5 Stabilize video if requested (vidstab two-pass)
|
# 4. Build FFmpeg Command with Unified filter_complex
|
||||||
|
cmd = ["ffmpeg", "-y", "-i", str(input_video_path)]
|
||||||
|
|
||||||
|
# --- Stability Pass 1 (if requested) ---
|
||||||
|
vidstab_filter = ""
|
||||||
if request.stabilize:
|
if request.stabilize:
|
||||||
print("📐 Starting video stabilization (vidstab)...")
|
print("📐 Starting video stabilization (Pass 1: Detection)...")
|
||||||
transforms_path = job_dir / "transforms.trf"
|
transforms_path = job_dir / "transforms.trf"
|
||||||
stabilized_path = job_dir / "stabilized.mp4"
|
|
||||||
|
|
||||||
try:
|
# Run detection pass
|
||||||
# Pass 1: Detect motion/shakiness
|
detect_cmd = [
|
||||||
print("📐 Pass 1: Detecting motion...")
|
"ffmpeg",
|
||||||
detect_cmd = [
|
"-y",
|
||||||
"ffmpeg",
|
"-i",
|
||||||
"-y",
|
str(input_video_path),
|
||||||
"-i",
|
"-vf",
|
||||||
str(input_video_path),
|
f"vidstabdetect=stepsize=6:shakiness=8:accuracy=9:result={transforms_path}",
|
||||||
"-vf",
|
"-f",
|
||||||
f"vidstabdetect=stepsize=6:shakiness=8:accuracy=9:result={transforms_path}",
|
"null",
|
||||||
"-f",
|
"-",
|
||||||
"null",
|
]
|
||||||
"-",
|
|
||||||
]
|
detect_proc = subprocess.run(
|
||||||
detect_proc = subprocess.run(
|
detect_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
detect_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
)
|
||||||
|
|
||||||
|
if detect_proc.returncode == 0 and transforms_path.exists():
|
||||||
|
print(
|
||||||
|
"✅ Stabilization Pass 1 complete. Integrating Pass 2 into main filter chain."
|
||||||
|
)
|
||||||
|
# We will add vidstabtransform to the video chain below
|
||||||
|
# optzoom=2 -> Adaptive zoom to avoid black borders
|
||||||
|
# zoom=0 -> Auto zoom
|
||||||
|
vidstab_filter = f"vidstabtransform=input={transforms_path}:smoothing=10:optzoom=2:zoom=0:interpol=bicubic,unsharp=5:5:0.8:3:3:0.4,"
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"⚠️ Stabilization Pass 1 failed: {detect_proc.stderr.decode()[:500]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if detect_proc.returncode != 0:
|
|
||||||
print(
|
|
||||||
f"⚠️ Stabilization pass 1 failed: {detect_proc.stderr.decode()[:500]}"
|
|
||||||
)
|
|
||||||
elif transforms_path.exists():
|
|
||||||
# Pass 2: Apply stabilization transform
|
|
||||||
print("📐 Pass 2: Applying stabilization...")
|
|
||||||
transform_cmd = [
|
|
||||||
"ffmpeg",
|
|
||||||
"-y",
|
|
||||||
"-i",
|
|
||||||
str(input_video_path),
|
|
||||||
"-vf",
|
|
||||||
f"vidstabtransform=input={transforms_path}:smoothing=10:crop=black:zoom=1,unsharp=5:5:0.8:3:3:0.4",
|
|
||||||
"-c:v",
|
|
||||||
"libx264",
|
|
||||||
"-profile:v",
|
|
||||||
"high",
|
|
||||||
"-r",
|
|
||||||
"30",
|
|
||||||
"-preset",
|
|
||||||
"medium",
|
|
||||||
"-crf",
|
|
||||||
"17",
|
|
||||||
"-b:v",
|
|
||||||
"10M",
|
|
||||||
"-maxrate",
|
|
||||||
"12M",
|
|
||||||
"-bufsize",
|
|
||||||
"20M",
|
|
||||||
"-c:a",
|
|
||||||
"copy",
|
|
||||||
str(stabilized_path),
|
|
||||||
]
|
|
||||||
transform_proc = subprocess.run(
|
|
||||||
transform_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
||||||
)
|
|
||||||
|
|
||||||
if transform_proc.returncode == 0 and stabilized_path.exists():
|
|
||||||
# Use stabilized video for further processing
|
|
||||||
input_video_path = stabilized_path
|
|
||||||
print("✅ Video stabilization complete!")
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"⚠️ Stabilization pass 2 failed: {transform_proc.stderr.decode()[:500]}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print("⚠️ Transforms file not created, skipping stabilization")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Stabilization error (continuing without): {e}")
|
|
||||||
|
|
||||||
stats["stabilize_duration"] = time.time() - start_step
|
stats["stabilize_duration"] = time.time() - start_step
|
||||||
start_step = time.time()
|
start_step = time.time()
|
||||||
|
|
||||||
# 4. Build FFmpeg Command
|
|
||||||
cmd = ["ffmpeg", "-y", "-i", str(input_video_path)]
|
|
||||||
|
|
||||||
video_filters = []
|
|
||||||
audio_filters = []
|
|
||||||
|
|
||||||
# 4. Build FFmpeg Command with Unified filter_complex
|
|
||||||
cmd = ["ffmpeg", "-y", "-i", str(input_video_path)]
|
|
||||||
|
|
||||||
# --- Audio Checks ---
|
# --- Audio Checks ---
|
||||||
has_original_audio = False
|
has_original_audio = False
|
||||||
try:
|
try:
|
||||||
@@ -565,10 +519,20 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
fc_parts = []
|
fc_parts = []
|
||||||
|
|
||||||
# A. Video Chain
|
# A. Video Chain
|
||||||
# 1. Scale & Crop to Fill 1080x1920 (Vertical Reel)
|
# Chain: [0:v] -> [stabilize] -> [scale/crop] -> [text] -> [vout]
|
||||||
# [0:v] -> [v_processed]
|
|
||||||
# Using 'increase' + 'crop' to ensure full screen coverage without black bars
|
# 1. Stabilization (if enabled) + Scaling/Cropping
|
||||||
video_filters_str = "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,unsharp=5:5:0.8:3:3:0.4"
|
# We apply stabilization FIRST on raw video, THEN crop to 9:16
|
||||||
|
|
||||||
|
# Start of video chain
|
||||||
|
v_chain = "[0:v]"
|
||||||
|
|
||||||
|
if vidstab_filter:
|
||||||
|
v_chain += vidstab_filter
|
||||||
|
# Note: vidstabtransform output is same res as input
|
||||||
|
|
||||||
|
# Scale & Crop to Fill 1080x1920 (Vertical Reel)
|
||||||
|
v_chain += "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920"
|
||||||
|
|
||||||
# 2. Text Overlay
|
# 2. Text Overlay
|
||||||
if request.text and request.draw_text:
|
if request.text and request.draw_text:
|
||||||
@@ -577,7 +541,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
# Subtitles (TikTok style) using ASS (already generated in TTS block)
|
# Subtitles (TikTok style) using ASS (already generated in TTS block)
|
||||||
print(f"🎬 Overlaying subtitles from TTS ASS: {tts_ass_path}")
|
print(f"🎬 Overlaying subtitles from TTS ASS: {tts_ass_path}")
|
||||||
ass_path_str = str(tts_ass_path).replace("\\", "/").replace(":", "\\:")
|
ass_path_str = str(tts_ass_path).replace("\\", "/").replace(":", "\\:")
|
||||||
text_filter = f"subtitles='{ass_path_str}'"
|
text_filter = f",subtitles='{ass_path_str}'"
|
||||||
else:
|
else:
|
||||||
# Standard Text (without TTS)
|
# Standard Text (without TTS)
|
||||||
print(
|
print(
|
||||||
@@ -588,13 +552,14 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
generate_simple_ass(request.text, std_ass_path, font_size=40)
|
generate_simple_ass(request.text, std_ass_path, font_size=40)
|
||||||
|
|
||||||
ass_path_str = str(std_ass_path).replace("\\", "/").replace(":", "\\:")
|
ass_path_str = str(std_ass_path).replace("\\", "/").replace(":", "\\:")
|
||||||
text_filter = f"subtitles='{ass_path_str}'"
|
text_filter = f",subtitles='{ass_path_str}'"
|
||||||
|
|
||||||
# Combine formatting + text
|
# Combine formatting + text
|
||||||
video_filters_str += f",{text_filter}"
|
v_chain += text_filter
|
||||||
|
|
||||||
# Define Video Chain
|
# End of video chain
|
||||||
fc_parts.append(f"[0:v]{video_filters_str}[vout]")
|
v_chain += "[vout]"
|
||||||
|
fc_parts.append(v_chain)
|
||||||
|
|
||||||
# B. Audio Chain
|
# B. Audio Chain
|
||||||
audio_mapped = False
|
audio_mapped = False
|
||||||
|
|||||||
Reference in new issue
Block a user