Add IPFS HLS streaming and GPU optimizations
- Add IPFSHLSOutput class that uploads segments to IPFS as they're created - Update streaming task to use IPFS HLS output for distributed streaming - Add /ipfs-stream endpoint to get IPFS playlist URL - Update /stream endpoint to redirect to IPFS when available - Add GPU persistence mode (STREAMING_GPU_PERSIST=1) to keep frames on GPU - Add hardware video decoding (NVDEC) support for faster video processing - Add GPU-accelerated primitive libraries: blending_gpu, color_ops_gpu, geometry_gpu - Add streaming_gpu module with GPUFrame class for tracking CPU/GPU data location - Add Dockerfile.gpu for building GPU-enabled worker image Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -159,36 +159,51 @@ class StreamInterpreter:
|
||||
return config
|
||||
|
||||
def _load_primitives(self, lib_name: str):
|
||||
"""Load primitives from a Python library file."""
|
||||
"""Load primitives from a Python library file.
|
||||
|
||||
Prefers GPU-accelerated versions (*_gpu.py) when available.
|
||||
"""
|
||||
import importlib.util
|
||||
|
||||
lib_paths = [
|
||||
self.primitive_lib_dir / f"{lib_name}.py",
|
||||
self.sexp_dir / "primitive_libs" / f"{lib_name}.py",
|
||||
self.sexp_dir.parent / "sexp_effects" / "primitive_libs" / f"{lib_name}.py",
|
||||
]
|
||||
# Try GPU version first, then fall back to CPU version
|
||||
lib_names_to_try = [f"{lib_name}_gpu", lib_name]
|
||||
|
||||
lib_path = None
|
||||
for p in lib_paths:
|
||||
if p.exists():
|
||||
lib_path = p
|
||||
actual_lib_name = lib_name
|
||||
|
||||
for try_lib in lib_names_to_try:
|
||||
lib_paths = [
|
||||
self.primitive_lib_dir / f"{try_lib}.py",
|
||||
self.sexp_dir / "primitive_libs" / f"{try_lib}.py",
|
||||
self.sexp_dir.parent / "sexp_effects" / "primitive_libs" / f"{try_lib}.py",
|
||||
]
|
||||
for p in lib_paths:
|
||||
if p.exists():
|
||||
lib_path = p
|
||||
actual_lib_name = try_lib
|
||||
break
|
||||
if lib_path:
|
||||
break
|
||||
|
||||
if not lib_path:
|
||||
print(f"Warning: primitive library '{lib_name}' not found", file=sys.stderr)
|
||||
return
|
||||
|
||||
spec = importlib.util.spec_from_file_location(lib_name, lib_path)
|
||||
spec = importlib.util.spec_from_file_location(actual_lib_name, lib_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Check if this is a GPU-accelerated module
|
||||
is_gpu = actual_lib_name.endswith('_gpu')
|
||||
gpu_tag = " [GPU]" if is_gpu else ""
|
||||
|
||||
count = 0
|
||||
for name in dir(module):
|
||||
if name.startswith('prim_'):
|
||||
func = getattr(module, name)
|
||||
prim_name = name[5:]
|
||||
dash_name = prim_name.replace('_', '-')
|
||||
# Register ONLY with namespace (geometry:ripple-displace)
|
||||
# Register with original lib_name namespace (geometry:rotate, not geometry_gpu:rotate)
|
||||
# Don't overwrite if already registered (allows pre-registration of overrides)
|
||||
key = f"{lib_name}:{dash_name}"
|
||||
if key not in self.primitives:
|
||||
@@ -199,7 +214,7 @@ class StreamInterpreter:
|
||||
prims = getattr(module, 'PRIMITIVES')
|
||||
if isinstance(prims, dict):
|
||||
for name, func in prims.items():
|
||||
# Register ONLY with namespace
|
||||
# Register with original lib_name namespace
|
||||
# Don't overwrite if already registered
|
||||
dash_name = name.replace('_', '-')
|
||||
key = f"{lib_name}:{dash_name}"
|
||||
@@ -207,7 +222,7 @@ class StreamInterpreter:
|
||||
self.primitives[key] = func
|
||||
count += 1
|
||||
|
||||
print(f"Loaded primitives: {lib_name} ({count} functions)", file=sys.stderr)
|
||||
print(f"Loaded primitives: {lib_name} ({count} functions){gpu_tag}", file=sys.stderr)
|
||||
|
||||
def _load_effect(self, effect_path: Path):
|
||||
"""Load and register an effect from a .sexp file."""
|
||||
@@ -807,8 +822,11 @@ class StreamInterpreter:
|
||||
self._record_error(f"Primitive {op} error: {e}")
|
||||
raise RuntimeError(f"Primitive {op} failed: {e}")
|
||||
|
||||
# Unknown - return as-is
|
||||
return expr
|
||||
# Unknown function call - raise meaningful error
|
||||
raise RuntimeError(f"Unknown function or primitive: '{op}'. "
|
||||
f"Available primitives: {sorted(list(self.primitives.keys())[:10])}... "
|
||||
f"Available effects: {sorted(list(self.effects.keys())[:10])}... "
|
||||
f"Available macros: {sorted(list(self.macros.keys())[:10])}...")
|
||||
|
||||
def _step_scans(self, ctx: Context, env: dict):
|
||||
"""Step scans based on trigger evaluation."""
|
||||
@@ -833,9 +851,9 @@ class StreamInterpreter:
|
||||
"""Run the streaming pipeline."""
|
||||
# Import output classes - handle both package and direct execution
|
||||
try:
|
||||
from .output import PipeOutput, DisplayOutput, FileOutput
|
||||
from .output import PipeOutput, DisplayOutput, FileOutput, HLSOutput, IPFSHLSOutput
|
||||
except ImportError:
|
||||
from output import PipeOutput, DisplayOutput, FileOutput
|
||||
from output import PipeOutput, DisplayOutput, FileOutput, HLSOutput, IPFSHLSOutput
|
||||
|
||||
self._init()
|
||||
|
||||
@@ -871,6 +889,16 @@ class StreamInterpreter:
|
||||
out = PipeOutput(size=(w, h), fps=fps, audio_source=audio)
|
||||
elif output == "preview":
|
||||
out = DisplayOutput(size=(w, h), fps=fps, audio_source=audio)
|
||||
elif output.endswith("/hls"):
|
||||
# HLS output - output is a directory path ending in /hls
|
||||
hls_dir = output[:-4] # Remove /hls suffix
|
||||
out = HLSOutput(hls_dir, size=(w, h), fps=fps, audio_source=audio)
|
||||
elif output.endswith("/ipfs-hls"):
|
||||
# IPFS HLS output - segments uploaded to IPFS as they're created
|
||||
hls_dir = output[:-9] # Remove /ipfs-hls suffix
|
||||
import os
|
||||
ipfs_gateway = os.environ.get("IPFS_GATEWAY_URL", "https://ipfs.io/ipfs")
|
||||
out = IPFSHLSOutput(hls_dir, size=(w, h), fps=fps, audio_source=audio, ipfs_gateway=ipfs_gateway)
|
||||
else:
|
||||
out = FileOutput(output, size=(w, h), fps=fps, audio_source=audio)
|
||||
|
||||
@@ -916,6 +944,8 @@ class StreamInterpreter:
|
||||
|
||||
finally:
|
||||
out.close()
|
||||
# Store output for access to properties like playlist_cid
|
||||
self.output = out
|
||||
print("\nDone", file=sys.stderr)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user