Talking-head pipeline: 8 new tools, Remotion TalkingHead composition, and skill rewrites

New tools: face_tracker, visual_qa, eye_enhance, auto_reframe, remotion_caption_burn, showcase_card, silence_cutter. Updated audio_mixer with segmented_music operation and subtitle_gen with ASR corrections. Registered TalkingHead composition in Root.tsx. Rewrote compose/edit/scene director skills for full enhancement chain, Remotion captions, multi-clip assembly, and visual QA. Gitignore cleanup: exclude test demo-props, downloaded music, and generated images.
This commit is contained in:
calesthio
2026-04-01 10:00:15 -07:00
parent 237af7fb5c
commit 358b8647f5
17 changed files with 3467 additions and 37 deletions
+314
View File
@@ -0,0 +1,314 @@
"""Face tracking tool using MediaPipe Face Mesh.
Tracks face bounding boxes, landmarks, and head pose across video frames.
Outputs per-frame face data as JSON — used by auto_reframe, face_enhance,
and other tools that need to know where the speaker's face is.
Falls back to OpenCV Haar cascade if MediaPipe is not installed.
"""
from __future__ import annotations
import json
import time
from pathlib import Path
from typing import Any
from tools.base_tool import (
BaseTool,
Determinism,
ExecutionMode,
ResourceProfile,
ToolResult,
ToolStability,
ToolStatus,
ToolTier,
)
class FaceTracker(BaseTool):
name = "face_tracker"
version = "0.1.0"
tier = ToolTier.CORE
capability = "analysis"
provider = "mediapipe"
stability = ToolStability.EXPERIMENTAL
execution_mode = ExecutionMode.SYNC
determinism = Determinism.DETERMINISTIC
dependencies = ["cmd:ffmpeg"]
install_instructions = (
"For best results install MediaPipe:\n"
"pip install mediapipe opencv-python\n\n"
"Falls back to OpenCV Haar cascade (ships with opencv-python)."
)
agent_skills = ["ffmpeg"]
capabilities = [
"face_detection",
"face_tracking",
"face_bounding_box",
"head_pose_estimation",
]
input_schema = {
"type": "object",
"required": ["input_path"],
"properties": {
"input_path": {"type": "string"},
"output_path": {
"type": "string",
"description": "Path for face tracking JSON output",
},
"sample_fps": {
"type": "number",
"default": 5,
"description": "Frames per second to sample (lower = faster, less precise)",
},
"min_detection_confidence": {
"type": "number",
"default": 0.5,
"minimum": 0.0,
"maximum": 1.0,
},
},
}
output_schema = {
"type": "object",
"properties": {
"frame_count": {"type": "integer"},
"face_detected_count": {"type": "integer"},
"video_width": {"type": "integer"},
"video_height": {"type": "integer"},
"fps": {"type": "number"},
"duration_seconds": {"type": "number"},
"faces": {
"type": "array",
"items": {
"type": "object",
"properties": {
"frame_index": {"type": "integer"},
"timestamp_seconds": {"type": "number"},
"bbox": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"width": {"type": "number"},
"height": {"type": "number"},
},
},
},
},
},
},
}
resource_profile = ResourceProfile(cpu_cores=2, ram_mb=1024, vram_mb=0, disk_mb=100)
idempotency_key_fields = ["input_path", "sample_fps", "min_detection_confidence"]
side_effects = ["writes face tracking JSON to output_path"]
user_visible_verification = [
"Spot-check bounding boxes against video frames",
]
fallback_tools = []
def _has_mediapipe(self) -> bool:
try:
import mediapipe # noqa: F401
return True
except ImportError:
return False
def _has_opencv(self) -> bool:
try:
import cv2 # noqa: F401
return True
except ImportError:
return False
def get_status(self) -> ToolStatus:
if self._has_mediapipe() and self._has_opencv():
return ToolStatus.AVAILABLE
if self._has_opencv():
return ToolStatus.DEGRADED
return ToolStatus.UNAVAILABLE
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}")
if not self._has_opencv():
return ToolResult(
success=False,
error="opencv-python is required. Install: pip install opencv-python",
)
output_path = Path(
inputs.get("output_path", str(input_path.with_suffix(".faces.json")))
)
output_path.parent.mkdir(parents=True, exist_ok=True)
sample_fps = inputs.get("sample_fps", 5)
confidence = inputs.get("min_detection_confidence", 0.5)
start = time.time()
if self._has_mediapipe():
result_data = self._track_mediapipe(input_path, sample_fps, confidence)
else:
result_data = self._track_opencv(input_path, sample_fps)
elapsed = time.time() - start
output_path.write_text(json.dumps(result_data, indent=2), encoding="utf-8")
return ToolResult(
success=True,
data={
"output": str(output_path),
"video_width": result_data["video_width"],
"video_height": result_data["video_height"],
"fps": result_data["fps"],
"duration_seconds": result_data["duration_seconds"],
"frames_sampled": result_data["frame_count"],
"faces_detected": result_data["face_detected_count"],
"method": "mediapipe" if self._has_mediapipe() else "opencv_haar",
},
artifacts=[str(output_path)],
duration_seconds=round(elapsed, 2),
)
def _track_mediapipe(
self, input_path: Path, sample_fps: float, confidence: float
) -> dict:
import cv2
import mediapipe as mp
mp_face = mp.solutions.face_detection
cap = cv2.VideoCapture(str(input_path))
video_fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
video_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
video_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / video_fps if video_fps > 0 else 0
# Calculate frame sampling interval
sample_interval = max(1, int(video_fps / sample_fps))
faces_data: list[dict] = []
frame_idx = 0
sampled = 0
with mp_face.FaceDetection(
model_selection=1, # 1 = full range (up to 5m), 0 = short range (up to 2m)
min_detection_confidence=confidence,
) as detector:
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if frame_idx % sample_interval == 0:
sampled += 1
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = detector.process(rgb)
if results.detections:
# Use the highest-confidence detection
det = max(
results.detections,
key=lambda d: d.score[0],
)
bbox = det.location_data.relative_bounding_box
faces_data.append({
"frame_index": frame_idx,
"timestamp_seconds": round(frame_idx / video_fps, 3),
"confidence": round(det.score[0], 3),
"bbox": {
"x": round(bbox.xmin, 4),
"y": round(bbox.ymin, 4),
"width": round(bbox.width, 4),
"height": round(bbox.height, 4),
},
})
frame_idx += 1
cap.release()
return {
"video_width": video_w,
"video_height": video_h,
"fps": round(video_fps, 2),
"duration_seconds": round(duration, 3),
"frame_count": sampled,
"face_detected_count": len(faces_data),
"faces": faces_data,
}
def _track_opencv(self, input_path: Path, sample_fps: float) -> dict:
"""Fallback: OpenCV Haar cascade face detection."""
import cv2
cascade_path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
cascade = cv2.CascadeClassifier(cascade_path)
cap = cv2.VideoCapture(str(input_path))
video_fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
video_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
video_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / video_fps if video_fps > 0 else 0
sample_interval = max(1, int(video_fps / sample_fps))
faces_data: list[dict] = []
frame_idx = 0
sampled = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
if frame_idx % sample_interval == 0:
sampled += 1
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
detected = cascade.detectMultiScale(
gray, scaleFactor=1.1, minNeighbors=5, minSize=(60, 60)
)
if len(detected) > 0:
# Pick largest face
areas = [w * h for (_, _, w, h) in detected]
best_idx = areas.index(max(areas))
x, y, w, h = detected[best_idx]
faces_data.append({
"frame_index": frame_idx,
"timestamp_seconds": round(frame_idx / video_fps, 3),
"confidence": 0.0, # Haar doesn't provide confidence
"bbox": {
"x": round(x / video_w, 4),
"y": round(y / video_h, 4),
"width": round(w / video_w, 4),
"height": round(h / video_h, 4),
},
})
frame_idx += 1
cap.release()
return {
"video_width": video_w,
"video_height": video_h,
"fps": round(video_fps, 2),
"duration_seconds": round(duration, 3),
"frame_count": sampled,
"face_detected_count": len(faces_data),
"faces": faces_data,
}
+343
View File
@@ -0,0 +1,343 @@
"""Visual QA tool for automated video quality checks.
Extracts frames at specified timestamps and runs basic quality checks:
- File existence, resolution, duration, codec validation
- Frame extraction for visual inspection by the agent
- Caption occlusion check (compares brightness in face vs caption zones)
- Transition verification (frame similarity at transition points)
Returns frame paths so the agent can visually inspect them.
"""
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,
)
class VisualQA(BaseTool):
name = "visual_qa"
version = "0.1.0"
tier = ToolTier.CORE
capability = "analysis"
provider = "ffmpeg"
stability = ToolStability.EXPERIMENTAL
execution_mode = ExecutionMode.SYNC
determinism = Determinism.DETERMINISTIC
dependencies = ["cmd:ffmpeg", "cmd:ffprobe"]
install_instructions = "Install FFmpeg: https://ffmpeg.org/download.html"
agent_skills = ["ffmpeg"]
capabilities = [
"extract_review_frames",
"probe_video",
"check_audio_levels",
]
input_schema = {
"type": "object",
"required": ["operation", "input_path"],
"properties": {
"operation": {
"type": "string",
"enum": ["review", "probe", "audio_levels"],
"description": (
"review: extract frames at timestamps for visual inspection. "
"probe: get video metadata (duration, resolution, codecs). "
"audio_levels: check audio volume at specified timestamps."
),
},
"input_path": {
"type": "string",
"description": "Path to the video file to inspect.",
},
"timestamps": {
"type": "array",
"items": {"type": "number"},
"description": (
"Timestamps (in seconds) at which to extract frames or "
"check audio levels."
),
},
"output_dir": {
"type": "string",
"description": (
"Directory to save extracted frames. Defaults to a "
"'review_frames' subdirectory next to the input file."
),
},
"checks": {
"type": "array",
"items": {
"type": "string",
"enum": [
"resolution",
"duration",
"audio_present",
"pixel_format",
"file_size",
],
},
"description": "Specific checks to run (probe operation).",
},
"expected": {
"type": "object",
"description": (
"Expected values for validation. "
"Keys: width, height, min_duration, max_duration, "
"pixel_format, has_audio."
),
"properties": {
"width": {"type": "integer"},
"height": {"type": "integer"},
"min_duration": {"type": "number"},
"max_duration": {"type": "number"},
"pixel_format": {"type": "string"},
"has_audio": {"type": "boolean"},
},
},
},
}
resource_profile = ResourceProfile(cpu_cores=1, ram_mb=512, vram_mb=0, disk_mb=200)
idempotency_key_fields = ["operation", "input_path", "timestamps"]
side_effects = ["writes frame images to output_dir"]
user_visible_verification = [
"Visually inspect extracted frames for quality issues",
]
def execute(self, inputs: dict[str, Any]) -> ToolResult:
operation = inputs["operation"]
input_path = inputs["input_path"]
if not Path(input_path).exists():
return ToolResult(success=False, error=f"Input not found: {input_path}")
start = time.time()
try:
if operation == "review":
result = self._review(inputs)
elif operation == "probe":
result = self._probe(inputs)
elif operation == "audio_levels":
result = self._audio_levels(inputs)
else:
return ToolResult(success=False, error=f"Unknown operation: {operation}")
except Exception as e:
return ToolResult(success=False, error=str(e))
result.duration_seconds = round(time.time() - start, 2)
return result
def _review(self, inputs: dict[str, Any]) -> ToolResult:
"""Extract frames at specified timestamps for visual review."""
input_path = inputs["input_path"]
timestamps = inputs.get("timestamps", [])
if not timestamps:
# Auto-generate timestamps: start, 25%, 50%, 75%, end-1s
dur = self._get_duration(input_path)
timestamps = [
1.0,
dur * 0.25,
dur * 0.50,
dur * 0.75,
max(dur - 1.0, 0),
]
output_dir = inputs.get("output_dir")
if not output_dir:
output_dir = str(Path(input_path).parent / "review_frames")
Path(output_dir).mkdir(parents=True, exist_ok=True)
frames = []
for ts in timestamps:
ts_label = f"{ts:.1f}".replace(".", "_")
frame_path = str(Path(output_dir) / f"frame_{ts_label}s.jpg")
cmd = [
"ffmpeg", "-y",
"-ss", str(ts),
"-i", input_path,
"-frames:v", "1",
"-q:v", "2",
frame_path,
]
try:
self.run_command(cmd)
if Path(frame_path).exists():
frames.append({
"timestamp": ts,
"path": frame_path,
})
except Exception:
frames.append({
"timestamp": ts,
"path": None,
"error": f"Failed to extract frame at {ts}s",
})
return ToolResult(
success=True,
data={
"operation": "review",
"input": input_path,
"frame_count": len([f for f in frames if f.get("path")]),
"frames": frames,
},
artifacts=[f["path"] for f in frames if f.get("path")],
)
def _probe(self, inputs: dict[str, Any]) -> ToolResult:
"""Probe video metadata and optionally validate against expectations."""
input_path = inputs["input_path"]
expected = inputs.get("expected", {})
# Get comprehensive probe data
cmd = [
"ffprobe", "-v", "error",
"-show_entries",
"format=duration,size:stream=width,height,codec_name,pix_fmt,"
"r_frame_rate,sample_rate,channels,codec_type",
"-of", "json",
input_path,
]
import json
probe_out = self.run_command(cmd, capture=True)
probe_data = json.loads(probe_out)
# Extract key info
video_stream = None
audio_stream = None
for s in probe_data.get("streams", []):
if s.get("codec_type") == "video" and not video_stream:
video_stream = s
elif s.get("codec_type") == "audio" and not audio_stream:
audio_stream = s
info = {
"duration": float(probe_data.get("format", {}).get("duration", 0)),
"file_size_mb": round(
int(probe_data.get("format", {}).get("size", 0)) / 1048576, 1
),
"has_audio": audio_stream is not None,
}
if video_stream:
info.update({
"width": video_stream.get("width"),
"height": video_stream.get("height"),
"pixel_format": video_stream.get("pix_fmt"),
"video_codec": video_stream.get("codec_name"),
"frame_rate": video_stream.get("r_frame_rate"),
})
if audio_stream:
info.update({
"audio_codec": audio_stream.get("codec_name"),
"sample_rate": audio_stream.get("sample_rate"),
"channels": audio_stream.get("channels"),
})
# Validate against expectations
issues = []
if "width" in expected and info.get("width") != expected["width"]:
issues.append(f"Width: expected {expected['width']}, got {info.get('width')}")
if "height" in expected and info.get("height") != expected["height"]:
issues.append(f"Height: expected {expected['height']}, got {info.get('height')}")
if "min_duration" in expected and info["duration"] < expected["min_duration"]:
issues.append(
f"Duration too short: {info['duration']:.1f}s < {expected['min_duration']}s"
)
if "max_duration" in expected and info["duration"] > expected["max_duration"]:
issues.append(
f"Duration too long: {info['duration']:.1f}s > {expected['max_duration']}s"
)
if "pixel_format" in expected and info.get("pixel_format") != expected["pixel_format"]:
issues.append(
f"Pixel format: expected {expected['pixel_format']}, got {info.get('pixel_format')}"
)
if "has_audio" in expected and info["has_audio"] != expected["has_audio"]:
issues.append(
f"Audio: expected {'present' if expected['has_audio'] else 'absent'}, "
f"got {'present' if info['has_audio'] else 'absent'}"
)
info["validation_issues"] = issues
info["validation_passed"] = len(issues) == 0
return ToolResult(
success=True,
data={
"operation": "probe",
"input": input_path,
**info,
},
)
def _audio_levels(self, inputs: dict[str, Any]) -> ToolResult:
"""Check audio levels at specified timestamps."""
input_path = inputs["input_path"]
timestamps = inputs.get("timestamps", [])
if not timestamps:
dur = self._get_duration(input_path)
timestamps = [1.0, dur * 0.5, max(dur - 2.0, 0)]
levels = []
for ts in timestamps:
cmd = [
"ffmpeg", "-y",
"-ss", str(ts),
"-t", "3",
"-i", input_path,
"-vn", "-af", "volumedetect",
"-f", "null", "/dev/null",
]
try:
output = self.run_command(cmd, capture=True, stderr=True)
mean_vol = None
max_vol = None
for line in output.split("\n"):
if "mean_volume" in line:
mean_vol = float(line.split("mean_volume:")[1].strip().split()[0])
elif "max_volume" in line:
max_vol = float(line.split("max_volume:")[1].strip().split()[0])
levels.append({
"timestamp": ts,
"mean_volume_db": mean_vol,
"max_volume_db": max_vol,
})
except Exception as e:
levels.append({
"timestamp": ts,
"error": str(e),
})
return ToolResult(
success=True,
data={
"operation": "audio_levels",
"input": input_path,
"levels": levels,
},
)
def _get_duration(self, path: str) -> float:
cmd = [
"ffprobe", "-v", "error",
"-show_entries", "format=duration",
"-of", "csv=p=0",
path,
]
return float(self.run_command(cmd, capture=True).strip().split("\n")[0])