Add JAX typography, xector primitives, deferred effect chains, and GPU streaming
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m28s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m28s
- Add JAX text rendering with font atlas, styled text placement, and typography primitives - Add xector (element-wise/reduction) operations library and sexp effects - Add deferred effect chain fusion for JIT-compiled effect pipelines - Expand drawing primitives with font management, alignment, shadow, and outline - Add interpreter support for function-style define and require - Add GPU persistence mode and hardware decode support to streaming - Add new sexp effects: cell_pattern, halftone, mosaic, and derived definitions - Add path registry for asset resolution - Add integration, primitives, and xector tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,12 +8,18 @@ GPU Acceleration:
|
||||
- Set STREAMING_GPU_PERSIST=1 to output CuPy arrays (frames stay on GPU)
|
||||
- Hardware video decoding (NVDEC) is used when available
|
||||
- Dramatically improves performance on GPU nodes
|
||||
|
||||
Async Prefetching:
|
||||
- Set STREAMING_PREFETCH=1 to enable background frame prefetching
|
||||
- Decodes upcoming frames while current frame is being processed
|
||||
"""
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
import subprocess
|
||||
import json
|
||||
import threading
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
|
||||
# Try to import CuPy for GPU acceleration
|
||||
@@ -28,6 +34,10 @@ except ImportError:
|
||||
# Disabled by default until all primitives support GPU frames
|
||||
GPU_PERSIST = os.environ.get("STREAMING_GPU_PERSIST", "0") == "1" and CUPY_AVAILABLE
|
||||
|
||||
# Async prefetch mode - decode frames in background thread
|
||||
PREFETCH_ENABLED = os.environ.get("STREAMING_PREFETCH", "1") == "1"
|
||||
PREFETCH_BUFFER_SIZE = int(os.environ.get("STREAMING_PREFETCH_SIZE", "10"))
|
||||
|
||||
# Check for hardware decode support (cached)
|
||||
_HWDEC_AVAILABLE = None
|
||||
|
||||
@@ -283,6 +293,122 @@ class VideoSource:
|
||||
self._proc = None
|
||||
|
||||
|
||||
class PrefetchingVideoSource:
|
||||
"""
|
||||
Video source with background prefetching for improved performance.
|
||||
|
||||
Wraps VideoSource and adds a background thread that pre-decodes
|
||||
upcoming frames while the main thread processes the current frame.
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, fps: float = 30, buffer_size: int = None):
|
||||
self._source = VideoSource(path, fps)
|
||||
self._buffer_size = buffer_size or PREFETCH_BUFFER_SIZE
|
||||
self._buffer = {} # time -> frame
|
||||
self._buffer_lock = threading.Lock()
|
||||
self._prefetch_time = 0.0
|
||||
self._frame_time = 1.0 / fps
|
||||
self._stop_event = threading.Event()
|
||||
self._request_event = threading.Event()
|
||||
self._target_time = 0.0
|
||||
|
||||
# Start prefetch thread
|
||||
self._thread = threading.Thread(target=self._prefetch_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
import sys
|
||||
print(f"PrefetchingVideoSource: {path} buffer_size={self._buffer_size}", file=sys.stderr)
|
||||
|
||||
def _prefetch_loop(self):
|
||||
"""Background thread that pre-reads frames."""
|
||||
while not self._stop_event.is_set():
|
||||
# Wait for work or timeout
|
||||
self._request_event.wait(timeout=0.01)
|
||||
self._request_event.clear()
|
||||
|
||||
if self._stop_event.is_set():
|
||||
break
|
||||
|
||||
# Prefetch frames ahead of target time
|
||||
target = self._target_time
|
||||
with self._buffer_lock:
|
||||
# Clean old frames (more than 1 second behind)
|
||||
old_times = [t for t in self._buffer.keys() if t < target - 1.0]
|
||||
for t in old_times:
|
||||
del self._buffer[t]
|
||||
|
||||
# Count how many frames we have buffered ahead
|
||||
buffered_ahead = sum(1 for t in self._buffer.keys() if t >= target)
|
||||
|
||||
# Prefetch if buffer not full
|
||||
if buffered_ahead < self._buffer_size:
|
||||
# Find next time to prefetch
|
||||
prefetch_t = target
|
||||
with self._buffer_lock:
|
||||
existing_times = set(self._buffer.keys())
|
||||
for _ in range(self._buffer_size):
|
||||
if prefetch_t not in existing_times:
|
||||
break
|
||||
prefetch_t += self._frame_time
|
||||
|
||||
# Read the frame (this is the slow part)
|
||||
try:
|
||||
frame = self._source.read_at(prefetch_t)
|
||||
with self._buffer_lock:
|
||||
self._buffer[prefetch_t] = frame
|
||||
except Exception as e:
|
||||
import sys
|
||||
print(f"Prefetch error at t={prefetch_t}: {e}", file=sys.stderr)
|
||||
|
||||
def read_at(self, t: float) -> np.ndarray:
|
||||
"""Read frame at specific time, using prefetch buffer if available."""
|
||||
self._target_time = t
|
||||
self._request_event.set() # Wake up prefetch thread
|
||||
|
||||
# Round to frame time for buffer lookup
|
||||
t_key = round(t / self._frame_time) * self._frame_time
|
||||
|
||||
# Check buffer first
|
||||
with self._buffer_lock:
|
||||
if t_key in self._buffer:
|
||||
return self._buffer[t_key]
|
||||
# Also check for close matches (within half frame time)
|
||||
for buf_t, frame in self._buffer.items():
|
||||
if abs(buf_t - t) < self._frame_time * 0.5:
|
||||
return frame
|
||||
|
||||
# Not in buffer - read directly (blocking)
|
||||
frame = self._source.read_at(t)
|
||||
|
||||
# Store in buffer
|
||||
with self._buffer_lock:
|
||||
self._buffer[t_key] = frame
|
||||
|
||||
return frame
|
||||
|
||||
def read(self) -> np.ndarray:
|
||||
"""Read frame (uses last cached or t=0)."""
|
||||
return self.read_at(0)
|
||||
|
||||
def skip(self):
|
||||
"""No-op for seek-based reading."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._source.size
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._source.path
|
||||
|
||||
def close(self):
|
||||
self._stop_event.set()
|
||||
self._request_event.set() # Wake up thread to exit
|
||||
self._thread.join(timeout=1.0)
|
||||
self._source.close()
|
||||
|
||||
|
||||
class AudioAnalyzer:
|
||||
"""Audio analyzer for energy and beat detection."""
|
||||
|
||||
@@ -394,7 +520,12 @@ class AudioAnalyzer:
|
||||
# === Primitives ===
|
||||
|
||||
def prim_make_video_source(path: str, fps: float = 30):
|
||||
"""Create a video source from a file path."""
|
||||
"""Create a video source from a file path.
|
||||
|
||||
Uses PrefetchingVideoSource if STREAMING_PREFETCH=1 (default).
|
||||
"""
|
||||
if PREFETCH_ENABLED:
|
||||
return PrefetchingVideoSource(path, fps)
|
||||
return VideoSource(path, fps)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user