mirror of
https://github.com/hanzoai/video.git
synced 2026-10-30 18:18:27 +00:00
11 production pipelines, 47 tools, 124 agent skills. Supports cloud APIs (fal.ai, OpenAI, ElevenLabs, Suno, HeyGen, Runway) and free local providers (diffusers, Piper TTS, WAN 2.1, Hunyuan, CogVideo). Co-authored-by: Hanzo Dev <[email protected]>
194 lines
6.0 KiB
Python
194 lines
6.0 KiB
Python
"""Face enhancement tool wrapping FFmpeg filters.
|
|
|
|
Applies skin smoothing, sharpening, and lighting correction presets
|
|
to talking-head footage. All presets are FFmpeg filter chains — no GPU
|
|
or external models required.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from tools.base_tool import (
|
|
BaseTool,
|
|
Determinism,
|
|
ExecutionMode,
|
|
ResourceProfile,
|
|
ToolResult,
|
|
ToolStability,
|
|
ToolTier,
|
|
)
|
|
|
|
|
|
# Named presets mapping to FFmpeg filter chains
|
|
PRESETS = {
|
|
"soft_skin": {
|
|
"description": "Gentle skin smoothing while preserving edges",
|
|
"vf": "smartblur=lr=1.0:ls=-0.5:lt=-3.0:cr=0.5:cs=-0.5:ct=-3.0",
|
|
},
|
|
"sharpen": {
|
|
"description": "Edge sharpening for crisp detail",
|
|
"vf": "unsharp=5:5:1.0:5:5:0.0",
|
|
},
|
|
"sharpen_light": {
|
|
"description": "Subtle sharpening for soft cameras",
|
|
"vf": "unsharp=3:3:0.5:3:3:0.0",
|
|
},
|
|
"brighten": {
|
|
"description": "Lift shadows and midtones for poorly lit footage",
|
|
"vf": "curves=all='0/0 0.25/0.35 0.5/0.55 0.75/0.8 1/1'",
|
|
},
|
|
"contrast_boost": {
|
|
"description": "Add punch with an S-curve contrast adjustment",
|
|
"vf": "curves=all='0/0 0.25/0.20 0.5/0.5 0.75/0.80 1/1'",
|
|
},
|
|
"warm": {
|
|
"description": "Warm skin tones — slight orange shift",
|
|
"vf": "colorbalance=rs=0.05:gs=0.0:bs=-0.05:rm=0.05:gm=0.0:bm=-0.03",
|
|
},
|
|
"cool": {
|
|
"description": "Cool tones — slight blue shift",
|
|
"vf": "colorbalance=rs=-0.03:gs=0.0:bs=0.05:rm=-0.02:gm=0.0:bm=0.03",
|
|
},
|
|
"denoise": {
|
|
"description": "Temporal noise reduction for grainy footage",
|
|
"vf": "hqdn3d=4:3:6:4",
|
|
},
|
|
"talking_head_standard": {
|
|
"description": "Combined preset: skin smoothing + sharpen edges + warm skin tones",
|
|
"vf": (
|
|
"smartblur=lr=1.0:ls=-0.5:lt=-3.0:cr=0.5:cs=-0.5:ct=-3.0,"
|
|
"unsharp=5:5:0.6:5:5:0.0,"
|
|
"colorbalance=rs=0.06:gs=0.01:bs=-0.04:rm=0.04:gm=0.01:bm=-0.03"
|
|
),
|
|
},
|
|
}
|
|
|
|
|
|
class FaceEnhance(BaseTool):
|
|
name = "face_enhance"
|
|
version = "0.1.0"
|
|
tier = ToolTier.CORE
|
|
capability = "enhancement"
|
|
provider = "ffmpeg"
|
|
stability = ToolStability.EXPERIMENTAL
|
|
execution_mode = ExecutionMode.SYNC
|
|
determinism = Determinism.DETERMINISTIC
|
|
|
|
dependencies = ["cmd:ffmpeg"]
|
|
install_instructions = "Install FFmpeg: https://ffmpeg.org/download.html"
|
|
agent_skills = ["ffmpeg"]
|
|
|
|
capabilities = [
|
|
"skin_smoothing",
|
|
"sharpening",
|
|
"lighting_correction",
|
|
"color_balance",
|
|
"denoise",
|
|
"preset_chain",
|
|
]
|
|
|
|
input_schema = {
|
|
"type": "object",
|
|
"required": ["input_path"],
|
|
"properties": {
|
|
"input_path": {"type": "string"},
|
|
"output_path": {"type": "string"},
|
|
"preset": {
|
|
"type": "string",
|
|
"enum": list(PRESETS.keys()),
|
|
"default": "talking_head_standard",
|
|
},
|
|
"presets": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Apply multiple presets in sequence",
|
|
},
|
|
"custom_vf": {
|
|
"type": "string",
|
|
"description": "Custom FFmpeg video filter string (advanced)",
|
|
},
|
|
"codec": {"type": "string", "default": "libx264"},
|
|
"crf": {"type": "integer", "default": 20},
|
|
},
|
|
}
|
|
|
|
resource_profile = ResourceProfile(cpu_cores=2, ram_mb=1024, vram_mb=0, disk_mb=2000)
|
|
idempotency_key_fields = ["input_path", "preset", "presets", "custom_vf"]
|
|
side_effects = ["writes enhanced video to output_path"]
|
|
user_visible_verification = [
|
|
"Compare enhanced output with original side-by-side",
|
|
"Verify skin texture is natural, not plastic",
|
|
]
|
|
|
|
def execute(self, inputs: dict[str, Any]) -> ToolResult:
|
|
input_path = Path(inputs["input_path"])
|
|
if not input_path.exists():
|
|
return ToolResult(success=False, error=f"Input not found: {input_path}")
|
|
|
|
output_path = Path(
|
|
inputs.get("output_path", str(input_path.with_stem(f"{input_path.stem}_enhanced")))
|
|
)
|
|
codec = inputs.get("codec", "libx264")
|
|
crf = inputs.get("crf", 20)
|
|
|
|
# Build filter chain
|
|
vf = self._build_filter(inputs)
|
|
if not vf:
|
|
return ToolResult(success=False, error="No preset, presets, or custom_vf specified")
|
|
|
|
start = time.time()
|
|
|
|
cmd = [
|
|
"ffmpeg", "-y",
|
|
"-i", str(input_path),
|
|
"-vf", vf,
|
|
"-c:v", codec, "-crf", str(crf),
|
|
"-c:a", "copy",
|
|
str(output_path),
|
|
]
|
|
|
|
try:
|
|
self.run_command(cmd)
|
|
except Exception as e:
|
|
return ToolResult(success=False, error=f"FFmpeg failed: {e}")
|
|
|
|
elapsed = time.time() - start
|
|
|
|
return ToolResult(
|
|
success=True,
|
|
data={
|
|
"input": str(input_path),
|
|
"output": str(output_path),
|
|
"filter": vf,
|
|
"preset": inputs.get("preset"),
|
|
},
|
|
artifacts=[str(output_path)],
|
|
duration_seconds=round(elapsed, 2),
|
|
)
|
|
|
|
def _build_filter(self, inputs: dict[str, Any]) -> str:
|
|
if "custom_vf" in inputs:
|
|
return inputs["custom_vf"]
|
|
|
|
if "presets" in inputs:
|
|
chains = []
|
|
for name in inputs["presets"]:
|
|
if name not in PRESETS:
|
|
continue
|
|
chains.append(PRESETS[name]["vf"])
|
|
return ",".join(chains)
|
|
|
|
preset_name = inputs.get("preset", "talking_head_standard")
|
|
preset = PRESETS.get(preset_name)
|
|
if preset:
|
|
return preset["vf"]
|
|
return ""
|
|
|
|
@staticmethod
|
|
def list_presets() -> dict[str, str]:
|
|
"""Return available presets and their descriptions."""
|
|
return {name: p["description"] for name, p in PRESETS.items()}
|