forked from genewildish/Mainline
feat(presets): Add upstream-default preset and enhance demo preset
- Add upstream-default preset matching upstream mainline behavior: - Terminal display (not pygame) - No message overlay - Classic effects: noise, fade, glitch, firehose - Mixed positioning mode - Enhance demo preset to showcase sideline features: - Hotswappable effects via effect plugins - LFO sensor modulation (oscillator sensor) - Mixed positioning mode - Message overlay with ntfy integration - Includes hud effect for visual feedback - Update all presets to use mixed positioning mode - Update completion script for --positioning flag Usage: python -m mainline --preset upstream-default --display terminal python -m mainline --preset demo --display pygame
This commit is contained in:
@@ -70,6 +70,12 @@ _mainline_completion() {
|
|||||||
COMPREPLY=($(compgen -W "demo demo-base demo-pygame demo-camera-showcase poetry headlines empty test-basic test-border test-scroll-camera" -- "${cur}"))
|
COMPREPLY=($(compgen -W "demo demo-base demo-pygame demo-camera-showcase poetry headlines empty test-basic test-border test-scroll-camera" -- "${cur}"))
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
--positioning)
|
||||||
|
# Positioning modes
|
||||||
|
COMPREPLY=($(compgen -W "absolute relative mixed" -- "${cur}"))
|
||||||
|
return
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Flag completion (start with --)
|
# Flag completion (start with --)
|
||||||
@@ -85,6 +91,7 @@ _mainline_completion() {
|
|||||||
--viewport
|
--viewport
|
||||||
--preset
|
--preset
|
||||||
--theme
|
--theme
|
||||||
|
--positioning
|
||||||
--websocket
|
--websocket
|
||||||
--websocket-port
|
--websocket-port
|
||||||
--allow-unsafe
|
--allow-unsafe
|
||||||
|
|||||||
@@ -265,6 +265,12 @@ def run_pipeline_mode_direct():
|
|||||||
)
|
)
|
||||||
|
|
||||||
display = DisplayRegistry.create(display_name)
|
display = DisplayRegistry.create(display_name)
|
||||||
|
|
||||||
|
# Set positioning mode
|
||||||
|
if "--positioning" in sys.argv:
|
||||||
|
idx = sys.argv.index("--positioning")
|
||||||
|
if idx + 1 < len(sys.argv):
|
||||||
|
params.positioning = sys.argv[idx + 1]
|
||||||
if not display:
|
if not display:
|
||||||
print(f" \033[38;5;196mFailed to create display: {display_name}\033[0m")
|
print(f" \033[38;5;196mFailed to create display: {display_name}\033[0m")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -139,6 +139,16 @@ def run_pipeline_mode(preset_name: str = "demo"):
|
|||||||
print("Error: Invalid viewport format. Use WxH (e.g., 40x15)")
|
print("Error: Invalid viewport format. Use WxH (e.g., 40x15)")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Set positioning mode from command line or config
|
||||||
|
if "--positioning" in sys.argv:
|
||||||
|
idx = sys.argv.index("--positioning")
|
||||||
|
if idx + 1 < len(sys.argv):
|
||||||
|
params.positioning = sys.argv[idx + 1]
|
||||||
|
else:
|
||||||
|
from engine import config as app_config
|
||||||
|
|
||||||
|
params.positioning = app_config.get_config().positioning
|
||||||
|
|
||||||
pipeline = Pipeline(config=preset.to_config())
|
pipeline = Pipeline(config=preset.to_config())
|
||||||
|
|
||||||
print(" \033[38;5;245mFetching content...\033[0m")
|
print(" \033[38;5;245mFetching content...\033[0m")
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ class Config:
|
|||||||
script_fonts: dict[str, str] = field(default_factory=_get_platform_font_paths)
|
script_fonts: dict[str, str] = field(default_factory=_get_platform_font_paths)
|
||||||
|
|
||||||
display: str = "pygame"
|
display: str = "pygame"
|
||||||
|
positioning: str = "mixed"
|
||||||
websocket: bool = False
|
websocket: bool = False
|
||||||
websocket_port: int = 8765
|
websocket_port: int = 8765
|
||||||
theme: str = "green"
|
theme: str = "green"
|
||||||
@@ -174,6 +175,7 @@ class Config:
|
|||||||
kata_glyphs="ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ",
|
kata_glyphs="ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ",
|
||||||
script_fonts=_get_platform_font_paths(),
|
script_fonts=_get_platform_font_paths(),
|
||||||
display=_arg_value("--display", argv) or "terminal",
|
display=_arg_value("--display", argv) or "terminal",
|
||||||
|
positioning=_arg_value("--positioning", argv) or "mixed",
|
||||||
websocket="--websocket" in argv,
|
websocket="--websocket" in argv,
|
||||||
websocket_port=_arg_int("--websocket-port", 8765, argv),
|
websocket_port=_arg_int("--websocket-port", 8765, argv),
|
||||||
theme=_arg_value("--theme", argv) or "green",
|
theme=_arg_value("--theme", argv) or "green",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -60,6 +60,7 @@ class PipelinePreset:
|
|||||||
source_items: list[dict[str, Any]] | None = None # For ListDataSource
|
source_items: list[dict[str, Any]] | None = None # For ListDataSource
|
||||||
enable_metrics: bool = True # Enable performance metrics collection
|
enable_metrics: bool = True # Enable performance metrics collection
|
||||||
enable_message_overlay: bool = False # Enable ntfy message overlay
|
enable_message_overlay: bool = False # Enable ntfy message overlay
|
||||||
|
positioning: str = "mixed" # Positioning mode: "absolute", "relative", "mixed"
|
||||||
|
|
||||||
def to_params(self) -> PipelineParams:
|
def to_params(self) -> PipelineParams:
|
||||||
"""Convert to PipelineParams (runtime configuration)."""
|
"""Convert to PipelineParams (runtime configuration)."""
|
||||||
@@ -68,6 +69,7 @@ class PipelinePreset:
|
|||||||
params = PipelineParams()
|
params = PipelineParams()
|
||||||
params.source = self.source
|
params.source = self.source
|
||||||
params.display = self.display
|
params.display = self.display
|
||||||
|
params.positioning = self.positioning
|
||||||
params.border = (
|
params.border = (
|
||||||
self.border
|
self.border
|
||||||
if isinstance(self.border, bool)
|
if isinstance(self.border, bool)
|
||||||
@@ -115,18 +117,38 @@ class PipelinePreset:
|
|||||||
source_items=data.get("source_items"),
|
source_items=data.get("source_items"),
|
||||||
enable_metrics=data.get("enable_metrics", True),
|
enable_metrics=data.get("enable_metrics", True),
|
||||||
enable_message_overlay=data.get("enable_message_overlay", False),
|
enable_message_overlay=data.get("enable_message_overlay", False),
|
||||||
|
positioning=data.get("positioning", "mixed"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Built-in presets
|
# Built-in presets
|
||||||
|
# Upstream-default preset: Matches the default upstream Mainline operation
|
||||||
|
UPSTREAM_PRESET = PipelinePreset(
|
||||||
|
name="upstream-default",
|
||||||
|
description="Upstream default operation (terminal display, legacy behavior)",
|
||||||
|
source="headlines",
|
||||||
|
display="terminal",
|
||||||
|
camera="scroll",
|
||||||
|
effects=["noise", "fade", "glitch", "firehose"],
|
||||||
|
enable_message_overlay=False,
|
||||||
|
positioning="mixed",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Demo preset: Showcases hotswappable effects and sensors
|
||||||
|
# This preset demonstrates the sideline features:
|
||||||
|
# - Hotswappable effects via effect plugins
|
||||||
|
# - Sensor integration (oscillator LFO for modulation)
|
||||||
|
# - Mixed positioning mode
|
||||||
|
# - Message overlay with ntfy integration
|
||||||
DEMO_PRESET = PipelinePreset(
|
DEMO_PRESET = PipelinePreset(
|
||||||
name="demo",
|
name="demo",
|
||||||
description="Demo mode with effect cycling and camera modes",
|
description="Demo: Hotswappable effects, LFO sensor modulation, mixed positioning",
|
||||||
source="headlines",
|
source="headlines",
|
||||||
display="pygame",
|
display="pygame",
|
||||||
camera="scroll",
|
camera="scroll",
|
||||||
effects=["noise", "fade", "glitch", "firehose"],
|
effects=["noise", "fade", "glitch", "firehose", "hud"],
|
||||||
enable_message_overlay=True,
|
enable_message_overlay=True,
|
||||||
|
positioning="mixed",
|
||||||
)
|
)
|
||||||
|
|
||||||
UI_PRESET = PipelinePreset(
|
UI_PRESET = PipelinePreset(
|
||||||
@@ -201,6 +223,7 @@ def _build_presets() -> dict[str, PipelinePreset]:
|
|||||||
# Add built-in presets as fallback (if not in YAML)
|
# Add built-in presets as fallback (if not in YAML)
|
||||||
builtins = {
|
builtins = {
|
||||||
"demo": DEMO_PRESET,
|
"demo": DEMO_PRESET,
|
||||||
|
"upstream-default": UPSTREAM_PRESET,
|
||||||
"poetry": POETRY_PRESET,
|
"poetry": POETRY_PRESET,
|
||||||
"pipeline": PIPELINE_VIZ_PRESET,
|
"pipeline": PIPELINE_VIZ_PRESET,
|
||||||
"websocket": WEBSOCKET_PRESET,
|
"websocket": WEBSOCKET_PRESET,
|
||||||
|
|||||||
18
presets.toml
18
presets.toml
@@ -53,6 +53,18 @@ viewport_height = 24
|
|||||||
# DEMO PRESETS (for demonstration and exploration)
|
# DEMO PRESETS (for demonstration and exploration)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
|
[presets.upstream-default]
|
||||||
|
description = "Upstream default operation (terminal display, legacy behavior)"
|
||||||
|
source = "headlines"
|
||||||
|
display = "terminal"
|
||||||
|
camera = "scroll"
|
||||||
|
effects = ["noise", "fade", "glitch", "firehose"]
|
||||||
|
camera_speed = 1.0
|
||||||
|
viewport_width = 80
|
||||||
|
viewport_height = 24
|
||||||
|
enable_message_overlay = false
|
||||||
|
positioning = "mixed"
|
||||||
|
|
||||||
[presets.demo-base]
|
[presets.demo-base]
|
||||||
description = "Demo: Base preset for effect hot-swapping"
|
description = "Demo: Base preset for effect hot-swapping"
|
||||||
source = "headlines"
|
source = "headlines"
|
||||||
@@ -63,17 +75,19 @@ camera_speed = 0.1
|
|||||||
viewport_width = 80
|
viewport_width = 80
|
||||||
viewport_height = 24
|
viewport_height = 24
|
||||||
enable_message_overlay = true
|
enable_message_overlay = true
|
||||||
|
positioning = "mixed"
|
||||||
|
|
||||||
[presets.demo-pygame]
|
[presets.demo-pygame]
|
||||||
description = "Demo: Pygame display version"
|
description = "Demo: Pygame display version"
|
||||||
source = "headlines"
|
source = "headlines"
|
||||||
display = "pygame"
|
display = "pygame"
|
||||||
camera = "feed"
|
camera = "feed"
|
||||||
effects = [] # Demo script will add/remove effects dynamically
|
effects = ["noise", "fade", "glitch", "firehose"] # Default effects
|
||||||
camera_speed = 0.1
|
camera_speed = 0.1
|
||||||
viewport_width = 80
|
viewport_width = 80
|
||||||
viewport_height = 24
|
viewport_height = 24
|
||||||
enable_message_overlay = true
|
enable_message_overlay = true
|
||||||
|
positioning = "mixed"
|
||||||
|
|
||||||
[presets.demo-camera-showcase]
|
[presets.demo-camera-showcase]
|
||||||
description = "Demo: Camera mode showcase"
|
description = "Demo: Camera mode showcase"
|
||||||
@@ -85,6 +99,7 @@ camera_speed = 0.5
|
|||||||
viewport_width = 80
|
viewport_width = 80
|
||||||
viewport_height = 24
|
viewport_height = 24
|
||||||
enable_message_overlay = true
|
enable_message_overlay = true
|
||||||
|
positioning = "mixed"
|
||||||
|
|
||||||
[presets.test-message-overlay]
|
[presets.test-message-overlay]
|
||||||
description = "Test: Message overlay with ntfy integration"
|
description = "Test: Message overlay with ntfy integration"
|
||||||
@@ -96,6 +111,7 @@ camera_speed = 0.1
|
|||||||
viewport_width = 80
|
viewport_width = 80
|
||||||
viewport_height = 24
|
viewport_height = 24
|
||||||
enable_message_overlay = true
|
enable_message_overlay = true
|
||||||
|
positioning = "mixed"
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# SENSOR CONFIGURATION
|
# SENSOR CONFIGURATION
|
||||||
|
|||||||
151
scripts/demo-lfo-effects.py
Normal file
151
scripts/demo-lfo-effects.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Pygame Demo: Effects with LFO Modulation
|
||||||
|
|
||||||
|
This demo shows how to use LFO (Low Frequency Oscillator) to modulate
|
||||||
|
effect intensities over time, creating smooth animated changes.
|
||||||
|
|
||||||
|
Effects modulated:
|
||||||
|
- noise: Random noise intensity
|
||||||
|
- fade: Fade effect intensity
|
||||||
|
- tint: Color tint intensity
|
||||||
|
- glitch: Glitch effect intensity
|
||||||
|
|
||||||
|
The LFO uses a sine wave to oscillate intensity between 0.0 and 1.0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from engine import config
|
||||||
|
from engine.display import DisplayRegistry
|
||||||
|
from engine.effects import get_registry
|
||||||
|
from engine.pipeline import Pipeline, PipelineConfig, PipelineContext, list_presets
|
||||||
|
from engine.pipeline.params import PipelineParams
|
||||||
|
from engine.pipeline.preset_loader import load_presets
|
||||||
|
from engine.sensors.oscillator import OscillatorSensor
|
||||||
|
from engine.sources import FEEDS
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LFOEffectConfig:
|
||||||
|
"""Configuration for LFO-modulated effect."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
frequency: float # LFO frequency in Hz
|
||||||
|
phase_offset: float # Phase offset (0.0 to 1.0)
|
||||||
|
min_intensity: float = 0.0
|
||||||
|
max_intensity: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
class LFOEffectDemo:
|
||||||
|
"""Demo controller that modulates effect intensities using LFO."""
|
||||||
|
|
||||||
|
def __init__(self, pipeline: Pipeline):
|
||||||
|
self.pipeline = pipeline
|
||||||
|
self.effects = [
|
||||||
|
LFOEffectConfig("noise", frequency=0.5, phase_offset=0.0),
|
||||||
|
LFOEffectConfig("fade", frequency=0.3, phase_offset=0.33),
|
||||||
|
LFOEffectConfig("tint", frequency=0.4, phase_offset=0.66),
|
||||||
|
LFOEffectConfig("glitch", frequency=0.6, phase_offset=0.9),
|
||||||
|
]
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.frame_count = 0
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update effect intensities based on LFO."""
|
||||||
|
elapsed = time.time() - self.start_time
|
||||||
|
self.frame_count += 1
|
||||||
|
|
||||||
|
for effect_cfg in self.effects:
|
||||||
|
# Calculate LFO value using sine wave
|
||||||
|
angle = (
|
||||||
|
(elapsed * effect_cfg.frequency + effect_cfg.phase_offset) * 2 * 3.14159
|
||||||
|
)
|
||||||
|
lfo_value = 0.5 + 0.5 * (angle.__sin__())
|
||||||
|
|
||||||
|
# Scale to intensity range
|
||||||
|
intensity = effect_cfg.min_intensity + lfo_value * (
|
||||||
|
effect_cfg.max_intensity - effect_cfg.min_intensity
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update effect intensity in pipeline
|
||||||
|
self.pipeline.set_effect_intensity(effect_cfg.name, intensity)
|
||||||
|
|
||||||
|
def run(self, duration: float = 30.0):
|
||||||
|
"""Run the demo for specified duration."""
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print("LFO EFFECT MODULATION DEMO")
|
||||||
|
print(f"{'=' * 60}")
|
||||||
|
print("\nEffects being modulated:")
|
||||||
|
for effect in self.effects:
|
||||||
|
print(f" - {effect.name}: {effect.frequency}Hz")
|
||||||
|
print(f"\nPress Ctrl+C to stop")
|
||||||
|
print(f"{'=' * 60}\n")
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
while time.time() - start < duration:
|
||||||
|
self.update()
|
||||||
|
time.sleep(0.016) # ~60 FPS
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nDemo stopped by user")
|
||||||
|
finally:
|
||||||
|
print(f"\nTotal frames rendered: {self.frame_count}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point for the LFO demo."""
|
||||||
|
# Configuration
|
||||||
|
effect_names = ["noise", "fade", "tint", "glitch"]
|
||||||
|
|
||||||
|
# Get pipeline config from preset
|
||||||
|
preset_name = "demo-pygame"
|
||||||
|
presets = load_presets()
|
||||||
|
preset = presets["presets"].get(preset_name)
|
||||||
|
if not preset:
|
||||||
|
print(f"Error: Preset '{preset_name}' not found")
|
||||||
|
print(f"Available presets: {list(presets['presets'].keys())}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Create pipeline context
|
||||||
|
ctx = PipelineContext()
|
||||||
|
ctx.terminal_width = preset.get("viewport_width", 80)
|
||||||
|
ctx.terminal_height = preset.get("viewport_height", 24)
|
||||||
|
|
||||||
|
# Create params
|
||||||
|
params = PipelineParams(
|
||||||
|
source=preset.get("source", "headlines"),
|
||||||
|
display="pygame", # Force pygame display
|
||||||
|
camera_mode=preset.get("camera", "feed"),
|
||||||
|
effect_order=effect_names, # Enable our effects
|
||||||
|
viewport_width=preset.get("viewport_width", 80),
|
||||||
|
viewport_height=preset.get("viewport_height", 24),
|
||||||
|
)
|
||||||
|
ctx.params = params
|
||||||
|
|
||||||
|
# Create pipeline config
|
||||||
|
pipeline_config = PipelineConfig(
|
||||||
|
source=preset.get("source", "headlines"),
|
||||||
|
display="pygame",
|
||||||
|
camera=preset.get("camera", "feed"),
|
||||||
|
effects=effect_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create pipeline
|
||||||
|
pipeline = Pipeline(config=pipeline_config, context=ctx)
|
||||||
|
|
||||||
|
# Build pipeline
|
||||||
|
pipeline.build()
|
||||||
|
|
||||||
|
# Create demo controller
|
||||||
|
demo = LFOEffectDemo(pipeline)
|
||||||
|
|
||||||
|
# Run demo
|
||||||
|
demo.run(duration=30.0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user