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:
gilesb
2026-01-29 01:27:39 +00:00
parent 17e3e23f06
commit d241e2a663
31 changed files with 5143 additions and 96 deletions

View File

@@ -793,6 +793,35 @@ class Interpreter:
return list(self.effects.values())[-1]
return None
def load_effect_from_string(self, sexp_content: str, effect_name: str = None) -> EffectDefinition:
"""Load an effect definition from an S-expression string.
Args:
sexp_content: The S-expression content as a string
effect_name: Optional name hint (used if effect doesn't define its own name)
Returns:
The loaded EffectDefinition
"""
expr = parse(sexp_content)
# Handle multiple top-level expressions
if isinstance(expr, list) and expr and isinstance(expr[0], list):
for e in expr:
self.eval(e)
else:
self.eval(expr)
# Return the effect if we can find it by name
if effect_name and effect_name in self.effects:
return self.effects[effect_name]
# Return the most recently loaded effect
if self.effects:
return list(self.effects.values())[-1]
return None
def run_effect(self, name: str, frame, params: Dict[str, Any],
state: Dict[str, Any]) -> tuple:
"""