293 lines
8.9 KiB
Python
293 lines
8.9 KiB
Python
"""
|
|
External tool runners for effects that can't be done in FFmpeg.
|
|
|
|
Supports:
|
|
- datamosh: via ffglitch or datamoshing Python CLI
|
|
- pixelsort: via Rust pixelsort or Python pixelsort-cli
|
|
"""
|
|
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
def find_tool(tool_names: List[str]) -> Optional[str]:
|
|
"""Find first available tool from a list of candidates."""
|
|
for name in tool_names:
|
|
path = shutil.which(name)
|
|
if path:
|
|
return path
|
|
return None
|
|
|
|
|
|
def check_python_package(package: str) -> bool:
|
|
"""Check if a Python package is installed."""
|
|
try:
|
|
result = subprocess.run(
|
|
["python3", "-c", f"import {package}"],
|
|
capture_output=True,
|
|
timeout=5,
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
# Tool detection
|
|
DATAMOSH_TOOLS = ["ffgac", "ffedit"] # ffglitch tools
|
|
PIXELSORT_TOOLS = ["pixelsort"] # Rust CLI
|
|
|
|
|
|
def get_available_tools() -> Dict[str, Optional[str]]:
|
|
"""Get dictionary of available external tools."""
|
|
return {
|
|
"datamosh": find_tool(DATAMOSH_TOOLS),
|
|
"pixelsort": find_tool(PIXELSORT_TOOLS),
|
|
"datamosh_python": "datamoshing" if check_python_package("datamoshing") else None,
|
|
"pixelsort_python": "pixelsort" if check_python_package("pixelsort") else None,
|
|
}
|
|
|
|
|
|
def run_datamosh(
|
|
input_path: Path,
|
|
output_path: Path,
|
|
params: Dict[str, Any],
|
|
) -> Tuple[bool, str]:
|
|
"""
|
|
Run datamosh effect using available tool.
|
|
|
|
Args:
|
|
input_path: Input video file
|
|
output_path: Output video file
|
|
params: Effect parameters (corruption, block_size, etc.)
|
|
|
|
Returns:
|
|
(success, error_message)
|
|
"""
|
|
tools = get_available_tools()
|
|
|
|
corruption = params.get("corruption", 0.3)
|
|
|
|
# Try ffglitch first
|
|
if tools.get("datamosh"):
|
|
ffgac = tools["datamosh"]
|
|
try:
|
|
# ffglitch approach: remove I-frames to create datamosh effect
|
|
# This is a simplified version - full datamosh needs custom scripts
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f:
|
|
# Write a simple ffglitch script that corrupts motion vectors
|
|
f.write(f"""
|
|
// Datamosh script - corrupt motion vectors
|
|
let corruption = {corruption};
|
|
|
|
export function glitch_frame(frame, stream) {{
|
|
if (frame.pict_type === 'P' && Math.random() < corruption) {{
|
|
// Corrupt motion vectors
|
|
let dominated = frame.mv?.forward?.overflow;
|
|
if (dominated) {{
|
|
for (let i = 0; i < dominated.length; i++) {{
|
|
if (Math.random() < corruption) {{
|
|
dominated[i] = [
|
|
Math.floor(Math.random() * 64 - 32),
|
|
Math.floor(Math.random() * 64 - 32)
|
|
];
|
|
}}
|
|
}}
|
|
}}
|
|
}}
|
|
return frame;
|
|
}}
|
|
""")
|
|
script_path = f.name
|
|
|
|
cmd = [
|
|
ffgac,
|
|
"-i", str(input_path),
|
|
"-s", script_path,
|
|
"-o", str(output_path),
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
Path(script_path).unlink(missing_ok=True)
|
|
|
|
if result.returncode == 0:
|
|
return True, ""
|
|
return False, result.stderr[:500]
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "Datamosh timeout"
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
# Fall back to Python datamoshing package
|
|
if tools.get("datamosh_python"):
|
|
try:
|
|
cmd = [
|
|
"python3", "-m", "datamoshing",
|
|
str(input_path),
|
|
str(output_path),
|
|
"--mode", "iframe_removal",
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
if result.returncode == 0:
|
|
return True, ""
|
|
return False, result.stderr[:500]
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
return False, "No datamosh tool available. Install ffglitch or: pip install datamoshing"
|
|
|
|
|
|
def run_pixelsort(
|
|
input_path: Path,
|
|
output_path: Path,
|
|
params: Dict[str, Any],
|
|
) -> Tuple[bool, str]:
|
|
"""
|
|
Run pixelsort effect using available tool.
|
|
|
|
Args:
|
|
input_path: Input image/frame file
|
|
output_path: Output image file
|
|
params: Effect parameters (sort_by, threshold_low, threshold_high, angle)
|
|
|
|
Returns:
|
|
(success, error_message)
|
|
"""
|
|
tools = get_available_tools()
|
|
|
|
sort_by = params.get("sort_by", "lightness")
|
|
threshold_low = params.get("threshold_low", 50)
|
|
threshold_high = params.get("threshold_high", 200)
|
|
angle = params.get("angle", 0)
|
|
|
|
# Try Rust pixelsort first (faster)
|
|
if tools.get("pixelsort"):
|
|
try:
|
|
cmd = [
|
|
tools["pixelsort"],
|
|
str(input_path),
|
|
"-o", str(output_path),
|
|
"--sort", sort_by,
|
|
"-r", str(angle),
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
if result.returncode == 0:
|
|
return True, ""
|
|
return False, result.stderr[:500]
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
# Fall back to Python pixelsort-cli
|
|
if tools.get("pixelsort_python"):
|
|
try:
|
|
cmd = [
|
|
"python3", "-m", "pixelsort",
|
|
"--image_path", str(input_path),
|
|
"--output", str(output_path),
|
|
"--angle", str(angle),
|
|
"--threshold", str(threshold_low / 255), # Normalize to 0-1
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
if result.returncode == 0:
|
|
return True, ""
|
|
return False, result.stderr[:500]
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
return False, "No pixelsort tool available. Install: cargo install pixelsort or pip install pixelsort-cli"
|
|
|
|
|
|
def run_pixelsort_video(
|
|
input_path: Path,
|
|
output_path: Path,
|
|
params: Dict[str, Any],
|
|
fps: float = 30,
|
|
) -> Tuple[bool, str]:
|
|
"""
|
|
Run pixelsort on a video by processing each frame.
|
|
|
|
This extracts frames, processes them, then reassembles.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
tmpdir = Path(tmpdir)
|
|
frames_in = tmpdir / "frame_%04d.png"
|
|
frames_out = tmpdir / "sorted_%04d.png"
|
|
|
|
# Extract frames
|
|
extract_cmd = [
|
|
"ffmpeg", "-y",
|
|
"-i", str(input_path),
|
|
"-vf", f"fps={fps}",
|
|
str(frames_in),
|
|
]
|
|
result = subprocess.run(extract_cmd, capture_output=True, timeout=300)
|
|
if result.returncode != 0:
|
|
return False, "Failed to extract frames"
|
|
|
|
# Process each frame
|
|
frame_files = sorted(tmpdir.glob("frame_*.png"))
|
|
for i, frame in enumerate(frame_files):
|
|
out_frame = tmpdir / f"sorted_{i:04d}.png"
|
|
success, error = run_pixelsort(frame, out_frame, params)
|
|
if not success:
|
|
return False, f"Frame {i}: {error}"
|
|
|
|
# Reassemble
|
|
# Get audio from original
|
|
reassemble_cmd = [
|
|
"ffmpeg", "-y",
|
|
"-framerate", str(fps),
|
|
"-i", str(tmpdir / "sorted_%04d.png"),
|
|
"-i", str(input_path),
|
|
"-map", "0:v", "-map", "1:a?",
|
|
"-c:v", "libx264", "-preset", "fast",
|
|
"-c:a", "copy",
|
|
str(output_path),
|
|
]
|
|
result = subprocess.run(reassemble_cmd, capture_output=True, timeout=300)
|
|
if result.returncode != 0:
|
|
return False, "Failed to reassemble video"
|
|
|
|
return True, ""
|
|
|
|
|
|
def run_external_effect(
|
|
effect_name: str,
|
|
input_path: Path,
|
|
output_path: Path,
|
|
params: Dict[str, Any],
|
|
is_video: bool = True,
|
|
) -> Tuple[bool, str]:
|
|
"""
|
|
Run an external effect tool.
|
|
|
|
Args:
|
|
effect_name: Name of effect (datamosh, pixelsort)
|
|
input_path: Input file
|
|
output_path: Output file
|
|
params: Effect parameters
|
|
is_video: Whether input is video (vs single image)
|
|
|
|
Returns:
|
|
(success, error_message)
|
|
"""
|
|
if effect_name == "datamosh":
|
|
return run_datamosh(input_path, output_path, params)
|
|
elif effect_name == "pixelsort":
|
|
if is_video:
|
|
return run_pixelsort_video(input_path, output_path, params)
|
|
else:
|
|
return run_pixelsort(input_path, output_path, params)
|
|
else:
|
|
return False, f"Unknown external effect: {effect_name}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Print available tools
|
|
print("Available external tools:")
|
|
for name, path in get_available_tools().items():
|
|
status = path if path else "NOT INSTALLED"
|
|
print(f" {name}: {status}")
|