Add streaming video compositor with sexp interpreter
- New streaming/ module for real-time video processing: - compositor.py: Main streaming compositor with cycle-crossfade - sexp_executor.py: Executes compiled sexp recipes in real-time - sexp_interp.py: Full S-expression interpreter for SLICE_ON Lambda - recipe_adapter.py: Bridges recipes to streaming compositor - sources.py: Video source with ffmpeg streaming - audio.py: Real-time audio analysis (energy, beats) - output.py: Preview (mpv) and file output with audio muxing - New templates/: - cycle-crossfade.sexp: Smooth zoom-based video cycling - process-pair.sexp: Dual-clip processing with effects - Key features: - Videos cycle in input-videos order (not definition order) - Cumulative whole-spin rotation - Zero-weight sources skip processing - Live audio-reactive effects - New effects: blend_multi for weighted layer compositing - Updated primitives and interpreter for streaming compatibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
125
streaming/demo.py
Normal file
125
streaming/demo.py
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo script for streaming compositor.
|
||||
|
||||
Usage:
|
||||
# Preview two videos blended
|
||||
python -m streaming.demo preview video1.mp4 video2.mp4
|
||||
|
||||
# Record output to file
|
||||
python -m streaming.demo record video1.mp4 video2.mp4 -o output.mp4
|
||||
|
||||
# Benchmark (no output)
|
||||
python -m streaming.demo benchmark video1.mp4 --duration 10
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from streaming import StreamingCompositor, VideoSource
|
||||
from streaming.output import NullOutput
|
||||
|
||||
|
||||
def demo_preview(sources: list, duration: float, effects: bool = False):
|
||||
"""Preview sources with optional simple effects."""
|
||||
effects_config = None
|
||||
if effects:
|
||||
effects_config = [
|
||||
[{"effect": "rotate", "angle": 15}],
|
||||
[{"effect": "zoom", "amount": 1.2}],
|
||||
][:len(sources)]
|
||||
|
||||
compositor = StreamingCompositor(
|
||||
sources=sources,
|
||||
effects_per_source=effects_config,
|
||||
recipe_dir=Path(__file__).parent.parent,
|
||||
)
|
||||
compositor.run(output="preview", duration=duration)
|
||||
|
||||
|
||||
def demo_record(sources: list, output_path: str, duration: float):
|
||||
"""Record blended output to file."""
|
||||
compositor = StreamingCompositor(
|
||||
sources=sources,
|
||||
recipe_dir=Path(__file__).parent.parent,
|
||||
)
|
||||
compositor.run(output=output_path, duration=duration)
|
||||
|
||||
|
||||
def demo_benchmark(sources: list, duration: float):
|
||||
"""Benchmark processing speed (no output)."""
|
||||
compositor = StreamingCompositor(
|
||||
sources=sources,
|
||||
recipe_dir=Path(__file__).parent.parent,
|
||||
)
|
||||
compositor.run(output="null", duration=duration)
|
||||
|
||||
|
||||
def demo_audio_reactive(sources: list, duration: float):
|
||||
"""Preview with live audio reactivity."""
|
||||
from streaming.audio import AudioAnalyzer
|
||||
|
||||
# Create compositor with energy-reactive effects
|
||||
effects_config = [
|
||||
[{
|
||||
"effect": "zoom",
|
||||
"amount": {"_binding": True, "source": "live_energy", "feature": "values", "range": [1.0, 1.5]},
|
||||
}]
|
||||
for _ in sources
|
||||
]
|
||||
|
||||
compositor = StreamingCompositor(
|
||||
sources=sources,
|
||||
effects_per_source=effects_config,
|
||||
recipe_dir=Path(__file__).parent.parent,
|
||||
)
|
||||
|
||||
# Start audio analyzer
|
||||
try:
|
||||
with AudioAnalyzer() as audio:
|
||||
print("Audio analyzer started. Make some noise!", file=sys.stderr)
|
||||
compositor.run(output="preview", duration=duration, audio_analyzer=audio)
|
||||
except Exception as e:
|
||||
print(f"Audio not available: {e}", file=sys.stderr)
|
||||
print("Running without audio...", file=sys.stderr)
|
||||
compositor.run(output="preview", duration=duration)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Streaming compositor demo")
|
||||
parser.add_argument("mode", choices=["preview", "record", "benchmark", "audio"],
|
||||
help="Demo mode")
|
||||
parser.add_argument("sources", nargs="+", help="Video source files")
|
||||
parser.add_argument("-o", "--output", help="Output file (for record mode)")
|
||||
parser.add_argument("-d", "--duration", type=float, default=30,
|
||||
help="Duration in seconds")
|
||||
parser.add_argument("--effects", action="store_true",
|
||||
help="Apply simple effects (for preview)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Verify sources exist
|
||||
for src in args.sources:
|
||||
if not Path(src).exists():
|
||||
print(f"Error: Source not found: {src}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.mode == "preview":
|
||||
demo_preview(args.sources, args.duration, args.effects)
|
||||
elif args.mode == "record":
|
||||
if not args.output:
|
||||
print("Error: --output required for record mode", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
demo_record(args.sources, args.output, args.duration)
|
||||
elif args.mode == "benchmark":
|
||||
demo_benchmark(args.sources, args.duration)
|
||||
elif args.mode == "audio":
|
||||
demo_audio_reactive(args.sources, args.duration)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user