Files
rose-ash/artdag/sexp/external_tools.py
giles cc2dcbddd4 Squashed 'core/' content from commit 4957443
git-subtree-dir: core
git-subtree-split: 4957443184ae0eb6323635a90a19acffb3e01d07
2026-02-24 23:09:39 +00:00

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}")