feat(processed-audio-sources): phase 1 - audio filter framework
Add the foundation for audio processing filters, mirroring the existing picture filter/postprocessing template system: - AudioFilter base class, AudioFilterRegistry, AudioFilterOptionDef - AudioProcessingTemplate dataclass + SQLite-backed store - audio_filter_template meta-filter with recursive resolution - Full REST API: CRUD templates + filter registry discovery - Dependency injection wired in dependencies.py and main.py
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
# Feature Context: Demo Mode
|
||||
|
||||
## Current State
|
||||
Starting implementation. No changes made yet.
|
||||
|
||||
## Key Architecture Notes
|
||||
- `EngineRegistry` (class-level dict) holds capture engines, auto-registered in `capture_engines/__init__.py`
|
||||
- `AudioEngineRegistry` (class-level dict) holds audio engines, auto-registered in `audio/__init__.py`
|
||||
- `LEDDeviceProvider` instances registered via `register_provider()` in `led_client.py`
|
||||
- Already has `MockDeviceProvider` + `MockClient` (device type "mock") for testing
|
||||
- Config is `pydantic_settings.BaseSettings` in `config.py`, loaded from YAML + env vars
|
||||
- Frontend header in `templates/index.html` line 27-31: title + version badge
|
||||
- Frontend bundle: `cd server && npm run build` (esbuild)
|
||||
- Data stored as JSON in `data/` directory, paths configured via `StorageConfig`
|
||||
|
||||
## Temporary Workarounds
|
||||
- None yet
|
||||
|
||||
## Cross-Phase Dependencies
|
||||
- Phase 1 (config flag) is foundational — all other phases depend on `is_demo_mode()`
|
||||
- Phase 2 & 3 (engines) can be done independently of each other
|
||||
- Phase 4 (seed data) depends on knowing what entities to create, which is informed by phases 2-3
|
||||
- Phase 5 (frontend) depends on the system info API field from phase 1
|
||||
- Phase 6 (engine resolution) depends on engines existing from phases 2-3
|
||||
|
||||
## Implementation Notes
|
||||
- Demo mode activated via `WLED_DEMO=true` env var or `demo: true` in YAML config
|
||||
- Isolated data directory `data/demo/` keeps demo entities separate from real config
|
||||
- Demo engines use `ENGINE_TYPE = "demo"` and are always registered but return `is_available() = True` only in demo mode
|
||||
- The existing `MockDeviceProvider`/`MockClient` can be reused or extended for demo device output
|
||||
@@ -1,44 +0,0 @@
|
||||
# Feature: Demo Mode
|
||||
|
||||
**Branch:** `feature/demo-mode`
|
||||
**Base branch:** `master`
|
||||
**Created:** 2026-03-20
|
||||
**Status:** 🟡 In Progress
|
||||
**Strategy:** Big Bang
|
||||
**Mode:** Automated
|
||||
**Execution:** Orchestrator
|
||||
|
||||
## Summary
|
||||
Add a demo mode that allows users to explore and test the app without real hardware. Virtual capture engines, audio engines, and device providers replace real hardware. An isolated data directory with seed data provides a fully populated sandbox. A visual indicator in the UI makes it clear the app is running in demo mode.
|
||||
|
||||
## Build & Test Commands
|
||||
- **Build (frontend):** `cd server && npm run build`
|
||||
- **Typecheck (frontend):** `cd server && npm run typecheck`
|
||||
- **Test (backend):** `cd server && python -m pytest ../tests/ -x`
|
||||
- **Server start:** `cd server && python -m wled_controller.main`
|
||||
|
||||
## Phases
|
||||
|
||||
- [x] Phase 1: Demo Mode Config & Flag [domain: backend] → [subplan](./phase-1-config-flag.md)
|
||||
- [x] Phase 2: Virtual Capture Engine [domain: backend] → [subplan](./phase-2-virtual-capture-engine.md)
|
||||
- [x] Phase 3: Virtual Audio Engine [domain: backend] → [subplan](./phase-3-virtual-audio-engine.md)
|
||||
- [x] Phase 4: Demo Device Provider & Seed Data [domain: backend] → [subplan](./phase-4-demo-device-seed-data.md)
|
||||
- [x] Phase 5: Frontend Demo Indicator & Sandbox UX [domain: fullstack] → [subplan](./phase-5-frontend-demo-ux.md)
|
||||
- [x] Phase 6: Demo-only Engine Resolution [domain: backend] → [subplan](./phase-6-engine-resolution.md)
|
||||
|
||||
## Phase Progress Log
|
||||
|
||||
| Phase | Domain | Status | Review | Build | Committed |
|
||||
|-------|--------|--------|--------|-------|-----------|
|
||||
| Phase 1: Config & Flag | backend | ✅ Done | ✅ | ✅ | ⬜ |
|
||||
| Phase 2: Virtual Capture Engine | backend | ✅ Done | ✅ | ✅ | ⬜ |
|
||||
| Phase 3: Virtual Audio Engine | backend | ✅ Done | ✅ | ✅ | ⬜ |
|
||||
| Phase 4: Demo Device & Seed Data | backend | ✅ Done | ✅ | ✅ | ⬜ |
|
||||
| Phase 5: Frontend Demo UX | fullstack | ✅ Done | ✅ | ✅ | ⬜ |
|
||||
| Phase 6: Engine Resolution | backend | ✅ Done | ✅ | ✅ | ⬜ |
|
||||
|
||||
## Final Review
|
||||
- [ ] Comprehensive code review
|
||||
- [ ] Full build passes
|
||||
- [ ] Full test suite passes
|
||||
- [ ] Merged to `master`
|
||||
@@ -1,42 +0,0 @@
|
||||
# Phase 1: Demo Mode Config & Flag
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Add a `demo` boolean flag to the application configuration and expose it to the frontend via the system info API. When demo mode is active, the server uses an isolated data directory so demo entities don't pollute real user data.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Add `demo: bool = False` field to `Config` class in `config.py`
|
||||
- [ ] Task 2: Add a module-level helper `is_demo_mode() -> bool` in `config.py` for easy import
|
||||
- [ ] Task 3: Modify `StorageConfig` path resolution: when `demo=True`, prefix all storage paths with `data/demo/` instead of `data/`
|
||||
- [ ] Task 4: Expose `demo_mode: bool` in the existing `GET /api/v1/system/info` endpoint response
|
||||
- [ ] Task 5: Add `WLED_DEMO=true` env var support (already handled by pydantic-settings env prefix `WLED_`)
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/config.py` — Add `demo` field, `is_demo_mode()` helper, storage path override
|
||||
- `server/src/wled_controller/api/routes/system.py` — Add `demo_mode` to system info response
|
||||
- `server/src/wled_controller/api/schemas/system.py` — Add `demo_mode` field to response schema
|
||||
|
||||
## Acceptance Criteria
|
||||
- `Config(demo=True)` is accepted; default is `False`
|
||||
- `WLED_DEMO=true` activates demo mode
|
||||
- `is_demo_mode()` returns the correct value
|
||||
- When demo mode is on, all storage files resolve under `data/demo/`
|
||||
- `GET /api/v1/system/info` includes `demo_mode: true/false`
|
||||
|
||||
## Notes
|
||||
- The env var will be `WLED_DEMO` because of `env_prefix="WLED_"` in pydantic-settings
|
||||
- Storage path override should happen at `Config` construction time, not lazily
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in after completion -->
|
||||
@@ -1,48 +0,0 @@
|
||||
# Phase 2: Virtual Capture Engine
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Create a `DemoCaptureEngine` that provides virtual displays and produces animated test pattern frames, allowing screen capture workflows to function in demo mode without real monitors.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Create `server/src/wled_controller/core/capture_engines/demo_engine.py` with `DemoCaptureEngine` and `DemoCaptureStream`
|
||||
- [ ] Task 2: `DemoCaptureEngine.ENGINE_TYPE = "demo"`, `ENGINE_PRIORITY = 1000` (highest in demo mode)
|
||||
- [ ] Task 3: `is_available()` returns `True` only when `is_demo_mode()` is True
|
||||
- [ ] Task 4: `get_available_displays()` returns 3 virtual displays:
|
||||
- "Demo Display 1080p" (1920×1080)
|
||||
- "Demo Ultrawide" (3440×1440)
|
||||
- "Demo Portrait" (1080×1920)
|
||||
- [ ] Task 5: `DemoCaptureStream.capture_frame()` produces animated test patterns:
|
||||
- Horizontally scrolling rainbow gradient (simple, visually clear)
|
||||
- Uses `time.time()` for animation so frames change over time
|
||||
- Returns proper `ScreenCapture` with RGB numpy array
|
||||
- [ ] Task 6: Register `DemoCaptureEngine` in `capture_engines/__init__.py`
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/core/capture_engines/demo_engine.py` — New file: DemoCaptureEngine + DemoCaptureStream
|
||||
- `server/src/wled_controller/core/capture_engines/__init__.py` — Register DemoCaptureEngine
|
||||
|
||||
## Acceptance Criteria
|
||||
- `DemoCaptureEngine.is_available()` is True only in demo mode
|
||||
- Virtual displays appear in the display list API when in demo mode
|
||||
- `capture_frame()` returns valid RGB frames that change over time
|
||||
- Engine is properly registered in EngineRegistry
|
||||
|
||||
## Notes
|
||||
- Test patterns should be computationally cheap (no heavy image processing)
|
||||
- Use numpy operations for pattern generation (vectorized, fast)
|
||||
- Frame dimensions must match the virtual display dimensions
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in after completion -->
|
||||
@@ -1,47 +0,0 @@
|
||||
# Phase 3: Virtual Audio Engine
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Create a `DemoAudioEngine` that provides virtual audio devices and produces synthetic audio data, enabling audio-reactive visualizations in demo mode.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Create `server/src/wled_controller/core/audio/demo_engine.py` with `DemoAudioEngine` and `DemoAudioCaptureStream`
|
||||
- [ ] Task 2: `DemoAudioEngine.ENGINE_TYPE = "demo"`, `ENGINE_PRIORITY = 1000`
|
||||
- [ ] Task 3: `is_available()` returns `True` only when `is_demo_mode()` is True
|
||||
- [ ] Task 4: `enumerate_devices()` returns 2 virtual devices:
|
||||
- "Demo Microphone" (input, not loopback)
|
||||
- "Demo System Audio" (loopback)
|
||||
- [ ] Task 5: `DemoAudioCaptureStream` implements:
|
||||
- `channels = 2`, `sample_rate = 44100`, `chunk_size = 1024`
|
||||
- `read_chunk()` produces synthetic audio: a mix of sine waves with slowly varying frequencies to simulate music-like beat patterns
|
||||
- Returns proper float32 ndarray
|
||||
- [ ] Task 6: Register `DemoAudioEngine` in `audio/__init__.py`
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/core/audio/demo_engine.py` — New file: DemoAudioEngine + DemoAudioCaptureStream
|
||||
- `server/src/wled_controller/core/audio/__init__.py` — Register DemoAudioEngine
|
||||
|
||||
## Acceptance Criteria
|
||||
- `DemoAudioEngine.is_available()` is True only in demo mode
|
||||
- Virtual audio devices appear in audio device enumeration when in demo mode
|
||||
- `read_chunk()` returns valid float32 audio data that varies over time
|
||||
- Audio analyzer produces non-trivial frequency band data from the synthetic signal
|
||||
|
||||
## Notes
|
||||
- Synthetic audio should produce interesting FFT results (multiple frequencies, amplitude modulation)
|
||||
- Keep it computationally lightweight
|
||||
- Must conform to `AudioCaptureStreamBase` interface exactly
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in after completion -->
|
||||
@@ -1,54 +0,0 @@
|
||||
# Phase 4: Demo Device Provider & Seed Data
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Create a demo device provider that exposes discoverable virtual LED devices, and build a seed data generator that populates the demo data directory with sample entities on first run.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Create `server/src/wled_controller/core/devices/demo_provider.py` — `DemoDeviceProvider` extending `LEDDeviceProvider`:
|
||||
- `device_type = "demo"`
|
||||
- `capabilities = {"manual_led_count", "power_control", "brightness_control", "static_color"}`
|
||||
- `create_client()` returns a `MockClient` (reuse existing)
|
||||
- `discover()` returns 3 pre-defined virtual devices:
|
||||
- "Demo LED Strip" (60 LEDs, ip="demo-strip")
|
||||
- "Demo LED Matrix" (256 LEDs / 16×16, ip="demo-matrix")
|
||||
- "Demo LED Ring" (24 LEDs, ip="demo-ring")
|
||||
- `check_health()` always returns online with simulated ~2ms latency
|
||||
- `validate_device()` returns `{"led_count": <from url>}`
|
||||
- [ ] Task 2: Register `DemoDeviceProvider` in `led_client.py` `_register_builtin_providers()`
|
||||
- [ ] Task 3: Create `server/src/wled_controller/core/demo_seed.py` — seed data generator:
|
||||
- Function `seed_demo_data(storage_config: StorageConfig)` that checks if demo data dir is empty and populates it
|
||||
- Seed entities: 3 devices (matching discover results), 2 output targets, 2 picture sources (using demo engine), 2 CSS sources (gradient + color_cycle), 1 audio source (using demo engine), 1 scene preset, 1 automation
|
||||
- Use proper ID formats matching existing conventions (e.g., `dev_<hex>`, `tgt_<hex>`, etc.)
|
||||
- [ ] Task 4: Call `seed_demo_data()` during server startup in `main.py` when demo mode is active (before stores are loaded)
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/core/devices/demo_provider.py` — New: DemoDeviceProvider
|
||||
- `server/src/wled_controller/core/devices/led_client.py` — Register DemoDeviceProvider
|
||||
- `server/src/wled_controller/core/demo_seed.py` — New: seed data generator
|
||||
- `server/src/wled_controller/main.py` — Call seed on demo startup
|
||||
|
||||
## Acceptance Criteria
|
||||
- Demo devices appear in discovery results when in demo mode
|
||||
- Seed data populates `data/demo/` with valid JSON files on first demo run
|
||||
- Subsequent demo runs don't overwrite existing demo data
|
||||
- All seeded entities load correctly in stores
|
||||
|
||||
## Notes
|
||||
- Seed data must match the exact schema expected by each store (look at existing JSON files for format)
|
||||
- Use the entity dataclass `to_dict()` / store patterns to generate valid data
|
||||
- Demo discovery should NOT appear when not in demo mode
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in after completion -->
|
||||
@@ -1,50 +0,0 @@
|
||||
# Phase 5: Frontend Demo Indicator & Sandbox UX
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** fullstack
|
||||
|
||||
## Objective
|
||||
Add visual indicators in the frontend that clearly communicate demo mode status to the user, including a badge, dismissible banner, and engine labeling.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Add `demo_mode` field to system info API response schema (if not already done in Phase 1)
|
||||
- [ ] Task 2: In frontend initialization (`app.ts` or `state.ts`), fetch system info and store `demoMode` in app state
|
||||
- [ ] Task 3: Add `<span class="demo-badge" id="demo-badge" style="display:none">DEMO</span>` next to app title in `index.html` header
|
||||
- [ ] Task 4: CSS for `.demo-badge`: amber/yellow pill shape, subtle pulse animation, clearly visible but not distracting
|
||||
- [ ] Task 5: On app load, if `demoMode` is true: show badge, set `document.body.dataset.demo = 'true'`
|
||||
- [ ] Task 6: Add a dismissible demo banner at the top of the page: "You're in demo mode — all devices and data are virtual. No real hardware is used." with a dismiss (×) button. Store dismissal in localStorage.
|
||||
- [ ] Task 7: Add i18n keys for demo badge and banner text in `en.json`, `ru.json`, `zh.json`
|
||||
- [ ] Task 8: In engine/display dropdowns, demo engines should display with "Demo: " prefix for clarity
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/templates/index.html` — Demo badge + banner HTML
|
||||
- `server/src/wled_controller/static/css/app.css` — Demo badge + banner styles
|
||||
- `server/src/wled_controller/static/js/app.ts` — Demo mode detection and UI toggle
|
||||
- `server/src/wled_controller/static/js/core/state.ts` — Store demo mode flag
|
||||
- `server/src/wled_controller/static/locales/en.json` — i18n keys
|
||||
- `server/src/wled_controller/static/locales/ru.json` — i18n keys
|
||||
- `server/src/wled_controller/static/locales/zh.json` — i18n keys
|
||||
|
||||
## Acceptance Criteria
|
||||
- Demo badge visible next to "LED Grab" title when in demo mode
|
||||
- Demo badge hidden when not in demo mode
|
||||
- Banner appears on first demo visit, can be dismissed, stays dismissed across refreshes
|
||||
- Engine dropdowns clearly label demo engines
|
||||
- All text is localized
|
||||
|
||||
## Notes
|
||||
- Badge should use `--warning-color` or a custom amber for the pill
|
||||
- Banner should be a thin strip, not intrusive
|
||||
- `localStorage` key: `demo-banner-dismissed`
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in after completion -->
|
||||
@@ -1,46 +0,0 @@
|
||||
# Phase 6: Demo-only Engine Resolution
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Ensure demo engines are the primary/preferred engines in demo mode, and are hidden when not in demo mode. This makes demo mode act as a "virtual platform" where only demo engines resolve.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Modify `EngineRegistry.get_available_engines()` to filter out engines with `ENGINE_TYPE == "demo"` when not in demo mode (they report `is_available()=False` anyway, but belt-and-suspenders)
|
||||
- [ ] Task 2: Modify `AudioEngineRegistry.get_available_engines()` similarly
|
||||
- [ ] Task 3: In demo mode, `get_best_available_engine()` should return the demo engine (already handled by priority=1000, but verify)
|
||||
- [ ] Task 4: Modify the `GET /api/v1/config/displays` endpoint: in demo mode, default to demo engine displays if no engine_type specified
|
||||
- [ ] Task 5: Modify the audio engine listing endpoint similarly
|
||||
- [ ] Task 6: Ensure `DemoDeviceProvider.discover()` only returns devices when in demo mode
|
||||
- [ ] Task 7: End-to-end verification: start server in demo mode, verify only demo engines/devices appear in API responses
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/core/capture_engines/factory.py` — Filter demo engines
|
||||
- `server/src/wled_controller/core/audio/factory.py` — Filter demo engines
|
||||
- `server/src/wled_controller/api/routes/system.py` — Display endpoint defaults
|
||||
- `server/src/wled_controller/api/routes/audio_templates.py` — Audio engine listing
|
||||
- `server/src/wled_controller/core/devices/demo_provider.py` — Guard discover()
|
||||
|
||||
## Acceptance Criteria
|
||||
- In demo mode: demo engines are primary, real engines may also be listed but demo is default
|
||||
- Not in demo mode: demo engines are completely hidden from all API responses
|
||||
- Display list defaults to demo displays in demo mode
|
||||
- Audio device list defaults to demo devices in demo mode
|
||||
|
||||
## Notes
|
||||
- This is the "demo OS identifier" concept — demo mode acts as a virtual platform
|
||||
- Be careful not to break existing behavior when demo=False (default)
|
||||
- The demo engines already have `is_available() = is_demo_mode()`, so the main concern is UI defaults
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in after completion -->
|
||||
@@ -1,85 +0,0 @@
|
||||
# Feature Context: Game Integration
|
||||
|
||||
## Configuration
|
||||
- **Development mode:** Automated
|
||||
- **Execution mode:** Orchestrator
|
||||
- **Strategy:** Big Bang
|
||||
- **Build (frontend):** `cd server && npm run build`
|
||||
- **Test:** `cd server && py -3.13 -m pytest tests/ --no-cov -q`
|
||||
- **Lint:** `cd server && ruff check src/ tests/ --fix`
|
||||
- **TypeScript:** `cd server && npx tsc --noEmit`
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Not a Capture Engine
|
||||
Game events are discrete data points, not pixel buffers. The integration lives as a separate `core/game_integration/` module with two output channels into existing pipelines:
|
||||
1. `GameEventColorStripSource/Stream` — renders LED effects on game events
|
||||
2. `GameEventValueSource` — exposes game metrics as 0.0-1.0 scalars for parameter binding
|
||||
|
||||
### Standardized Event Vocabulary
|
||||
All adapters map game-specific data into universal event categories (health, kill, death, round_start, etc.). Users configure effects against categories, not game-specific IDs. This means a "health < 30% → flash red" config works across CS2, LoL, and any future game.
|
||||
|
||||
### Three Adapter Tiers
|
||||
1. **Built-in** — CS2 GSI, LoL Live Client, Dota 2 GSI, generic webhook (ship with app)
|
||||
2. **Community YAML** — declarative adapter files mapping JSON paths to standard events (no code)
|
||||
3. **Generic webhook** — fallback with UI-guided JSON path mapping
|
||||
|
||||
### Event Types
|
||||
- **Continuous** (health, armor, mana, ammo) → best for ValueSource (drive brightness/speed/color)
|
||||
- **Trigger** (kill, death, round_start, bomb_planted) → best for ColorStripStream (fire effects)
|
||||
|
||||
## Tech Stack Context
|
||||
|
||||
### Backend Patterns (from codebase analysis)
|
||||
- **Entity pattern**: dataclass model (`storage/`) + JSON/SQLite store (`storage/*_store.py`) + Pydantic schemas (`api/schemas/`) + routes (`api/routes/`)
|
||||
- **Store base**: `BaseSqliteStore[T]` with write-through cache, RLock, `_check_name_unique()`
|
||||
- **ID generation**: `f"{prefix}_{uuid.uuid4().hex[:8]}"`
|
||||
- **Events**: `fire_entity_event(entity_type, action, entity_id)` for UI invalidation
|
||||
- **ColorStripSource**: inheritance with `_SOURCE_TYPE_MAP` registry, `to_dict()`/`from_dict()`/`create_from_kwargs()`/`apply_update()`
|
||||
- **ColorStripStream**: `_SIMPLE_STREAM_MAP` in `color_strip_stream_manager.py`
|
||||
- **ValueSource**: similar inheritance with `_VALUE_SOURCE_MAP` registry
|
||||
- **NotificationColorStripStream**: closest analog — event-driven, deque + lock, 30 FPS render loop, double-buffered output
|
||||
- **Bindable**: `BindableFloat`/`BindableColor` for value source parameter binding
|
||||
- **Dependencies**: `init_dependencies()` in `dependencies.py`, FastAPI `Depends()`
|
||||
- **STORE_MAP**: in `api/routes/system.py` for backup/restore
|
||||
|
||||
### Frontend Patterns
|
||||
- Vanilla TypeScript modules, no framework
|
||||
- `CardSection` for entity lists with reconciliation
|
||||
- `Modal` base class with `snapshotValues()` for dirty check
|
||||
- `TreeNav` for sidebar navigation in Streams tab
|
||||
- `DataCache` for state management
|
||||
- `fetchWithAuth()` for all API calls
|
||||
- `t('key')` for i18n, keys in en.json/ru.json/zh.json
|
||||
- `fire_entity_event` → frontend listens for cache invalidation
|
||||
- Icons from `core/icons.ts` (SVG paths), NEVER emoji
|
||||
- `IconSelect` for predefined item grids, `EntitySelect` for entity references
|
||||
|
||||
## Current State
|
||||
Feature not yet started. Branch created, plan files written.
|
||||
|
||||
## Temporary Workarounds
|
||||
None yet.
|
||||
|
||||
## Cross-Phase Dependencies
|
||||
- Phase 2 depends on Phase 1 (EventBus, AdapterRegistry)
|
||||
- Phase 3 depends on Phase 1 (GameAdapter ABC, MappingAdapter)
|
||||
- Phase 4 depends on Phase 1 (EventBus) and Phase 2 (GameIntegrationStore for config)
|
||||
- Phase 5 depends on Phase 1 (EventBus) and Phase 2 (GameIntegrationStore)
|
||||
- Phase 6 depends on Phase 2 (API endpoints) and Phase 3 (adapter metadata)
|
||||
- Phase 7 depends on Phase 4 (CSS source type) and Phase 5 (value source type)
|
||||
- Phase 8 depends on all prior phases
|
||||
- **Phases 4 and 5 are independent** — can run in parallel
|
||||
|
||||
## Deferred Work
|
||||
None yet.
|
||||
|
||||
## Failed Approaches
|
||||
None yet.
|
||||
|
||||
## Review Findings Log
|
||||
None yet.
|
||||
|
||||
## Phase Execution Log
|
||||
| Phase | Agent Used | Test Writer | Parallel | Notes |
|
||||
|-------|-----------|-------------|----------|-------|
|
||||
@@ -1,53 +0,0 @@
|
||||
# Feature: Game Integration
|
||||
|
||||
**Branch:** `feature/game-integration`
|
||||
**Base branch:** `master`
|
||||
**Created:** 2026-03-30
|
||||
**Status:** 🟡 In Progress
|
||||
**Strategy:** Big Bang
|
||||
**Mode:** Automated
|
||||
**Execution:** Orchestrator
|
||||
|
||||
## Summary
|
||||
|
||||
A system that receives real-time events from games (CS2, LoL, Dota 2, etc.) and drives LED effects through the existing color strip and value source pipelines. Uses a standardized event vocabulary so users configure effects against universal categories (health, kill, death) rather than game-specific IDs. Supports built-in adapters, community YAML adapter files, and a generic webhook fallback.
|
||||
|
||||
## Build & Test Commands
|
||||
- **Build (frontend):** `cd server && npm run build`
|
||||
- **Test:** `cd server && py -3.13 -m pytest tests/ --no-cov -q`
|
||||
- **Lint:** `cd server && ruff check src/ tests/ --fix`
|
||||
- **TypeScript:** `cd server && npx tsc --noEmit`
|
||||
|
||||
## Phases
|
||||
|
||||
- [x] Phase 1: Core Event Bus & Adapter Framework [backend] → [subplan](./phase-1-event-bus.md)
|
||||
- [x] Phase 2: Storage & API — Game Integration Configs [backend] → [subplan](./phase-2-storage-api.md)
|
||||
- [x] Phase 3: Built-in Game Adapters [backend] → [subplan](./phase-3-adapters.md)
|
||||
- [x] Phase 4: GameEventColorStripStream [backend] → [subplan](./phase-4-css-stream.md)
|
||||
- [x] Phase 5: GameEventValueSource [backend] → [subplan](./phase-5-value-source.md)
|
||||
- [x] Phase 6: Frontend — Game Integration Management UI [frontend] → [subplan](./phase-6-frontend-management.md)
|
||||
- [x] Phase 7: Frontend — ColorStrip & ValueSource Game Bindings [frontend] → [subplan](./phase-7-frontend-bindings.md)
|
||||
- [x] Phase 8: Effect Presets & Polish [fullstack] → [subplan](./phase-8-presets-polish.md)
|
||||
|
||||
## Phase Progress Log
|
||||
|
||||
| Phase | Domain | Status | Review | Build | Committed |
|
||||
|-------|--------|--------|--------|-------|-----------|
|
||||
| Phase 1: Event Bus & Adapter Framework | backend | ✅ Done | ✅ | ⬜ | ⬜ |
|
||||
| Phase 2: Storage & API | backend | ✅ Done | ✅ | ⬜ | ⬜ |
|
||||
| Phase 3: Built-in Adapters | backend | ✅ Done | ✅ | ⬜ | ⬜ |
|
||||
| Phase 4: CSS Stream | backend | ✅ Done | ✅ | ⬜ | ⬜ |
|
||||
| Phase 5: Value Source | backend | ✅ Done | ✅ | ⬜ | ⬜ |
|
||||
| Phase 6: Frontend Management UI | frontend | ✅ Done | ✅ | ⬜ | ⬜ |
|
||||
| Phase 7: Frontend Bindings | frontend | ✅ Done | ✅ | ⬜ | ⬜ |
|
||||
| Phase 8: Presets & Polish | fullstack | ✅ Done | ✅ | ✅ | ⬜ |
|
||||
|
||||
## Parallel Phases
|
||||
|
||||
Phases 4 and 5 are independent (no shared files) — can run in parallel.
|
||||
|
||||
## Final Review
|
||||
- [ ] Comprehensive code review
|
||||
- [ ] Full build passes
|
||||
- [ ] Full test suite passes
|
||||
- [ ] Merged to `master`
|
||||
@@ -1,107 +0,0 @@
|
||||
# Phase 1: Core Event Bus & Adapter Framework
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Build the foundational event infrastructure: the standardized event model, in-process pub/sub bus, adapter ABC, adapter registry, and the YAML-driven mapping adapter. This phase creates the core abstractions that all subsequent phases depend on.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Task 1: Create `core/game_integration/__init__.py` package
|
||||
- [x] Task 2: Define standardized event vocabulary as an enum/constant module (`core/game_integration/events.py`)
|
||||
- Categories: health, armor, shield, mana, energy, ammo, gold, fuel, speed (continuous)
|
||||
- Categories: kill, death, assist, damage_taken, damage_dealt (combat triggers)
|
||||
- Categories: match_start, match_end, round_start, round_end (match flow triggers)
|
||||
- Categories: objective_captured, objective_lost, objective_progress (objective)
|
||||
- Categories: stunned, blinded, buffed, debuffed (status effects)
|
||||
- Categories: team_a, team_b (team affiliation)
|
||||
- Each event type has metadata: display name, category, value_type (continuous/trigger), default_range (min, max)
|
||||
- [x] Task 3: Create `GameEvent` frozen dataclass (`core/game_integration/events.py`)
|
||||
- Fields: `adapter_id`, `event_type` (str from vocabulary), `value` (float 0.0-1.0), `raw_data` (dict), `timestamp` (float, monotonic)
|
||||
- [x] Task 4: Create `GameEventBus` (`core/game_integration/event_bus.py`)
|
||||
- Thread-safe pub/sub with `threading.Lock`
|
||||
- `publish(event: GameEvent)` — dispatches to matching subscribers
|
||||
- `subscribe(event_type: str, callback)` → returns subscription ID
|
||||
- `subscribe_all(callback)` → receives all events (for diagnostics/live monitor)
|
||||
- `unsubscribe(subscription_id)`
|
||||
- `get_recent_events(limit=50)` → returns recent events deque for diagnostics
|
||||
- `get_stats()` → event counts per type, last event timestamp
|
||||
- Use `collections.deque(maxlen=100)` for recent events
|
||||
- [x] Task 5: Create `GameAdapter` ABC (`core/game_integration/base_adapter.py`)
|
||||
- Class attributes: `ADAPTER_TYPE`, `DISPLAY_NAME`, `GAME_NAME`, `SUPPORTED_EVENTS` (list of event type strings)
|
||||
- `parse_payload(cls, payload: dict, adapter_config: dict, prev_state: dict) -> tuple[list[GameEvent], dict]` — returns events + updated state (for diff detection)
|
||||
- `validate_auth(cls, headers: dict, payload: dict, adapter_config: dict) -> bool`
|
||||
- `get_config_schema(cls) -> dict` — returns JSON schema for adapter-specific config fields (for UI auto-generation)
|
||||
- `get_setup_instructions(cls) -> str` — returns markdown setup guide for the game
|
||||
- [x] Task 6: Create `AdapterRegistry` (`core/game_integration/adapter_registry.py`)
|
||||
- `register(adapter_class)`, `get_adapter(adapter_type)`, `get_all_adapters()`, `get_available_adapters()`
|
||||
- Follow `EngineRegistry` pattern
|
||||
- [x] Task 7: Create `MappingAdapter` (`core/game_integration/mapping_adapter.py`)
|
||||
- Parses community YAML adapter files into a generic adapter instance
|
||||
- YAML schema: adapter name, game name, protocol (webhook/poll), mappings list
|
||||
- Each mapping: `source_path` (JSONPath-like), `event` (standard type), `min`/`max` (for continuous), `trigger` mode (on_change/on_increase/on_decrease/on_value)
|
||||
- `load_adapter_from_yaml(path) -> GameAdapter` — factory function
|
||||
- `validate_adapter_yaml(data: dict) -> list[str]` — returns validation errors
|
||||
- [x] Task 8: Create `core/game_integration/adapters/__init__.py` package (empty, for Phase 3)
|
||||
- [x] Task 9: Write unit tests for GameEventBus (publish, subscribe, unsubscribe, thread safety)
|
||||
- [x] Task 10: Write unit tests for AdapterRegistry (register, get, duplicates)
|
||||
- [x] Task 11: Write unit tests for MappingAdapter (YAML parsing, payload translation, validation)
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/core/game_integration/__init__.py` — package init, re-exports
|
||||
- `server/src/wled_controller/core/game_integration/events.py` — GameEvent dataclass + event vocabulary
|
||||
- `server/src/wled_controller/core/game_integration/event_bus.py` — GameEventBus
|
||||
- `server/src/wled_controller/core/game_integration/base_adapter.py` — GameAdapter ABC
|
||||
- `server/src/wled_controller/core/game_integration/adapter_registry.py` — AdapterRegistry
|
||||
- `server/src/wled_controller/core/game_integration/mapping_adapter.py` — MappingAdapter + YAML loader
|
||||
- `server/src/wled_controller/core/game_integration/adapters/__init__.py` — empty package
|
||||
- `server/tests/core/test_game_event_bus.py` — EventBus tests
|
||||
- `server/tests/core/test_adapter_registry.py` — Registry tests
|
||||
- `server/tests/core/test_mapping_adapter.py` — MappingAdapter tests
|
||||
|
||||
## Acceptance Criteria
|
||||
- GameEvent is frozen/immutable with all required fields
|
||||
- Event vocabulary covers all standard categories with metadata
|
||||
- EventBus correctly dispatches events to type-specific and wildcard subscribers
|
||||
- EventBus is thread-safe (concurrent publish/subscribe doesn't crash)
|
||||
- AdapterRegistry follows EngineRegistry pattern faithfully
|
||||
- MappingAdapter can load a YAML file and translate a JSON payload into GameEvents
|
||||
- MappingAdapter validates YAML schema and reports errors clearly
|
||||
- All tests pass
|
||||
|
||||
## Notes
|
||||
- Use `time.monotonic()` for timestamps (not `time.time()`) — monotonic is immune to clock adjustments
|
||||
- Keep the event vocabulary extensible — new types should just be new entries, no code changes
|
||||
- The MappingAdapter is key for scaling to many games without writing Python code per game
|
||||
- `prev_state` parameter on `parse_payload` enables diff-based trigger detection (e.g. CS2 kills counter)
|
||||
|
||||
## Review Checklist
|
||||
- [x] All tasks completed
|
||||
- [x] Code follows project conventions (PEP 8, type annotations, immutability)
|
||||
- [x] No unintended side effects
|
||||
- [x] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
**Completed:** All 11 tasks implemented and tested. 55 tests pass, 0 failures. Ruff clean.
|
||||
|
||||
### What was built
|
||||
- `events.py` — `GameEvent` frozen dataclass + 23-type event vocabulary with `EventTypeMetadata` (category, value_type, default_range). Helper functions: `get_event_vocabulary()`, `get_event_metadata()`, `is_known_event_type()`.
|
||||
- `event_bus.py` — `GameEventBus` with thread-safe pub/sub. Type-specific and wildcard subscriptions, `deque(maxlen=100)` for recent events, per-type event counts. Callbacks invoked outside lock to prevent deadlocks.
|
||||
- `base_adapter.py` — `GameAdapter` ABC with `ADAPTER_TYPE`/`DISPLAY_NAME`/`GAME_NAME`/`SUPPORTED_EVENTS` class vars. Abstract methods: `parse_payload()`, `validate_auth()`. Default implementations: `get_config_schema()`, `get_setup_instructions()`.
|
||||
- `adapter_registry.py` — `AdapterRegistry` following `EngineRegistry` pattern (class-level dict, register/get/list/clear).
|
||||
- `mapping_adapter.py` — `MappingAdapter` (concrete, instance-based) + `load_adapter_from_yaml()` factory + `validate_adapter_yaml()` validator. Supports dot-notation JSON paths, 4 trigger modes (on_change/on_increase/on_decrease/on_value), value normalization to 0.0-1.0, header-based auth.
|
||||
- `adapters/__init__.py` — empty package ready for Phase 3 built-in adapters.
|
||||
- `__init__.py` — re-exports all public API.
|
||||
|
||||
### Key design decisions
|
||||
- `MappingAdapter` is instance-based (not classmethod-based like built-in adapters) because each YAML file creates a unique adapter with its own mappings. The `parse_payload`/`validate_auth` methods use `# type: ignore[override]` for the instance vs classmethod difference.
|
||||
- `prev_state` dict keys are `source_path` strings, values are the last numeric value seen. This enables diff-based trigger detection (e.g., CS2 kill counter increments).
|
||||
- Non-numeric values in payloads are treated as trigger events with value=1.0.
|
||||
|
||||
### What Phase 2 needs
|
||||
- `GameEventBus` instance should be created as a singleton in `dependencies.py` and injected via FastAPI `Depends()`.
|
||||
- `AdapterRegistry` is ready for built-in adapters to call `AdapterRegistry.register(MyAdapter)` at import time.
|
||||
- The `GameIntegrationStore` (Phase 2) will need to store adapter configs that include `adapter_id` fields matching what `parse_payload` expects in `adapter_config`.
|
||||
@@ -1,102 +0,0 @@
|
||||
# Phase 2: Storage & API — Game Integration Configs
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Create the persistence layer and REST API for managing game integration configurations. Users create/edit/delete game integration configs, and games POST events to an ingestion endpoint.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Task 1: Create `GameIntegrationConfig` dataclass (`storage/game_integration.py`)
|
||||
- Fields: id (gi_<8hex>), name, adapter_type, enabled, adapter_config (dict), event_mappings (list of EventMapping dicts), created_at, updated_at, description, tags
|
||||
- `EventMapping` dataclass: event_type, effect, color ([R,G,B]), duration_ms, intensity, priority
|
||||
- Standard to_dict/from_dict/create_from_kwargs/apply_update methods
|
||||
- [x] Task 2: Create `GameIntegrationStore` (`storage/game_integration_store.py`)
|
||||
- Extend BaseSqliteStore[GameIntegrationConfig]
|
||||
- CRUD methods: create_integration, update_integration, delete (inherited)
|
||||
- Name uniqueness validation
|
||||
- get_references() for cascade prevention
|
||||
- [x] Task 3: Create Pydantic schemas (`api/schemas/game_integration.py`)
|
||||
- GameIntegrationCreate, GameIntegrationUpdate, GameIntegrationResponse, GameIntegrationListResponse
|
||||
- EventMappingSchema
|
||||
- GameEventPayload (for ingestion endpoint)
|
||||
- AdapterInfoResponse, AdapterListResponse (for adapter metadata endpoint)
|
||||
- GameIntegrationStatusResponse (for diagnostics)
|
||||
- [x] Task 4: Create API routes (`api/routes/game_integration.py`)
|
||||
- GET /api/v1/game-integrations — list all configs
|
||||
- POST /api/v1/game-integrations — create config
|
||||
- GET /api/v1/game-integrations/{id} — get config
|
||||
- PUT /api/v1/game-integrations/{id} — update config
|
||||
- DELETE /api/v1/game-integrations/{id} — delete config
|
||||
- POST /api/v1/game-integrations/{id}/event — receive game event payload (ingestion)
|
||||
- GET /api/v1/game-integrations/{id}/status — connection status, last event, event count
|
||||
- GET /api/v1/game-integrations/{id}/events — recent events for debugging
|
||||
- GET /api/v1/game-adapters — list available adapter types + supported events + config schema
|
||||
- [x] Task 5: Wire into main.py — create store, register in dependencies, include router
|
||||
- [x] Task 6: Add to STORE_MAP in api/routes/system.py for backup/restore
|
||||
- N/A — backups are SQLite-level (full database snapshot). Added `game_integrations` to `_ENTITY_TABLES` in `database.py` which automatically includes it in backup/restore.
|
||||
- [x] Task 7: Add game_integrations_file to StorageConfig in config.py (if JSON-based) or ensure DB table
|
||||
- Added `"game_integrations"` to `_ENTITY_TABLES` in `database.py` — table auto-created on startup.
|
||||
- [x] Task 8: Event ingestion logic — parse payload through adapter, publish to EventBus, track per-integration state (for diff detection)
|
||||
- [x] Task 9: Write tests for store (CRUD, validation, uniqueness)
|
||||
- [x] Task 10: Write tests for API routes (create, list, update, delete, event ingestion)
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/storage/game_integration.py` — dataclass models
|
||||
- `server/src/wled_controller/storage/game_integration_store.py` — SQLite store
|
||||
- `server/src/wled_controller/api/schemas/game_integration.py` — Pydantic schemas
|
||||
- `server/src/wled_controller/api/routes/game_integration.py` — REST endpoints
|
||||
- `server/src/wled_controller/main.py` — wire store + router
|
||||
- `server/src/wled_controller/api/dependencies.py` — add get_game_integration_store
|
||||
- `server/src/wled_controller/api/routes/system.py` — add to STORE_MAP
|
||||
- `server/src/wled_controller/config.py` — add storage config (if needed)
|
||||
- `server/tests/storage/test_game_integration_store.py` — store tests
|
||||
- `server/tests/api/test_game_integration_routes.py` — route tests
|
||||
|
||||
## Acceptance Criteria
|
||||
- Full CRUD for game integration configs via REST API
|
||||
- Event ingestion endpoint parses payload through the correct adapter and publishes to EventBus
|
||||
- Adapter metadata endpoint returns available adapters with supported events and config schemas
|
||||
- Status endpoint shows last event time and event counts
|
||||
- Store validates name uniqueness
|
||||
- Included in backup/restore via STORE_MAP
|
||||
- All tests pass
|
||||
|
||||
## Notes
|
||||
- The ingestion endpoint must be low-latency — games send at tick rate (16-64 Hz)
|
||||
- Store per-integration prev_state dict in memory (not persisted) for diff-based trigger detection
|
||||
- Auth for ingestion endpoint: adapter-level auth (e.g. CS2 token) checked before standard API auth
|
||||
|
||||
## Review Checklist
|
||||
- [x] All tasks completed
|
||||
- [x] Code follows project conventions
|
||||
- [x] No unintended side effects
|
||||
- [x] Tests pass
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
**Completed:** All 10 tasks implemented and tested. 48 tests pass, 0 failures. Ruff clean.
|
||||
|
||||
### What was built
|
||||
- `storage/game_integration.py` — `EventMapping` and `GameIntegrationConfig` dataclasses with full `to_dict()`/`from_dict()`/`create_from_kwargs()`/`apply_update()` methods. `apply_update()` returns a new instance (immutable pattern).
|
||||
- `storage/game_integration_store.py` — `GameIntegrationStore` extending `BaseSqliteStore[GameIntegrationConfig]`. CRUD with name uniqueness, write-through caching. Aliases: `get_all_integrations`, `get_integration`, `delete_integration`.
|
||||
- `api/schemas/game_integration.py` — Full Pydantic schema suite: `EventMappingSchema`, `GameIntegrationCreate/Update/Response/ListResponse`, `GameEventPayload`, `AdapterInfoResponse/AdapterListResponse`, `GameIntegrationStatusResponse`, `GameEventResponse/RecentEventsResponse`.
|
||||
- `api/routes/game_integration.py` — 9 endpoints: full CRUD + event ingestion + status + recent events + adapter listing. Ingestion endpoint skips `AuthRequired` (uses adapter-level auth via `validate_auth()`). Per-integration runtime state tracked in-memory (`_prev_states`, `_integration_stats`) with thread-safe access.
|
||||
- `database.py` — Added `"game_integrations"` to `_ENTITY_TABLES` so the table is auto-created and included in database backups.
|
||||
- `api/dependencies.py` — Added `get_game_integration_store()` and `get_game_event_bus()` getters + `init_dependencies()` params.
|
||||
- `api/__init__.py` — Registered `game_integration_router`.
|
||||
- `main.py` — Creates `GameIntegrationStore(db)` and `GameEventBus()`, passes both to `init_dependencies()`.
|
||||
|
||||
### Key design decisions
|
||||
- **Ingestion auth**: The `/event` endpoint does NOT use `AuthRequired`. Instead, it calls `adapter_cls.validate_auth()` with request headers and adapter config. This allows game clients (CS2 GSI, etc.) to authenticate with their own token scheme without needing the app's API key.
|
||||
- **Runtime state**: Per-integration `prev_state` (for diff detection) and event stats are stored in module-level dicts with a threading lock. Not persisted — resets on server restart.
|
||||
- **Connected heuristic**: An integration is "connected" if it received an event within the last 30 seconds (based on monotonic time).
|
||||
- **Immutable updates**: `GameIntegrationConfig.apply_update()` returns a new instance; the store replaces the cache entry.
|
||||
|
||||
### What Phase 3+ needs
|
||||
- `GameEventBus` singleton is now available via `get_game_event_bus()` dependency — Phase 4/5 streams can subscribe to it.
|
||||
- `GameIntegrationStore` is available via `get_game_integration_store()` — Phase 6 frontend can call the CRUD API.
|
||||
- Built-in adapters (Phase 3) should call `AdapterRegistry.register(MyAdapter)` at import time to appear in `GET /api/v1/game-adapters`.
|
||||
- The `adapter_config` field on each integration stores adapter-specific secrets (e.g. CS2 auth token). Phase 3 adapters define their config schema via `get_config_schema()`.
|
||||
@@ -1,110 +0,0 @@
|
||||
# Phase 3: Built-in Game Adapters
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Implement built-in adapters for popular games and ship example community adapter YAML files. Each adapter translates a game's native data format into standardized GameEvents.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Task 1: CS2 Game State Integration adapter (`core/game_integration/adapters/cs2_adapter.py`)
|
||||
- Parse CS2 GSI JSON payload (player.state, round, map sections)
|
||||
- Events: player_health, player_armor, player_ammo, player_money, kill, death, round_start, round_end, bomb_planted, bomb_defused, flashbang, team
|
||||
- Auth: validate payload["auth"]["token"] against adapter_config["auth_token"]
|
||||
- Diff-based detection for kills (compare match_stats.kills with prev_state)
|
||||
- Setup instructions: how to create gamestate_integration_*.cfg in CS2
|
||||
- Config schema: auth_token (string)
|
||||
- [x] Task 2: Dota 2 Game State Integration adapter (`core/game_integration/adapters/dota2_adapter.py`)
|
||||
- Similar to CS2 GSI format but different payload structure
|
||||
- Events: player_health, player_mana, kill, death, match_start, match_end, gold
|
||||
- Auth: same pattern as CS2
|
||||
- Config schema: auth_token
|
||||
- [x] Task 3: League of Legends Live Client Data API adapter (`core/game_integration/adapters/lol_adapter.py`)
|
||||
- Poll-based: fetches from https://127.0.0.1:2999/liveclientdata/allgamedata
|
||||
- Events: player_health, player_mana, player_level, death, respawn, gold, game_time
|
||||
- Adapter manages its own polling thread (started/stopped with integration enable/disable)
|
||||
- Config schema: poll_interval_ms (int, default 500)
|
||||
- Note: LoL uses self-signed SSL cert — needs verify=False or custom cert handling
|
||||
- [x] Task 4: Generic webhook adapter (`core/game_integration/adapters/generic_webhook_adapter.py`)
|
||||
- User-defined JSON path mappings (configured in adapter_config)
|
||||
- Config schema: mappings list (same format as MappingAdapter YAML)
|
||||
- Effectively a MappingAdapter configured via API rather than YAML file
|
||||
- [x] Task 5: Register all built-in adapters in `core/game_integration/adapters/__init__.py`
|
||||
- [x] Task 6: Create example community adapter YAML files
|
||||
- `server/src/wled_controller/data/game_adapters/minecraft.yaml` — via webhook mod
|
||||
- `server/src/wled_controller/data/game_adapters/valorant.yaml` — via Overwolf/Insights API
|
||||
- `server/src/wled_controller/data/game_adapters/rocket_league.yaml` — via SOS plugin
|
||||
- [x] Task 7: Community adapter loader — scan data/game_adapters/ on startup, register as available
|
||||
- [x] Task 8: Write tests for CS2 adapter (payload parsing, auth validation, diff detection)
|
||||
- [x] Task 9: Write tests for Dota 2 adapter
|
||||
- [x] Task 10: Write tests for LoL adapter (mock HTTP responses)
|
||||
- [x] Task 11: Write tests for generic webhook adapter
|
||||
- [x] Task 12: Write tests for community YAML adapter loading
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/core/game_integration/adapters/cs2_adapter.py`
|
||||
- `server/src/wled_controller/core/game_integration/adapters/dota2_adapter.py`
|
||||
- `server/src/wled_controller/core/game_integration/adapters/lol_adapter.py`
|
||||
- `server/src/wled_controller/core/game_integration/adapters/generic_webhook_adapter.py`
|
||||
- `server/src/wled_controller/core/game_integration/adapters/__init__.py` — register all
|
||||
- `server/src/wled_controller/data/game_adapters/minecraft.yaml`
|
||||
- `server/src/wled_controller/data/game_adapters/valorant.yaml`
|
||||
- `server/src/wled_controller/data/game_adapters/rocket_league.yaml`
|
||||
- `server/tests/core/test_cs2_adapter.py`
|
||||
- `server/tests/core/test_dota2_adapter.py`
|
||||
- `server/tests/core/test_lol_adapter.py`
|
||||
- `server/tests/core/test_generic_webhook_adapter.py`
|
||||
- `server/tests/core/test_community_adapter_loader.py`
|
||||
|
||||
## Acceptance Criteria
|
||||
- CS2 adapter correctly parses real GSI payloads into standardized events
|
||||
- Dota 2 adapter handles its GSI format
|
||||
- LoL adapter can poll (mocked) and produce events
|
||||
- Generic webhook adapter translates arbitrary JSON using user-defined mappings
|
||||
- Community YAML files load and register correctly
|
||||
- Auth validation works per adapter
|
||||
- All tests pass with realistic payload samples
|
||||
|
||||
## Notes
|
||||
- Use real CS2/Dota2 GSI payload samples from documentation for tests
|
||||
- LoL polling thread must be stoppable (daemon thread or event flag)
|
||||
- Community adapter directory should be configurable (default: data/game_adapters/)
|
||||
- Generic webhook adapter reuses MappingAdapter logic from Phase 1
|
||||
|
||||
## Review Checklist
|
||||
- [x] All tasks completed
|
||||
- [x] Code follows project conventions
|
||||
- [x] No unintended side effects
|
||||
- [x] Tests pass
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
**Completed:** All 12 tasks implemented and tested. 92 tests pass, 0 failures. Ruff clean.
|
||||
|
||||
### What was built
|
||||
|
||||
- `adapters/cs2_adapter.py` — `CS2Adapter` parsing CS2 GSI payloads. Continuous events: health, armor, money (gold), ammo. Diff-based triggers: kill, death. Phase triggers: round_start, round_end, bomb_planted (objective_captured), bomb_defused (objective_lost). Flash detection (blinded). Team affiliation (team_a/team_b). Auth via `payload["auth"]["token"]`.
|
||||
- `adapters/dota2_adapter.py` — `Dota2Adapter` parsing Dota 2 GSI payloads. Continuous: health (hp/max_hp ratio), mana (mp/max_mp ratio), gold (configurable max). Diff-based: kill, death. Match flow: match_start (PRE_GAME/GAME_IN_PROGRESS), match_end (POST_GAME/DISCONNECT). Auth via `payload["auth"]["token"]`.
|
||||
- `adapters/lol_adapter.py` — `LoLAdapter` parsing LoL Live Client Data payloads + `LoLPoller` daemon thread for polling `https://127.0.0.1:2999/liveclientdata/allgamedata`. Continuous: health, mana, level (speed), gold. Triggers: death (health drops to 0), respawn (objective_progress). No auth (local-only API). Poller uses `threading.Event` for clean stop, `ssl.CERT_NONE` for self-signed cert.
|
||||
- `adapters/generic_webhook_adapter.py` — `GenericWebhookAdapter` delegating to `MappingAdapter` internally. User defines mappings in `adapter_config["mappings"]`. Auth via configurable header (default: Authorization with Bearer prefix support).
|
||||
- `adapters/__init__.py` — Registers all 4 built-in adapters with `AdapterRegistry` on import.
|
||||
- `community_loader.py` — Scans `data/game_adapters/` for `.yaml`/`.yml` files, loads them as `MappingAdapter` instances keyed as `community_<stem>`. Module-level registry with `register_community_adapters()`, `get_community_adapter()`, `get_community_adapter_info()`.
|
||||
- `data/game_adapters/minecraft.yaml` — Webhook-based, maps health/armor/food/XP/kills/deaths.
|
||||
- `data/game_adapters/valorant.yaml` — Webhook-based via Overwolf, maps health/shield/money/kills/deaths/round/spike.
|
||||
- `data/game_adapters/rocket_league.yaml` — Webhook-based via SOS plugin bridge, maps boost/speed/goals/time/teams.
|
||||
|
||||
### Key design decisions
|
||||
|
||||
- **CS2/Dota2 auth** uses `payload["auth"]["token"]` (not HTTP headers) — matches how Valve's GSI actually sends the token.
|
||||
- **LoL polling** is opt-in via `LoLPoller` class, not auto-started by the adapter. The integration manager (Phase 4+) should instantiate and manage poller lifecycle.
|
||||
- **Generic webhook** creates a transient `MappingAdapter` per `parse_payload` call. This is simple and stateless — the adapter_config is the source of truth. For high-frequency usage, caching the MappingAdapter instance could be a future optimization.
|
||||
- **Community adapters** are separate from `AdapterRegistry` (which holds class-based adapters). They live in `community_loader._community_adapters` since they're instance-based MappingAdapters.
|
||||
|
||||
### What Phase 4+ needs
|
||||
|
||||
- Import `wled_controller.core.game_integration.adapters` in `main.py` to trigger built-in adapter registration.
|
||||
- Call `register_community_adapters()` from `community_loader` during app startup.
|
||||
- The adapter listing endpoint (`GET /api/v1/game-adapters`) should also include `get_community_adapter_info()` results.
|
||||
- LoL polling needs lifecycle management — start `LoLPoller` when a LoL integration is enabled, stop when disabled.
|
||||
@@ -1,78 +0,0 @@
|
||||
# Phase 4: GameEventColorStripStream
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Create a new ColorStripSource/Stream type that renders LED effects in response to game events. Follows the NotificationColorStripStream pattern — event-driven with a 30 FPS render loop and double-buffered output.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Task 1: Create `GameEventColorStripSource` dataclass in `storage/color_strip_source.py`
|
||||
- Fields: game_integration_id (str), idle_color (BindableColor), event_mappings (list of EventMapping dicts — override/supplement config-level mappings)
|
||||
- source_type = "game_event"
|
||||
- Implement to_dict/from_dict/create_from_kwargs/apply_update
|
||||
- sharable = False (each target gets its own stream)
|
||||
- [x] Task 2: Register "game_event" in `_SOURCE_TYPE_MAP` in color_strip_source.py
|
||||
- [x] Task 3: Create `GameEventColorStripStream` (`core/processing/game_event_stream.py`)
|
||||
- Constructor: parse event_mappings into lookup dict, subscribe to EventBus
|
||||
- `_on_game_event(event)` — callback from EventBus, enqueue effect (thread-safe via deque)
|
||||
- Effect types: flash, pulse, sweep, color_shift, breathing
|
||||
- Priority-based layering — higher priority effects override lower (same as notification)
|
||||
- 30 FPS background render thread with frame_time sleep
|
||||
- Double-buffered output under threading.Lock
|
||||
- Idle: output idle_color when no active effects
|
||||
- `get_latest_colors()` → returns current rendered frame (np.ndarray)
|
||||
- `start()` / `stop()` for lifecycle
|
||||
- Cleanup: unsubscribe from EventBus on stop
|
||||
- [x] Task 4: Register in `_SIMPLE_STREAM_MAP` in `color_strip_stream_manager.py`
|
||||
- [x] Task 5: Wire EventBus injection — stream needs access to the singleton EventBus
|
||||
- Added `game_event_bus` parameter to ColorStripStreamManager constructor
|
||||
- Injection via `set_event_bus()` using same hasattr pattern as asset_store
|
||||
- [x] Task 6: Write tests for GameEventColorStripSource (serialization, factory, update)
|
||||
- [x] Task 7: Write tests for GameEventColorStripStream (event → effect rendering, priority, idle state, lifecycle)
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/storage/color_strip_source.py` — add GameEventColorStripSource + register
|
||||
- `server/src/wled_controller/core/processing/game_event_stream.py` — new stream class
|
||||
- `server/src/wled_controller/core/processing/color_strip_stream_manager.py` — register in _SIMPLE_STREAM_MAP, inject EventBus
|
||||
- `server/src/wled_controller/core/processing/processor_manager.py` — pass EventBus to stream manager (if needed)
|
||||
- `server/tests/core/test_game_event_css.py` — source + stream tests
|
||||
|
||||
## Acceptance Criteria
|
||||
- "game_event" source type serializes/deserializes correctly
|
||||
- Stream subscribes to EventBus and renders effects when events arrive
|
||||
- Multiple simultaneous effects layer by priority
|
||||
- Idle state outputs the configured idle_color
|
||||
- Stream cleans up subscriptions on stop
|
||||
- All tests pass
|
||||
|
||||
## Notes
|
||||
- Reuse effect rendering logic from NotificationColorStripStream where possible (flash, pulse, sweep are identical)
|
||||
- Consider extracting shared effect rendering into a utility if duplication is significant
|
||||
- The stream needs the EventBus singleton — simplest injection is via the stream manager's dependencies
|
||||
- ⚠️ Big Bang: color_strip_source.py and stream_manager.py are shared files — coordinate with Phase 5 if running in parallel
|
||||
|
||||
## Review Checklist
|
||||
- [x] All tasks completed
|
||||
- [x] Code follows project conventions
|
||||
- [x] No unintended side effects
|
||||
- [x] Tests pass (28/28)
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
### What was built
|
||||
- `GameEventColorStripSource` dataclass in `storage/color_strip_source.py` with full serialization, factory, and update support
|
||||
- `GameEventColorStripStream` in `core/processing/game_event_stream.py` — event-driven stream with 5 effects (flash, pulse, sweep, color_shift, breathing), 30 FPS render loop, double-buffered output, priority-based effect layering
|
||||
- EventBus injection via `game_event_bus` parameter on `ColorStripStreamManager` constructor + `set_event_bus()` method on the stream
|
||||
|
||||
### Integration points for later phases
|
||||
- **Phase 7 (frontend)**: The `source_type = "game_event"` needs a UI editor. Fields: `game_integration_id` (EntitySelect), `idle_color` (BindableColor picker), `event_mappings` (list of EventMapping dicts with effect/color/duration/intensity/priority), `led_count`
|
||||
- **Phase 8 (wiring)**: `processor_manager.py` needs to pass the `GameEventBus` singleton to `ColorStripStreamManager` via the new `game_event_bus=` constructor parameter. The bus is created in Phase 1's `init_dependencies()`.
|
||||
|
||||
### Files modified
|
||||
- `server/src/wled_controller/storage/color_strip_source.py` — added `GameEventColorStripSource` class + registered in `_SOURCE_TYPE_MAP`
|
||||
- `server/src/wled_controller/core/processing/game_event_stream.py` — new file
|
||||
- `server/src/wled_controller/core/processing/color_strip_stream_manager.py` — added import, `_SIMPLE_STREAM_MAP` entry, `game_event_bus` constructor param, injection hook
|
||||
- `server/tests/core/test_game_event_css.py` — 28 tests covering source serialization, stream lifecycle, rendering, effects, auto-size, and hot-update
|
||||
@@ -1,69 +0,0 @@
|
||||
# Phase 5: GameEventValueSource
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Create a new ValueSource type that exposes game metrics (health, ammo, mana, etc.) as 0.0-1.0 scalar values. These can be bound to any existing effect parameter (brightness, speed, color position) via the BindableFloat/BindableColor system.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Task 1: Create `GameEventValueSource` dataclass in `storage/value_source.py`
|
||||
- Fields: game_integration_id (str), event_type (str — from standard vocabulary), min_game_value (float, default 0.0), max_game_value (float, default 100.0), smoothing (float, 0.0-1.0, EMA alpha), default_value (float, 0.5), timeout (float, seconds before reverting to default)
|
||||
- source_type = "game_event"
|
||||
- Implement to_dict/from_dict
|
||||
- [x] Task 2: Register "game_event" in `_VALUE_SOURCE_MAP` in value_source.py
|
||||
- [x] Task 3: Create `GameEventValueStream` runtime resolver (`core/value_sources/game_event_value_source.py`)
|
||||
- Subscribe to EventBus for the configured event_type
|
||||
- Normalize incoming value using min/max mapping → 0.0-1.0
|
||||
- Apply EMA smoothing if configured
|
||||
- Track last_event_time for timeout detection
|
||||
- `get_value()` → returns current normalized value (or default if timed out)
|
||||
- `get_color()` → returns None (game event value source only provides scalars)
|
||||
- Thread-safe (EventBus callback + get_value from render thread)
|
||||
- Cleanup: unsubscribe from EventBus on release
|
||||
- [x] Task 4: Register in value source manager / factory
|
||||
- Add case for "game_event" source_type in the value stream creation logic
|
||||
- Inject EventBus reference
|
||||
- [x] Task 5: Write tests for GameEventValueSource (serialization, from_dict, defaults)
|
||||
- [x] Task 6: Write tests for GameEventValueStream (normalization, smoothing, timeout, thread safety)
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/storage/value_source.py` — add GameEventValueSource + register
|
||||
- `server/src/wled_controller/core/value_sources/game_event_value_source.py` — runtime resolver
|
||||
- `server/src/wled_controller/core/processing/value_stream.py` — register factory case + event_bus param
|
||||
- `server/tests/core/test_game_event_value_source.py` — tests
|
||||
|
||||
## Acceptance Criteria
|
||||
- "game_event" value source type serializes/deserializes correctly
|
||||
- Runtime resolver normalizes game values to 0.0-1.0 using min/max
|
||||
- EMA smoothing works correctly (smooth transitions, not jumpy)
|
||||
- Timeout reverts to default_value when no events received
|
||||
- Can be bound to BindableFloat properties on any ColorStripSource
|
||||
- All tests pass
|
||||
|
||||
## Notes
|
||||
- EMA formula: `smoothed = alpha * new_value + (1 - alpha) * smoothed` where alpha = 1 - smoothing
|
||||
- Timeout uses monotonic clock — compare current time vs last_event_time
|
||||
- Best suited for continuous event types (health, mana, ammo) — triggers (kill, death) are less useful here
|
||||
- ⚠️ Big Bang: value_source.py is a shared file — coordinate with Phase 4 if running in parallel
|
||||
|
||||
## Review Checklist
|
||||
- [x] All tasks completed
|
||||
- [x] Code follows project conventions
|
||||
- [x] No unintended side effects
|
||||
- [x] Tests pass (24/24)
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
### What was implemented
|
||||
- **GameEventValueSource** dataclass in `storage/value_source.py` with all required fields (game_integration_id, event_type, min/max mapping, smoothing, default_value, timeout)
|
||||
- **GameEventValueStream** in new module `core/value_sources/game_event_value_source.py` — subscribes to GameEventBus, normalizes values, applies EMA smoothing, handles timeout with monotonic clock, thread-safe via threading.Lock
|
||||
- **ValueStreamManager** updated with `event_bus` parameter and factory case for `GameEventValueSource` in `core/processing/value_stream.py`
|
||||
- **24 tests** covering serialization, normalization (7 cases), smoothing (3 cases), timeout (4 cases), lifecycle (4 cases), thread safety, and hot-update
|
||||
|
||||
### Integration notes for downstream phases
|
||||
- `ValueStreamManager.__init__` now accepts an optional `event_bus: GameEventBus` parameter — Phase 8 (wiring) needs to pass the EventBus instance when constructing ValueStreamManager in `dependencies.py`
|
||||
- New module `core/value_sources/` created — contains `__init__.py` and `game_event_value_source.py`
|
||||
- No changes to API schemas or routes — Phase 7 (frontend) will need to add "game_event" to the value source editor UI
|
||||
@@ -1,123 +0,0 @@
|
||||
# Phase 6: Frontend — Game Integration Management UI
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** frontend
|
||||
|
||||
## Objective
|
||||
Build the UI for creating, configuring, and monitoring game integrations. Users pick a game from a visual grid, configure adapter settings with guided instructions, set up event-to-effect mappings visually, and monitor live events.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Task 1: Add "Game" tab/group to Streams tree navigation in streams.ts
|
||||
- New tree group with game controller icon
|
||||
- Children: game integrations list, game adapters info
|
||||
- [x] Task 2: Create game integration cards in the Streams tab
|
||||
- CardSection instance for game integrations
|
||||
- Card shows: game name/icon, adapter type badge, status indicator (connected/waiting/error), last event timestamp, event count
|
||||
- Status indicator: green dot = events received recently, yellow = waiting, red = error/timeout
|
||||
- [x] Task 3: Create game integration editor modal (`templates/modals/game-integration-editor.html`)
|
||||
- Step 1: Game picker — searchable grid of available adapters with game icons (IconSelect pattern)
|
||||
- Step 2: Adapter config — auto-generated fields from adapter's config_schema (text inputs for auth tokens, number inputs for intervals)
|
||||
- Step 3: Setup instructions — per-game markdown instructions (e.g. CS2 cfg file content)
|
||||
- Step 4: Event mapping editor — visual grid of standard event categories
|
||||
- Each mapping row: event type (dropdown), effect type (IconSelect: flash/pulse/sweep/color_shift/breathing), color picker, duration slider, intensity slider, priority
|
||||
- Effect preset selector at top of mapping editor (dropdown: "FPS Combat", "MOBA Health", etc.)
|
||||
- Name, description, tags fields
|
||||
- [x] Task 4: Create TypeScript module `static/js/features/game-integration.ts`
|
||||
- CRUD functions using fetchWithAuth
|
||||
- Cache for game integrations data
|
||||
- Cache for game adapters metadata
|
||||
- Card rendering functions
|
||||
- Modal open/save/delete handlers
|
||||
- Event mapping editor logic (add/remove/reorder mappings)
|
||||
- [x] Task 5: Game adapter icons — add SVG icons for supported games in `core/icons.ts`
|
||||
- Generic gamepad icon for unknown games
|
||||
- Stylized icons for CS2, LoL, Dota 2, Minecraft, Valorant, Rocket League
|
||||
- [x] Task 6: Live event monitor panel
|
||||
- Expandable panel on the game integration card (or modal tab)
|
||||
- Shows real-time feed of incoming events: timestamp, event_type, value, color-coded by category
|
||||
- Fetches from GET /api/v1/game-integrations/{id}/events (polling every 2s or WebSocket later)
|
||||
- Useful for debugging: "is my game sending data?"
|
||||
- [x] Task 7: Connection test button
|
||||
- Button in modal that opens a test panel showing "Waiting for events..."
|
||||
- When first event arrives, shows success with event details
|
||||
- Helps users verify their game config is correct
|
||||
- [x] Task 8: Add i18n keys for all new strings (en.json, ru.json, zh.json)
|
||||
- Game integration section titles, modal labels, status messages, event category names, effect type names, error messages
|
||||
- [x] Task 9: CSS styles for game integration cards and modal (`static/css/game-integration.css`)
|
||||
- Game picker grid layout
|
||||
- Event mapping editor rows
|
||||
- Status indicators (colored dots)
|
||||
- Live event monitor feed
|
||||
- [x] Task 10: Wire into app.ts — import module, add to window exports for onclick handlers
|
||||
- [x] Task 11: Wire into streams.ts — add cache, load function, CardSection, tree nav integration
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/static/js/features/game-integration.ts` — main module
|
||||
- `server/src/wled_controller/static/js/features/streams.ts` — add game tab, cache, card section
|
||||
- `server/src/wled_controller/static/js/app.ts` — import and window exports
|
||||
- `server/src/wled_controller/static/js/core/icons.ts` — game icons
|
||||
- `server/src/wled_controller/static/js/types.ts` — TypeScript types for game integration
|
||||
- `server/src/wled_controller/static/css/game-integration.css` — styles
|
||||
- `server/src/wled_controller/templates/modals/game-integration-editor.html` — modal template
|
||||
- `server/src/wled_controller/templates/index.html` — include modal template
|
||||
- `server/src/wled_controller/static/locales/en.json` — i18n keys
|
||||
- `server/src/wled_controller/static/locales/ru.json` — i18n keys
|
||||
- `server/src/wled_controller/static/locales/zh.json` — i18n keys
|
||||
|
||||
## Acceptance Criteria
|
||||
- Game integration tab appears in Streams tree navigation
|
||||
- Cards display with correct status indicators and game icons
|
||||
- Modal wizard guides user through game selection → config → mapping
|
||||
- Adapter-specific config fields are auto-generated from schema
|
||||
- Setup instructions display per game
|
||||
- Event mapping editor allows visual configuration with effect previews
|
||||
- Effect presets populate mapping editor with sensible defaults
|
||||
- Live event monitor shows incoming events
|
||||
- Connection test provides clear feedback
|
||||
- All i18n keys present in all 3 languages
|
||||
- UI follows existing project conventions (no emoji, SVG icons, IconSelect/EntitySelect)
|
||||
|
||||
## Notes
|
||||
- Follow existing modal patterns: Modal base class, snapshotValues() for dirty check
|
||||
- Follow CardSection pattern for entity list with reconciliation
|
||||
- Use fetchWithAuth for ALL API calls
|
||||
- Icons must be SVG paths in icons.ts — NEVER emoji
|
||||
- Use IconSelect for game picker and effect type selector
|
||||
- Use EntitySelect for game_integration_id references
|
||||
- The event mapping editor is the most complex UI piece — consider a sub-component approach
|
||||
|
||||
## Review Checklist
|
||||
- [x] All tasks completed
|
||||
- [x] Code follows frontend conventions
|
||||
- [x] No unintended side effects
|
||||
- [x] TypeScript compiles without errors (only pre-existing SystemMetricsValueSource error remains)
|
||||
- [x] Bundle builds successfully
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
All 11 tasks implemented. Key implementation details:
|
||||
|
||||
**Files created:**
|
||||
- `server/src/wled_controller/static/js/features/game-integration.ts` — main module (CRUD, cards, modal handlers, event monitor, connection test)
|
||||
- `server/src/wled_controller/static/css/game-integration.css` — styles for mapping editor, event feed, status indicators, connection test panel
|
||||
- `server/src/wled_controller/templates/modals/game-integration-editor.html` — modal with adapter picker, config fields, mapping editor, live events, connection test
|
||||
|
||||
**Files modified:**
|
||||
- `icon-paths.ts` — added gamepad2, crosshair, swords, shield, pickaxe, rocketIcon, circleDot
|
||||
- `icons.ts` — added ICON_GAMEPAD, ICON_CROSSHAIR, ICON_SWORDS, ICON_SHIELD, ICON_PICKAXE, ICON_ROCKET_ICON, ICON_CIRCLE_DOT, getGameAdapterIcon()
|
||||
- `types.ts` — added GameIntegration, GameAdapterInfo, GameEventMapping, GameEventRecord, GameIntegrationStatus
|
||||
- `state.ts` — added gameIntegrationsCache, gameAdaptersCache, _cachedGameIntegrations, _cachedGameAdapters
|
||||
- `streams.ts` — added game tab to tree nav (under Integrations group), CardSection, cache fetching, reconciliation
|
||||
- `app.ts` — imported and wired all game integration functions to window
|
||||
- `global.d.ts` — added window type declarations for game integration functions
|
||||
- `index.html` — included game-integration-editor.html modal
|
||||
- `all.css` — imported game-integration.css
|
||||
- `en.json`, `ru.json`, `zh.json` — added ~50 i18n keys each
|
||||
|
||||
**API endpoints consumed:** GET/POST/PUT/DELETE /game-integrations, GET /game-adapters, GET /game-integrations/{id}/events, GET /game-integrations/{id}/status
|
||||
|
||||
**Conventions followed:** No emoji (SVG icons only), fetchWithAuth for all API calls, IconSelect for adapter picker, Modal subclass with snapshotValues() dirty check, TagInput for tags, CardSection with reconciliation, cache.invalidate() before reload, all strings via t() with i18n keys in 3 locales.
|
||||
|
||||
**Note:** The mapping editor uses plain `<select>` for event_type (since the available events are dynamic per adapter and may be unknown user-defined strings). Effect type selector has a select element that could be upgraded to IconSelect in a follow-up if desired. The preset selector is intentionally a plain select since it is a simple action trigger, not a form value.
|
||||
@@ -1,79 +0,0 @@
|
||||
# Phase 7: Frontend — ColorStrip & ValueSource Game Bindings
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** frontend
|
||||
|
||||
## Objective
|
||||
Add "game_event" as a selectable source type in the ColorStrip and ValueSource editors so users can create game-driven LED streams and parameter bindings from the existing entity UIs.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Task 1: Add "game_event" to ColorStripSource type selector (IconSelect)
|
||||
- New icon + label in the source type grid
|
||||
- When selected, show game integration picker (EntitySelect) and event mapping editor
|
||||
- [x] Task 2: Game integration picker in CSS editor
|
||||
- EntitySelect dropdown listing available game integrations
|
||||
- When selected, show the integration's supported events
|
||||
- [x] Task 3: Inline event mapping UI in CSS source editor
|
||||
- Simplified version of the full mapping editor from Phase 6
|
||||
- Allows override/supplement of the integration-level mappings
|
||||
- Idle color picker
|
||||
- [x] Task 4: Add "game_event" to ValueSource type selector
|
||||
- New icon + label in the value source type grid
|
||||
- [x] Task 5: Game value source config fields
|
||||
- Game integration picker (EntitySelect)
|
||||
- Event type picker (dropdown of continuous events from the selected integration)
|
||||
- Min/max game value inputs
|
||||
- Smoothing slider (0.0-1.0)
|
||||
- Default value input
|
||||
- Timeout input (seconds)
|
||||
- [x] Task 6: Add i18n keys for new source type labels and config fields
|
||||
- [x] Task 7: Update TypeScript types for new source types
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/static/js/features/streams.ts` — CSS editor game_event fields
|
||||
- `server/src/wled_controller/static/js/features/game-integration.ts` — shared helpers
|
||||
- `server/src/wled_controller/static/js/types.ts` — type definitions
|
||||
- `server/src/wled_controller/templates/modals/` — update CSS and value source modals
|
||||
- `server/src/wled_controller/static/locales/en.json`
|
||||
- `server/src/wled_controller/static/locales/ru.json`
|
||||
- `server/src/wled_controller/static/locales/zh.json`
|
||||
|
||||
## Acceptance Criteria
|
||||
- "game_event" appears in both source type selectors with appropriate icon
|
||||
- Selecting it shows game integration picker and relevant config fields
|
||||
- CSS editor shows event mapping override UI
|
||||
- Value source editor shows normalization and smoothing controls
|
||||
- All i18n keys present
|
||||
|
||||
## Notes
|
||||
- Depends on Phase 4 (CSS source type exists) and Phase 5 (value source type exists)
|
||||
- Reuse game-integration.ts helpers for adapter/event metadata fetching
|
||||
|
||||
## Review Checklist
|
||||
- [x] All tasks completed
|
||||
- [x] Code follows frontend conventions
|
||||
- [x] TypeScript compiles without errors
|
||||
- [x] Bundle builds successfully
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
All 7 tasks implemented. Key implementation details:
|
||||
|
||||
**Files modified:**
|
||||
- `icons.ts` — added `game_event` to both `_colorStripTypeIcons` and `_valueSourceTypeIcons` maps (gamepad2 icon)
|
||||
- `types.ts` — added `game_event` to `CSSSourceType` and `ValueSourceType` unions, added `GameEventValueSource` interface, added game event fields to `ColorStripSource` interface, also fixed pre-existing `system_metrics` missing from `ValueSourceType`
|
||||
- `color-strips.ts` — added `game_event` to `CSS_TYPE_KEYS`, `CSS_SECTION_MAP`, `CSS_TYPE_SETUP`, and `_typeHandlers`; added helper functions for game integration EntitySelect dropdown, idle color BindableColorWidget, inline event mapping editor (add/remove/preset), and game mapping collection for save
|
||||
- `value-sources.ts` — added `game_event` to `VS_FLOAT_TYPE_KEYS`; added section toggle in `onValueSourceTypeChange`; added loading/save logic for game event fields; added EntitySelect for game integration picker with filtered continuous event type dropdown
|
||||
- `css-editor.html` — added `css-editor-game-event-section` with game integration select, idle color container, mapping preset select, inline mapping editor, and add mapping button
|
||||
- `value-source-editor.html` — added `value-source-game-event-section` with game integration select, event type select (continuous events), min/max game value inputs, smoothing/default/timeout sliders
|
||||
- `app.ts` — imported and wired `addCSSGameMapping`, `removeCSSGameMapping`, `onCSSGameMappingPresetChange` to window
|
||||
- `global.d.ts` — added window type declarations for the 3 new exported functions
|
||||
- `en.json`, `ru.json`, `zh.json` — added ~28 i18n keys each for CSS and value source game_event labels, hints, and errors
|
||||
|
||||
**UI behavior:**
|
||||
- CSS editor: selecting "Game Event" type shows a game integration EntitySelect, BindableColor idle color picker, and an inline event mapping editor with preset loading (FPS Combat / MOBA Health), add/remove mapping rows (event type, effect type, color, duration, intensity, priority)
|
||||
- Value source editor: selecting "Game Event" type shows a game integration EntitySelect, event type dropdown (filtered to continuous events from the adapter's supported_events), min/max game value number inputs, smoothing slider, default value slider, timeout slider
|
||||
|
||||
**Conventions followed:** No emoji (SVG icons only), EntitySelect for game integration picker, BindableColorWidget for idle color, fetchWithAuth, cache.invalidate() + reload pattern, all strings via t() with i18n keys in 3 locales, dirty check via snapshotValues(), widget cleanup in onForceClose().
|
||||
@@ -1,84 +0,0 @@
|
||||
# Phase 8: Effect Presets & Polish
|
||||
|
||||
**Status:** ✅ Complete
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** fullstack
|
||||
|
||||
## Objective
|
||||
Ship built-in effect presets, add a WebSocket endpoint for real-time event streaming to the frontend live monitor, and add a setup wizard for guided game configuration.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Task 1: Define effect presets as data (`core/game_integration/presets.py`)
|
||||
- "FPS Combat": health→red glow, kill→green flash, death→full red pulse, round_start→team color sweep
|
||||
- "MOBA Health": health→gradient green-yellow-red, mana→blue glow, death→fade to black
|
||||
- "Racing": speed→color temperature, boost→rainbow flash
|
||||
- "Generic Alert": any trigger→white flash
|
||||
- Each preset: name, description, target_game_types (fps/moba/racing/any), event_mappings list
|
||||
- [x] Task 2: Preset API endpoints
|
||||
- GET /api/v1/game-integrations/presets — list available presets
|
||||
- POST /api/v1/game-integrations/{id}/apply-preset — apply preset to integration
|
||||
- [ ] Task 3: WebSocket endpoint for live event streaming — DEFERRED (polling works; WebSocket is a future optimization)
|
||||
- [x] Task 4: Frontend preset selector in modal
|
||||
- Dropdown of presets loaded from API with descriptions
|
||||
- "Apply" populates the event mapping editor
|
||||
- [ ] Task 5: Game adapter setup wizard — DEFERRED (setup instructions already shown per-adapter)
|
||||
- [ ] Task 6: Community adapter import UI — DEFERRED (community adapters are loaded from data/game_adapters/ dir on startup; file upload is a future enhancement)
|
||||
- [x] Task 7: Final integration testing — wiring verified, all 606 tests pass
|
||||
- [ ] Task 8: Documentation for creating community adapters — DEFERRED (not blocking release)
|
||||
- [x] Task 9 (added): Wiring fixes — game_event_bus passed through ProcessorDependencies to ColorStripStreamManager and ValueStreamManager
|
||||
- [x] Task 10 (added): Adapter registration — import built-in adapters and call register_community_adapters() in main.py
|
||||
|
||||
## Files to Modify/Create
|
||||
- `server/src/wled_controller/core/game_integration/presets.py`
|
||||
- `server/src/wled_controller/api/routes/game_integration.py` — preset + WS endpoints
|
||||
- `server/src/wled_controller/api/schemas/game_integration.py` — preset schemas
|
||||
- `server/src/wled_controller/static/js/features/game-integration.ts` — preset UI, wizard, import
|
||||
- `server/src/wled_controller/templates/modals/game-integration-editor.html` — wizard steps
|
||||
- `server/src/wled_controller/static/css/game-integration.css` — wizard styles
|
||||
- `server/src/wled_controller/static/locales/en.json`
|
||||
- `server/src/wled_controller/static/locales/ru.json`
|
||||
- `server/src/wled_controller/static/locales/zh.json`
|
||||
- `server/tests/core/test_game_presets.py`
|
||||
|
||||
## Acceptance Criteria
|
||||
- At least 4 effect presets ship out of the box
|
||||
- Presets can be applied to any game integration via API and UI
|
||||
- WebSocket endpoint streams events in real-time
|
||||
- Setup wizard provides clear per-game instructions
|
||||
- Community adapter import works (file upload + URL)
|
||||
- Full pipeline works end-to-end: game event → LED effect
|
||||
|
||||
## Notes
|
||||
- WebSocket uses FastAPI's built-in WebSocket support
|
||||
- Presets are read-only built-in data, not user-editable (users can modify after applying)
|
||||
- Setup wizard should pre-fill the server URL for webhook-based games
|
||||
|
||||
## Review Checklist
|
||||
- [x] All core tasks completed (presets, API, wiring, frontend, tests)
|
||||
- [x] Code follows project conventions
|
||||
- [x] Build passes (frontend + backend)
|
||||
- [x] All 606 tests pass
|
||||
- [x] Linting clean (ruff)
|
||||
- [x] TypeScript clean (tsc --noEmit)
|
||||
|
||||
## Handoff Notes (Final Phase)
|
||||
|
||||
**Files created:**
|
||||
- `server/src/wled_controller/core/game_integration/presets.py` — 4 built-in effect presets (FPS Combat, MOBA Health, Racing, Generic Alert) as frozen dataclasses
|
||||
- `server/tests/core/test_game_presets.py` — preset data structure tests (10 tests)
|
||||
- `server/tests/core/test_game_wiring.py` — GameEventBus wiring verification tests (4 tests)
|
||||
|
||||
**Files modified:**
|
||||
- `server/src/wled_controller/api/schemas/game_integration.py` — added EffectPresetResponse, PresetListResponse, ApplyPresetRequest schemas
|
||||
- `server/src/wled_controller/api/routes/game_integration.py` — added GET /presets and POST /{id}/apply-preset endpoints (presets route placed BEFORE {integration_id} to avoid path parameter conflict)
|
||||
- `server/src/wled_controller/core/processing/processor_manager.py` — added game_event_bus to ProcessorDependencies; wired to ColorStripStreamManager and ValueStreamManager
|
||||
- `server/src/wled_controller/main.py` — added adapter import, register_community_adapters() call, game_event_bus in ProcessorDependencies
|
||||
- `server/src/wled_controller/static/js/features/game-integration.ts` — replaced hardcoded preset data with API-loaded presets from /game-integrations/presets
|
||||
- `server/src/wled_controller/static/js/types.ts` — added EffectPreset interface
|
||||
- `server/src/wled_controller/static/locales/en.json`, `ru.json`, `zh.json` — added game_integration.mapping.select_preset key
|
||||
- `server/tests/api/routes/test_game_integration_routes.py` — added TestPresets class (6 tests: list, mappings, apply replace/append, unknown key/integration)
|
||||
|
||||
**Key wiring fix:** GameEventBus was created in main.py but NOT passed to ProcessorDependencies, meaning ColorStripStreamManager and ValueStreamManager could never receive it. Now properly threaded through.
|
||||
|
||||
**Deferred items:** WebSocket live streaming (Task 3), setup wizard (Task 5), community adapter import UI (Task 6), adapter documentation (Task 8) — none are blocking for the feature to function end-to-end.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Feature Context: Processed Audio Sources
|
||||
|
||||
## Configuration
|
||||
- **Development mode:** Automated
|
||||
- **Execution mode:** Orchestrator
|
||||
- **Strategy:** Big Bang
|
||||
- **Build (Python):** `cd server && ruff check src/ tests/ --fix`
|
||||
- **Build (TypeScript):** `cd server && npx tsc --noEmit && npm run build`
|
||||
- **Test:** `cd server && py -3.13 -m pytest tests/ --no-cov -q`
|
||||
|
||||
## Current State
|
||||
Phase 1 (Audio Filter Framework) implemented. Core framework is in place:
|
||||
- `AudioFilter` base class, `AudioFilterRegistry`, `AudioFilterOptionDef` in `core/audio/filters/`
|
||||
- `AudioProcessingTemplate` dataclass + `AudioProcessingTemplateStore` (SQLite-backed) in `storage/`
|
||||
- `audio_filter_template` meta-filter with recursive resolution
|
||||
- Full REST API: CRUD templates + filter registry discovery
|
||||
- Dependency injection wired in `dependencies.py` and `main.py`
|
||||
|
||||
## Key Architecture Reference
|
||||
|
||||
### Existing Pattern to Mirror: Processed Picture Sources
|
||||
- `ProcessedPictureSource` references `source_stream_id` + `postprocessing_template_id`
|
||||
- `PostprocessingTemplate` contains ordered `List[FilterInstance]`
|
||||
- `FilterInstance` = `filter_id` (string) + `options` (dict)
|
||||
- `FilterRegistry` handles registration, lookup, instantiation
|
||||
- `filter_template` meta-filter embeds one template inside another
|
||||
- `PostprocessingTemplateStore` has `resolve_filter_instances()` for recursive expansion
|
||||
- Picture filters transform images; audio filters will transform `AudioAnalysis`
|
||||
|
||||
### Current Audio Source Types (to be replaced)
|
||||
- `MultichannelAudioSource` → renamed to `CaptureAudioSource`
|
||||
- `MonoAudioSource` → removed, replaced by channel_extract filter
|
||||
- `BandExtractAudioSource` → removed, replaced by band_extract filter
|
||||
|
||||
### AudioAnalysis Structure (filter input/output)
|
||||
```python
|
||||
AudioAnalysis:
|
||||
timestamp: float
|
||||
rms: float # Overall RMS level
|
||||
peak: float # Peak amplitude
|
||||
spectrum: np.ndarray[64] # Log-spaced FFT bands
|
||||
beat: bool # Beat detected
|
||||
beat_intensity: float # 0-1 beat strength
|
||||
left_rms: float # Left channel RMS
|
||||
left_spectrum: np.ndarray # Left channel spectrum
|
||||
right_rms: float # Right channel RMS
|
||||
right_spectrum: np.ndarray # Right channel spectrum
|
||||
```
|
||||
|
||||
### Key Existing Files
|
||||
- `storage/audio_source.py` — current source dataclasses
|
||||
- `storage/audio_source_store.py` — CRUD + resolve_audio_source()
|
||||
- `core/audio/analysis.py` — AudioAnalyzer, AudioAnalysis
|
||||
- `core/audio/band_filter.py` — existing band filtering logic
|
||||
- `core/processing/audio_stream.py` — AudioColorStripStream
|
||||
- `core/processing/value_stream.py` — AudioValueStream
|
||||
- `core/filters/base.py` — PostprocessingFilter (picture filter base class)
|
||||
- `core/filters/registry.py` — FilterRegistry (picture filters)
|
||||
- `storage/postprocessing_template.py` — PostprocessingTemplate dataclass
|
||||
- `storage/postprocessing_template_store.py` — template store with resolve_filter_instances()
|
||||
|
||||
## Temporary Workarounds
|
||||
_(none yet)_
|
||||
|
||||
## Cross-Phase Dependencies
|
||||
- Phase 2 depends on Phase 1 (filter framework)
|
||||
- Phase 3 depends on Phase 1 (template store for ProcessedAudioSource)
|
||||
- Phase 4 depends on Phases 1-3 (all backend pieces)
|
||||
- Phase 5 depends on Phase 1 (template API)
|
||||
- Phase 6 depends on Phase 3 (source type API)
|
||||
- Phase 7 depends on all prior phases
|
||||
|
||||
## Deferred Work
|
||||
_(none yet)_
|
||||
|
||||
## Failed Approaches
|
||||
_(none yet)_
|
||||
|
||||
## Review Findings Log
|
||||
_(none yet)_
|
||||
|
||||
## Phase Execution Log
|
||||
| Phase | Agent Used | Test Writer | Parallel | Notes |
|
||||
|-------|-----------|-------------|----------|-------|
|
||||
| Phase 1 | impl-agent | — | No | Tasks 7+8 skipped (SQLite migration made them obsolete) |
|
||||
| Phase 2 | — | — | — | — |
|
||||
| Phase 3 | — | — | — | — |
|
||||
| Phase 4 | — | — | — | — |
|
||||
| Phase 5 | — | — | — | — |
|
||||
| Phase 6 | — | — | — | — |
|
||||
| Phase 7 | — | — | — | — |
|
||||
|
||||
## Environment & Runtime Notes
|
||||
- Platform: Windows 10
|
||||
- Python: 3.13
|
||||
- Server port: 8080
|
||||
- Shell: bash (Git Bash on Windows)
|
||||
|
||||
## Implementation Notes
|
||||
- Clean-slate approach: no migration of existing MonoAudioSource/BandExtractAudioSource data
|
||||
- 5 of 11 filters are stateful (peak hold, envelope follower, spectral smoothing, compressor, delay) — need per-stream instance lifecycle
|
||||
- Audio filters operate on AudioAnalysis snapshots, not raw audio samples
|
||||
- Big Bang strategy: intermediate phases may break the build; only Phase 7 enforces build/tests
|
||||
@@ -0,0 +1,55 @@
|
||||
# Feature: Processed Audio Sources
|
||||
|
||||
**Branch:** `feature/processed-audio-sources`
|
||||
**Base branch:** `master`
|
||||
**Created:** 2026-03-31
|
||||
**Status:** 🟡 In Progress
|
||||
**Strategy:** Big Bang
|
||||
**Mode:** Automated
|
||||
**Execution:** Orchestrator
|
||||
|
||||
## Summary
|
||||
|
||||
Replace hardcoded `MonoAudioSource` and `BandExtractAudioSource` types with a composable
|
||||
**ProcessedAudioSource + AudioProcessingTemplate + AudioFilter** system — mirroring the
|
||||
existing processed picture source pattern. Rename `MultichannelAudioSource` to
|
||||
`CaptureAudioSource`. Adds 11 audio filters: channel extract, band extract, peak hold,
|
||||
gain, noise gate, envelope follower, spectral smoothing, compressor, inverter, beat gate,
|
||||
and delay.
|
||||
|
||||
Clean-slate approach: no data migration for old source types.
|
||||
|
||||
## Build & Test Commands
|
||||
- **Build (Python):** `cd server && ruff check src/ tests/ --fix`
|
||||
- **Build (TypeScript):** `cd server && npx tsc --noEmit && npm run build`
|
||||
- **Test:** `cd server && py -3.13 -m pytest tests/ --no-cov -q`
|
||||
|
||||
## Phases
|
||||
|
||||
- [ ] Phase 1: Audio Filter Framework [domain: backend] → [subplan](./phase-1-audio-filter-framework.md)
|
||||
- [ ] Phase 2: Audio Filters [domain: backend] → [subplan](./phase-2-audio-filters.md)
|
||||
- [ ] Phase 3: Processed Audio Source Model [domain: backend] → [subplan](./phase-3-processed-audio-source-model.md)
|
||||
- [ ] Phase 4: Runtime Integration [domain: backend] → [subplan](./phase-4-runtime-integration.md)
|
||||
- [ ] Phase 5: Frontend — Audio Processing Templates [domain: frontend] → [subplan](./phase-5-frontend-templates.md)
|
||||
- [ ] Phase 6: Frontend — Source Types [domain: frontend] → [subplan](./phase-6-frontend-source-types.md)
|
||||
- [ ] Phase 7: Testing & Polish [domain: backend] → [subplan](./phase-7-testing-polish.md)
|
||||
- [ ] Phase 8: Frontend Design Consistency Review [domain: frontend] → [subplan](./phase-8-frontend-design-review.md)
|
||||
|
||||
## Phase Progress Log
|
||||
|
||||
| Phase | Domain | Status | Review | Build | Committed |
|
||||
|-------|--------|--------|--------|-------|-----------|
|
||||
| Phase 1: Audio Filter Framework | backend | 🔨 In Progress | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 2: Audio Filters | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 3: Processed Audio Source Model | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 4: Runtime Integration | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 5: Frontend — Audio Processing Templates | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 6: Frontend — Source Types | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 7: Testing & Polish | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 8: Frontend Design Review | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
|
||||
## Final Review
|
||||
- [ ] Comprehensive code review
|
||||
- [ ] Full build passes
|
||||
- [ ] Full test suite passes
|
||||
- [ ] Merged to `master`
|
||||
@@ -0,0 +1,106 @@
|
||||
# Phase 1: Audio Filter Framework
|
||||
|
||||
**Status:** 🔨 In Progress
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Build the foundation for audio processing: base filter class, registry, template storage, and API endpoints. This mirrors the existing picture filter/postprocessing template system.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Task 1: Create `AudioFilter` base class in `core/audio/filters/base.py`
|
||||
- Abstract `process(analysis: AudioAnalysis) -> AudioAnalysis` method
|
||||
- `is_stateful` property (False by default, overridden by stateful filters)
|
||||
- `reset()` method for stateful filters
|
||||
- `AudioFilterOptionDef` class for declaring filter option schemas (mirrors `FilterOptionDef` from picture filters)
|
||||
- Class-level `filter_id`, `name`, `description`, `options` declarations
|
||||
- [x] Task 2: Create `AudioFilterRegistry` in `core/audio/filters/registry.py`
|
||||
- `register(filter_class)` — register by filter_id
|
||||
- `get_filter_class(filter_id)` — lookup
|
||||
- `create_instance(filter_id, options)` → instantiated AudioFilter
|
||||
- `get_available_filters()` → list of filter metadata + option schemas
|
||||
- Mirrors `FilterRegistry` from `core/filters/registry.py`
|
||||
- [x] Task 3: Create `AudioProcessingTemplate` dataclass in `storage/audio_processing_template.py`
|
||||
- Fields: `id`, `name`, `filters: List[FilterInstance]`, `description`, `tags`, `created_at`, `updated_at`
|
||||
- Reuse existing `FilterInstance` from `core/filters/filter_instance.py`
|
||||
- ID prefix: `apt_` (audio processing template)
|
||||
- [x] Task 4: Create `AudioProcessingTemplateStore` in `storage/audio_processing_template_store.py`
|
||||
- SQLite-backed CRUD (same pattern as `PostprocessingTemplateStore`)
|
||||
- `resolve_filter_instances()` — recursive expansion of `audio_filter_template` meta-filter
|
||||
- Cycle detection in template composition
|
||||
- Reference validation (check filter_ids exist in registry)
|
||||
- [x] Task 5: Create `audio_filter_template` meta-filter in `core/audio/filters/audio_filter_template.py`
|
||||
- Option: `template_id` referencing another AudioProcessingTemplate
|
||||
- Never instantiated at runtime — expanded during resolution
|
||||
- Mirrors `filter_template` from picture filters
|
||||
- [x] Task 6: Create `core/audio/filters/__init__.py` — register all filters with the registry
|
||||
- [x] Task 7: ~~Add `audio_processing_templates_file` to `StorageConfig` in `config.py`~~ SKIPPED — storage uses SQLite now, not JSON files. No config change needed.
|
||||
- [x] Task 8: ~~Add audio processing templates to `STORE_MAP` in `api/routes/system.py`~~ SKIPPED — STORE_MAP no longer exists. Backup/restore works at the SQLite database level; new tables are automatically included.
|
||||
- [x] Task 9: Create API schemas in `api/schemas/audio_processing.py`
|
||||
- `AudioProcessingTemplateCreate`, `AudioProcessingTemplateUpdate`, `AudioProcessingTemplateResponse`
|
||||
- `AudioFilterInstanceSchema` (reuse `FilterInstanceSchema` from existing schemas)
|
||||
- [x] Task 10: Create API routes in `api/routes/audio_processing_templates.py`
|
||||
- `GET /api/v1/audio-processing-templates` — list all
|
||||
- `POST /api/v1/audio-processing-templates` — create
|
||||
- `GET /api/v1/audio-processing-templates/{template_id}` — get one
|
||||
- `PUT /api/v1/audio-processing-templates/{template_id}` — update
|
||||
- `DELETE /api/v1/audio-processing-templates/{template_id}` — delete (with ref checks placeholder)
|
||||
- [x] Task 11: Create filter registry endpoint in `api/routes/audio_filters.py`
|
||||
- `GET /api/v1/audio-filters` — returns available filters with option schemas
|
||||
- Dynamically populates `audio_filter_template` options with current template IDs
|
||||
- [x] Task 12: Register new routes in the FastAPI app
|
||||
|
||||
## Files to Modify/Create
|
||||
- `core/audio/filters/base.py` — **created** — AudioFilter base class + AudioFilterOptionDef
|
||||
- `core/audio/filters/registry.py` — **created** — AudioFilterRegistry
|
||||
- `core/audio/filters/audio_filter_template.py` — **created** — meta-filter
|
||||
- `core/audio/filters/__init__.py` — **created** — filter registration
|
||||
- `storage/audio_processing_template.py` — **created** — dataclass
|
||||
- `storage/audio_processing_template_store.py` — **created** — store
|
||||
- `api/schemas/audio_processing.py` — **created** — Pydantic schemas
|
||||
- `api/routes/audio_processing_templates.py` — **created** — template CRUD routes
|
||||
- `api/routes/audio_filters.py` — **created** — filter registry endpoint
|
||||
- `api/dependencies.py` — **modified** — added getter + init param for audio_processing_template_store
|
||||
- `api/__init__.py` — **modified** — registered new routers
|
||||
- `main.py` — **modified** — created store instance, imported audio filter package, wired to init_dependencies
|
||||
|
||||
## Acceptance Criteria
|
||||
- AudioFilter base class exists with process/reset/options API
|
||||
- AudioFilterRegistry can register, lookup, and instantiate filters
|
||||
- AudioProcessingTemplate can be created, read, updated, deleted via API
|
||||
- `audio_filter_template` meta-filter is handled during resolution (recursive expansion)
|
||||
- Filter registry endpoint returns available filters with option schemas
|
||||
- Backup/restore includes audio processing templates (automatic via SQLite)
|
||||
|
||||
## Notes
|
||||
- Reuse `FilterInstance` from `core/filters/filter_instance.py` — no need to create a separate one for audio
|
||||
- The `audio_filter_template` meta-filter option should dynamically list available template IDs (like the picture filter version)
|
||||
- Phase 2 will register the actual filters; this phase just sets up the framework with only the meta-filter registered
|
||||
- Tasks 7 and 8 from original plan were skipped — the codebase migrated from JSON files to SQLite since the plan was written
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
### What was built
|
||||
- Complete audio filter framework: `AudioFilter` base class, `AudioFilterRegistry`, `AudioFilterOptionDef`
|
||||
- `AudioProcessingTemplate` dataclass + `AudioProcessingTemplateStore` (SQLite-backed CRUD)
|
||||
- `audio_filter_template` meta-filter with recursive resolution and cycle detection
|
||||
- Full REST API: CRUD for templates + filter registry discovery endpoint
|
||||
- Dependency injection wired through `dependencies.py` and `main.py`
|
||||
|
||||
### What Phase 2 needs to know
|
||||
- Register new audio filters by importing them in `core/audio/filters/__init__.py` and decorating with `@AudioFilterRegistry.register`
|
||||
- Each filter implements `process(analysis: AudioAnalysis) -> AudioAnalysis`
|
||||
- Stateful filters override `is_stateful` property to return `True` and implement `reset()`
|
||||
- The `AudioFilterOptionDef` class mirrors `FilterOptionDef` exactly (same option_type values, same validation logic)
|
||||
|
||||
### Known deviations from plan
|
||||
- Tasks 7 and 8 skipped: `StorageConfig` no longer holds per-entity file paths (SQLite migration), and `STORE_MAP` no longer exists (database-level backup/restore)
|
||||
- Delete endpoint has a TODO placeholder for reference checks — Phase 3 will add `ProcessedAudioSource` which references templates
|
||||
@@ -0,0 +1,97 @@
|
||||
# Phase 2: Audio Filters
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Implement all 11 audio filters and register them with the AudioFilterRegistry.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: **Channel Extract** filter (`core/audio/filters/channel_extract.py`)
|
||||
- Options: `channel` (select: mono | left | right)
|
||||
- Stateful: No
|
||||
- Behavior: Replaces main rms/spectrum with selected channel data. If "mono", averages L+R. If "left"/"right", copies that channel's data to the main fields.
|
||||
- [ ] Task 2: **Band Extract** filter (`core/audio/filters/band_extract.py`)
|
||||
- Options: `band` (select: bass | mid | treble | custom), `freq_low` (float, 20-20000), `freq_high` (float, 20-20000)
|
||||
- Stateful: No
|
||||
- Behavior: Computes a band mask for the 64 log-spaced bins, applies it to spectrum, recomputes RMS from in-band data. Reuse logic from existing `core/audio/band_filter.py`.
|
||||
- Presets: bass=20-250Hz, mid=250-4000Hz, treble=4000-20000Hz
|
||||
- [ ] Task 3: **Peak Hold** filter (`core/audio/filters/peak_hold.py`)
|
||||
- Options: `decay_rate` (float, 0.1-50.0, dB/s), `per_bin` (bool, default true)
|
||||
- Stateful: Yes
|
||||
- Behavior: For each spectrum bin (if per_bin) or for rms/peak, retains the maximum value seen and decays it over time. Outputs the max of current value and held peak.
|
||||
- [ ] Task 4: **Gain** filter (`core/audio/filters/gain.py`)
|
||||
- Options: `factor` (float, 0.1-10.0, default 1.0)
|
||||
- Stateful: No
|
||||
- Behavior: Multiplies rms, peak, spectrum, and per-channel values by factor. Clamps to [0, 1] for rms/peak.
|
||||
- [ ] Task 5: **Noise Gate** filter (`core/audio/filters/noise_gate.py`)
|
||||
- Options: `threshold` (float, 0.0-1.0), `hysteresis` (float, 0.0-0.2, default 0.05)
|
||||
- Stateful: No (hysteresis is stateless — it's a secondary threshold, not temporal)
|
||||
- Behavior: If rms < threshold, zeros out all levels and spectrum. Hysteresis means: if gate was open and rms drops below (threshold - hysteresis), close it; if gate was closed and rms rises above threshold, open it.
|
||||
- Actually stateful for hysteresis tracking: needs to remember gate open/closed state.
|
||||
- [ ] Task 6: **Envelope Follower** filter (`core/audio/filters/envelope_follower.py`)
|
||||
- Options: `attack_ms` (float, 1-500, default 10), `release_ms` (float, 10-2000, default 200)
|
||||
- Stateful: Yes
|
||||
- Behavior: Smooths rms and peak with asymmetric time constants. When signal rises, uses attack rate. When signal falls, uses release rate. Applied per-bin to spectrum optionally.
|
||||
- Fast attack + slow release = punchy transients that fade smoothly.
|
||||
- [ ] Task 7: **Spectral Smoothing** filter (`core/audio/filters/spectral_smoothing.py`)
|
||||
- Options: `factor` (float, 0.0-0.99, default 0.5)
|
||||
- Stateful: Yes (maintains previous spectrum state)
|
||||
- Behavior: Applies exponential moving average per-bin: `smoothed[i] = factor * prev[i] + (1-factor) * current[i]`. Higher factor = smoother/slower.
|
||||
- [ ] Task 8: **Compressor** filter (`core/audio/filters/compressor.py`)
|
||||
- Options: `threshold` (float, 0.0-1.0, default 0.5), `ratio` (float, 1.0-20.0, default 4.0), `makeup_gain` (float, 0.0-2.0, default 1.0)
|
||||
- Stateful: Yes (envelope tracking for gain reduction)
|
||||
- Behavior: When signal exceeds threshold, reduces by ratio. `output = threshold + (input - threshold) / ratio`. Apply makeup_gain after. Applied to rms, peak, and spectrum.
|
||||
- [ ] Task 9: **Inverter** filter (`core/audio/filters/inverter.py`)
|
||||
- Options: none (or `invert_spectrum` bool, default true)
|
||||
- Stateful: No
|
||||
- Behavior: `rms = 1.0 - rms`, `peak = 1.0 - peak`, spectrum bins inverted if option set. Beat fields unchanged.
|
||||
- [ ] Task 10: **Beat Gate** filter (`core/audio/filters/beat_gate.py`)
|
||||
- Options: `hold_ms` (float, 10-500, default 50) — how long to hold signal after beat
|
||||
- Stateful: Yes (tracks last beat timestamp)
|
||||
- Behavior: When beat detected, passes signal through for `hold_ms` milliseconds. Between beats, zeros out rms/peak/spectrum. Beat fields themselves always pass through.
|
||||
- [ ] Task 11: **Delay** filter (`core/audio/filters/delay.py`)
|
||||
- Options: `delay_ms` (float, 10-2000, default 100)
|
||||
- Stateful: Yes (ring buffer of AudioAnalysis snapshots)
|
||||
- Behavior: Buffers incoming AudioAnalysis snapshots and outputs the one from `delay_ms` ago. Ring buffer sized based on ~30Hz update rate.
|
||||
- [ ] Task 12: Register all 11 filters in `core/audio/filters/__init__.py`
|
||||
- [ ] Task 13: Update Noise Gate to be stateful (hysteresis requires gate state tracking)
|
||||
|
||||
## Files to Modify/Create
|
||||
- `core/audio/filters/channel_extract.py` — **create**
|
||||
- `core/audio/filters/band_extract.py` — **create**
|
||||
- `core/audio/filters/peak_hold.py` — **create**
|
||||
- `core/audio/filters/gain.py` — **create**
|
||||
- `core/audio/filters/noise_gate.py` — **create**
|
||||
- `core/audio/filters/envelope_follower.py` — **create**
|
||||
- `core/audio/filters/spectral_smoothing.py` — **create**
|
||||
- `core/audio/filters/compressor.py` — **create**
|
||||
- `core/audio/filters/inverter.py` — **create**
|
||||
- `core/audio/filters/beat_gate.py` — **create**
|
||||
- `core/audio/filters/delay.py` — **create**
|
||||
- `core/audio/filters/__init__.py` — **modify** — register all filters
|
||||
|
||||
## Acceptance Criteria
|
||||
- All 11 filters are implemented and registered
|
||||
- Each filter correctly transforms AudioAnalysis according to its specification
|
||||
- Stateful filters (peak hold, envelope follower, spectral smoothing, compressor, beat gate, delay, noise gate) properly maintain and reset state
|
||||
- Filter option schemas are complete and accurate
|
||||
- All filters are accessible via `GET /api/v1/audio-filters`
|
||||
|
||||
## Notes
|
||||
- 6 stateful filters: peak hold, envelope follower, spectral smoothing, compressor, beat gate, delay. Noise gate is also stateful due to hysteresis.
|
||||
- Band extract can reuse math from existing `core/audio/band_filter.py` — `compute_band_mask()` and `apply_band_filter()`
|
||||
- Filters must produce a NEW AudioAnalysis (immutability principle), not mutate the input
|
||||
- For delay filter, ring buffer size = `delay_ms / (1000 / update_rate)`. At 30Hz, 2000ms delay = 60 slots.
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in by the implementation agent after completing this phase. -->
|
||||
@@ -0,0 +1,78 @@
|
||||
# Phase 3: Processed Audio Source Model
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Add the `ProcessedAudioSource` type, rename `MultichannelAudioSource` to `CaptureAudioSource`, remove `MonoAudioSource` and `BandExtractAudioSource`, and update the store's resolution logic.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Rename `MultichannelAudioSource` → `CaptureAudioSource` in `storage/audio_source.py`
|
||||
- Change class name, update `source_type` default to `"capture"`
|
||||
- Same fields: `device_index`, `is_loopback`, `audio_template_id`
|
||||
- [ ] Task 2: Add `ProcessedAudioSource` dataclass in `storage/audio_source.py`
|
||||
- Fields: `audio_source_id: str` (input source), `audio_processing_template_id: str`
|
||||
- `source_type` = `"processed"`
|
||||
- Inherits standard base fields (id, name, description, tags, created_at, updated_at)
|
||||
- [ ] Task 3: Remove `MonoAudioSource` class entirely
|
||||
- [ ] Task 4: Remove `BandExtractAudioSource` class entirely
|
||||
- [ ] Task 5: Update `create_audio_source()` factory function to handle new types
|
||||
- [ ] Task 6: Update `AudioSourceStore` resolution logic:
|
||||
- `resolve_audio_source()` now returns: device info (from CaptureAudioSource at chain end) + ordered list of filter chains (from AudioProcessingTemplates along the chain)
|
||||
- Walk chain: ProcessedAudioSource → ... → CaptureAudioSource
|
||||
- Collect all audio_processing_template_ids in order
|
||||
- Cycle detection for ProcessedAudioSource chains
|
||||
- [ ] Task 7: Update `ResolvedAudioSource` dataclass:
|
||||
- Remove `channel` and `freq_low`/`freq_high` fields (handled by filters now)
|
||||
- Add `filter_instances: List[FilterInstance]` — flattened, ordered list of all filters to apply
|
||||
- Or add `template_ids: List[str]` and resolve at runtime
|
||||
- [ ] Task 8: Update reference validation in store:
|
||||
- `ProcessedAudioSource.audio_source_id` must reference an existing audio source
|
||||
- `ProcessedAudioSource.audio_processing_template_id` must reference an existing template
|
||||
- Delete checks: can't delete a source referenced by another ProcessedAudioSource
|
||||
- Delete checks: can't delete a template referenced by a ProcessedAudioSource
|
||||
- [ ] Task 9: Update API schemas in `api/schemas/audio_sources.py`
|
||||
- Remove `MonoAudioSourceCreate/Update/Response` schemas
|
||||
- Remove `BandExtractAudioSourceCreate/Update/Response` schemas
|
||||
- Add `CaptureAudioSourceCreate/Update/Response` (rename from Multichannel)
|
||||
- Add `ProcessedAudioSourceCreate/Update/Response`
|
||||
- Update discriminated union to use new type literals
|
||||
- [ ] Task 10: Update API routes in `api/routes/audio_sources.py`
|
||||
- Handle new source types in create/update endpoints
|
||||
- Remove handling of old types
|
||||
- Update WebSocket test endpoint to work with ProcessedAudioSource
|
||||
- [ ] Task 11: Update any imports/references across the codebase that reference the old types
|
||||
|
||||
## Files to Modify/Create
|
||||
- `storage/audio_source.py` — **modify** — rename, add, remove dataclasses
|
||||
- `storage/audio_source_store.py` — **modify** — new resolution logic, validation
|
||||
- `api/schemas/audio_sources.py` — **modify** — new schemas
|
||||
- `api/routes/audio_sources.py` — **modify** — handle new types
|
||||
- Any files importing `MultichannelAudioSource`, `MonoAudioSource`, `BandExtractAudioSource` — **modify**
|
||||
|
||||
## Acceptance Criteria
|
||||
- `CaptureAudioSource` replaces `MultichannelAudioSource` (same behavior, new name/type)
|
||||
- `ProcessedAudioSource` can be created referencing a source + template
|
||||
- `MonoAudioSource` and `BandExtractAudioSource` are fully removed
|
||||
- Chain resolution walks ProcessedAudioSource → ... → CaptureAudioSource correctly
|
||||
- Cycle detection prevents circular source references
|
||||
- Reference validation prevents dangling references
|
||||
- API accepts/returns new type discriminators
|
||||
|
||||
## Notes
|
||||
- Clean-slate: no migration of existing data. Old source type records will be lost.
|
||||
- The `source_type` string changes from `"multichannel"` to `"capture"` — this is a breaking change but acceptable for clean-slate.
|
||||
- `ResolvedAudioSource` is consumed by `AudioColorStripStream` and `AudioValueStream` — they will need updates in Phase 4.
|
||||
- Template reference checks in the store need coordination with `AudioProcessingTemplateStore` — may need to pass it as a dependency.
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in by the implementation agent after completing this phase. -->
|
||||
@@ -0,0 +1,71 @@
|
||||
# Phase 4: Runtime Integration
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Wire the audio filter pipeline into the runtime audio streaming system so that ProcessedAudioSources actually apply their filter chains to live audio data.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Create filter pipeline executor in `core/audio/filters/pipeline.py`
|
||||
- `AudioFilterPipeline` class:
|
||||
- `__init__(filter_instances: List[FilterInstance], registry: AudioFilterRegistry)`
|
||||
- Instantiates all filters from FilterInstance specs
|
||||
- `process(analysis: AudioAnalysis) -> AudioAnalysis` — runs analysis through all filters in order
|
||||
- `reset()` — resets all stateful filters
|
||||
- `close()` — cleanup resources
|
||||
- Handles stateful filter lifecycle (create on init, reset on demand, close on cleanup)
|
||||
- [ ] Task 2: Update `AudioColorStripStream` in `core/processing/audio_stream.py`
|
||||
- On construction: if source is ProcessedAudioSource, resolve the full chain:
|
||||
- Walk to CaptureAudioSource for device info
|
||||
- Collect all AudioProcessingTemplates along the chain
|
||||
- Resolve all filter instances (with template expansion)
|
||||
- Create AudioFilterPipeline
|
||||
- In render loop: after getting AudioAnalysis from ManagedAudioStream, run it through the filter pipeline before visualization
|
||||
- Remove old inline channel selection and band filtering code (now handled by filters)
|
||||
- On stop: close the filter pipeline
|
||||
- [ ] Task 3: Update `AudioValueStream` in `core/processing/value_stream.py`
|
||||
- Same pattern: resolve ProcessedAudioSource chain, create filter pipeline, apply in get_value()
|
||||
- Remove old inline channel/band handling
|
||||
- [ ] Task 4: Hot-update support for filter templates
|
||||
- When an AudioProcessingTemplate is updated, running streams that use it should re-resolve their filter pipeline
|
||||
- Listen for template update events (or implement a refresh mechanism)
|
||||
- Re-create AudioFilterPipeline with updated filter instances
|
||||
- Reset stateful filter state on pipeline refresh
|
||||
- [ ] Task 5: Update WebSocket test endpoint in `api/routes/audio_sources.py`
|
||||
- For ProcessedAudioSource: resolve chain, create pipeline, apply filters to test stream data
|
||||
- Return filtered analysis in real-time over WebSocket
|
||||
- [ ] Task 6: Update any code that calls `AudioSourceStore.resolve_audio_source()` to handle the new return shape
|
||||
|
||||
## Files to Modify/Create
|
||||
- `core/audio/filters/pipeline.py` — **create** — AudioFilterPipeline
|
||||
- `core/processing/audio_stream.py` — **modify** — integrate filter pipeline
|
||||
- `core/processing/value_stream.py` — **modify** — integrate filter pipeline
|
||||
- `api/routes/audio_sources.py` — **modify** — update WebSocket test
|
||||
- Any other consumers of `resolve_audio_source()` — **modify**
|
||||
|
||||
## Acceptance Criteria
|
||||
- ProcessedAudioSource chains are resolved and filter pipelines created at stream start
|
||||
- AudioAnalysis passes through the filter chain before visualization/value extraction
|
||||
- Stateful filters maintain correct state across frames
|
||||
- Hot-update of templates refreshes running filter pipelines
|
||||
- WebSocket test endpoint works with processed sources
|
||||
- Old inline channel/band code removed from stream classes
|
||||
|
||||
## Notes
|
||||
- ⚠️ Temporary breakage: Removing inline channel/band code from AudioColorStripStream breaks existing MonoAudioSource/BandExtractAudioSource flows — but those types were already removed in Phase 3.
|
||||
- Filter pipeline must be thread-safe: AudioColorStripStream and AudioValueStream run in background threads.
|
||||
- For hot-update: consider using an event callback from the template store rather than polling.
|
||||
- The filter pipeline should produce a new AudioAnalysis each time (immutability), not mutate the shared snapshot from ManagedAudioStream.
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in by the implementation agent after completing this phase. -->
|
||||
@@ -0,0 +1,70 @@
|
||||
# Phase 5: Frontend — Audio Processing Templates
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** frontend
|
||||
|
||||
## Objective
|
||||
Build the frontend UI for managing Audio Processing Templates — list, create, edit, delete, with a filter editor and real-time preview.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Create TypeScript module `static/js/features/audio-processing-templates.ts`
|
||||
- Fetch/cache audio processing templates via DataCache
|
||||
- CRUD operations using fetchWithAuth
|
||||
- CardSection for template list with reconciliation
|
||||
- [ ] Task 2: Create template editor modal
|
||||
- Name, description, tags fields
|
||||
- Ordered filter list with add/remove/reorder controls
|
||||
- Per-filter option controls (sliders, selects, toggles) driven by option schemas from `GET /api/v1/audio-filters`
|
||||
- Template composition support: `audio_filter_template` shows EntitySelect for sub-template
|
||||
- Dirty check via snapshotValues()
|
||||
- [ ] Task 3: Add Audio Processing Templates section to the Streams tab
|
||||
- New sub-tab or section alongside existing Audio Sources
|
||||
- CardSection rendering with template name, filter count, description
|
||||
- Create/Edit/Delete actions per card
|
||||
- [ ] Task 4: Real-time audio preview
|
||||
- "Test" button on template editor that opens a WebSocket connection
|
||||
- Shows spectrum visualization (reuse existing audio test pattern)
|
||||
- Applies template's filters to a selected source in real-time
|
||||
- Source picker (EntitySelect) for choosing input audio source
|
||||
- [ ] Task 5: Add i18n keys for all new UI strings (en.json, ru.json, zh.json)
|
||||
- Template section labels, filter names, option labels, buttons, errors
|
||||
- [ ] Task 6: Register module in `app.js` / global exports if needed for inline onclick handlers
|
||||
- [ ] Task 7: Fetch and cache audio filter registry data (for building filter option UIs)
|
||||
|
||||
## Files to Modify/Create
|
||||
- `static/js/features/audio-processing-templates.ts` — **create** — main module
|
||||
- `static/js/features/audio-processing-template-modal.ts` — **create** — editor modal
|
||||
- `static/css/dashboard.css` — **modify** — styles for template editor
|
||||
- `static/js/app.js` — **modify** — register module, add window exports
|
||||
- `templates/dashboard.html` (or relevant Jinja template) — **modify** — add section
|
||||
- `static/js/core/i18n/en.json` — **modify** — new keys
|
||||
- `static/js/core/i18n/ru.json` — **modify** — new keys
|
||||
- `static/js/core/i18n/zh.json` — **modify** — new keys
|
||||
|
||||
## Acceptance Criteria
|
||||
- Audio Processing Templates section visible in Streams tab
|
||||
- Templates can be created, edited, deleted
|
||||
- Filter editor shows all 11 available filters with correct option controls
|
||||
- Template composition (audio_filter_template) works via EntitySelect
|
||||
- Real-time preview shows filtered audio data
|
||||
- All strings are internationalized
|
||||
|
||||
## Notes
|
||||
- Follow existing patterns from postprocessing template UI
|
||||
- Use IconSelect for filter type selection (if icon set supports it) or a custom filter picker
|
||||
- NEVER use plain HTML `<select>` — use project custom selectors (CRITICAL project rule)
|
||||
- NEVER use emoji — use SVG icons from `core/icons.ts`
|
||||
- Use fetchWithAuth for ALL API calls (project rule)
|
||||
- Call cache.invalidate() before load functions in save/delete handlers
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in by the implementation agent after completing this phase. -->
|
||||
@@ -0,0 +1,67 @@
|
||||
# Phase 6: Frontend — Source Types
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** frontend
|
||||
|
||||
## Objective
|
||||
Update the audio source UI to support the new `ProcessedAudioSource` and `CaptureAudioSource` types, and remove the old `MonoAudioSource` and `BandExtractAudioSource` UI.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Update audio source TypeScript types/interfaces for new source types
|
||||
- Add `ProcessedAudioSource` type with `audio_source_id` + `audio_processing_template_id`
|
||||
- Rename `MultichannelAudioSource` type to `CaptureAudioSource` (source_type: "capture")
|
||||
- Remove `MonoAudioSource` and `BandExtractAudioSource` types
|
||||
- [ ] Task 2: Create `ProcessedAudioSource` card component
|
||||
- EntitySelect for input audio source (any audio source)
|
||||
- EntitySelect for audio processing template
|
||||
- Show resolved chain info (which capture source at the end)
|
||||
- Create/Edit/Delete actions
|
||||
- [ ] Task 3: Update `CaptureAudioSource` card (relabeled from Multichannel)
|
||||
- Same fields (device selector, loopback toggle, template selector)
|
||||
- Updated label/icon to say "Capture Audio Source"
|
||||
- [ ] Task 4: Remove `MonoAudioSource` card component/rendering
|
||||
- [ ] Task 5: Remove `BandExtractAudioSource` card component/rendering
|
||||
- [ ] Task 6: Update audio source creation dialog/flow
|
||||
- Source type picker now shows: Capture, Processed (instead of Multichannel, Mono, Band Extract)
|
||||
- Type-specific form fields
|
||||
- [ ] Task 7: Update EntitySelect dropdowns that list audio sources
|
||||
- Show type badges (Capture vs Processed) for clarity
|
||||
- Audio source selectors in CSS editor, value source editor, etc.
|
||||
- [ ] Task 8: Update i18n keys for renamed/new source types
|
||||
- [ ] Task 9: Update any inline onclick handlers or window exports in app.js
|
||||
|
||||
## Files to Modify/Create
|
||||
- `static/js/features/audio-sources.ts` — **modify** — new types, remove old types
|
||||
- `static/js/features/audio-source-modal.ts` (or equivalent) — **modify** — updated editor
|
||||
- `static/css/dashboard.css` — **modify** — any style updates
|
||||
- `static/js/app.js` — **modify** — update exports if needed
|
||||
- `static/js/core/i18n/en.json` — **modify** — updated keys
|
||||
- `static/js/core/i18n/ru.json` — **modify** — updated keys
|
||||
- `static/js/core/i18n/zh.json` — **modify** — updated keys
|
||||
|
||||
## Acceptance Criteria
|
||||
- ProcessedAudioSource can be created/edited/deleted from the UI
|
||||
- CaptureAudioSource shows correctly with updated label
|
||||
- MonoAudioSource and BandExtractAudioSource UI is completely removed
|
||||
- EntitySelect for audio sources shows type badges
|
||||
- Source type picker shows only Capture and Processed
|
||||
- All strings are internationalized
|
||||
|
||||
## Notes
|
||||
- NEVER use plain HTML `<select>` — use project custom selectors
|
||||
- NEVER use emoji — use SVG icons
|
||||
- Use fetchWithAuth for ALL API calls
|
||||
- Call cache.invalidate() before load in save/delete handlers
|
||||
- Check DOM ID conflicts when adding new card types (project checklist)
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- Filled in by the implementation agent after completing this phase. -->
|
||||
@@ -0,0 +1,85 @@
|
||||
# Phase 7: Testing & Polish
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** backend
|
||||
|
||||
## Objective
|
||||
Verify the full feature works end-to-end, write tests, fix any remaining issues, and clean up dead code.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Write unit tests for each audio filter
|
||||
- Test each filter transforms AudioAnalysis correctly
|
||||
- Test stateful filters maintain and reset state
|
||||
- Test edge cases: empty spectrum, zero rms, all-zero input
|
||||
- Test filter option validation
|
||||
- [ ] Task 2: Write unit tests for AudioFilterPipeline
|
||||
- Test chain of filters produces expected output
|
||||
- Test empty pipeline passes through unchanged
|
||||
- Test reset() resets all stateful filters
|
||||
- [ ] Task 3: Write integration tests for AudioProcessingTemplateStore
|
||||
- CRUD operations
|
||||
- Template composition (audio_filter_template) expansion
|
||||
- Cycle detection in template composition
|
||||
- Reference validation
|
||||
- [ ] Task 4: Write integration tests for ProcessedAudioSource in AudioSourceStore
|
||||
- CRUD operations
|
||||
- Chain resolution (ProcessedAudioSource → ... → CaptureAudioSource)
|
||||
- Cycle detection in source chains
|
||||
- Reference validation (source + template must exist)
|
||||
- Cascade delete checks
|
||||
- [ ] Task 5: Write API tests for audio processing template endpoints
|
||||
- Create, read, update, delete
|
||||
- Validation errors (missing fields, invalid filter_ids)
|
||||
- Reference check on delete
|
||||
- [ ] Task 6: Write API tests for updated audio source endpoints
|
||||
- Create/update ProcessedAudioSource and CaptureAudioSource
|
||||
- Reject old types (mono, band_extract)
|
||||
- Validation of source/template references
|
||||
- [ ] Task 7: Verify backup/restore includes audio processing templates
|
||||
- Create templates, backup, restore, verify they survive
|
||||
- [ ] Task 8: Full build verification
|
||||
- `ruff check src/ tests/ --fix` passes
|
||||
- `npx tsc --noEmit` passes
|
||||
- `npm run build` succeeds
|
||||
- `py -3.13 -m pytest tests/ --no-cov -q` — all tests pass
|
||||
- [ ] Task 9: Clean up dead code
|
||||
- Remove any remaining imports of `MonoAudioSource`, `BandExtractAudioSource`
|
||||
- Remove old `band_filter.py` if fully superseded by the band_extract filter (or keep if still used elsewhere)
|
||||
- Remove unused schema classes
|
||||
- Verify no orphaned i18n keys
|
||||
- [ ] Task 10: Update system health/info endpoints if they enumerate audio source types
|
||||
|
||||
## Files to Modify/Create
|
||||
- `tests/test_audio_filters.py` — **create** — filter unit tests
|
||||
- `tests/test_audio_filter_pipeline.py` — **create** — pipeline tests
|
||||
- `tests/test_audio_processing_template_store.py` — **create** — store tests
|
||||
- `tests/test_audio_source_store.py` — **modify** — updated for new types
|
||||
- `tests/test_audio_processing_templates_api.py` — **create** — API tests
|
||||
- `tests/test_audio_sources_api.py` — **modify** — updated for new types
|
||||
- Various files — **modify** — dead code cleanup
|
||||
|
||||
## Acceptance Criteria
|
||||
- All new tests pass
|
||||
- All existing tests pass (no regressions)
|
||||
- Full build (Python + TypeScript) succeeds
|
||||
- Ruff linting passes
|
||||
- No dead code referencing removed types
|
||||
- Backup/restore round-trips correctly with audio processing templates
|
||||
|
||||
## Notes
|
||||
- This is the Big Bang verification phase — the first time build + tests are enforced
|
||||
- Expect some breakage from Phases 1-6 that needs fixing here
|
||||
- Focus on fixing real issues, not cosmetic cleanup
|
||||
- `band_filter.py` may still be used by the band_extract filter — check before removing
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- N/A — final phase -->
|
||||
@@ -0,0 +1,65 @@
|
||||
# Phase 8: Frontend Design Consistency Review
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** frontend
|
||||
|
||||
## Objective
|
||||
Review all new frontend UI created in Phases 5-6 for visual consistency, design quality, and UX polish using the frontend-design skill agent. Fix any issues found.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1: Review Audio Processing Templates section for design consistency
|
||||
- Card layout, spacing, typography alignment with existing sections
|
||||
- Filter editor modal — controls alignment, visual hierarchy, grouping
|
||||
- Responsive behavior at different viewport widths
|
||||
- [ ] Task 2: Review Processed Audio Source cards for design consistency
|
||||
- Card style matches existing source cards (capture, picture, value sources)
|
||||
- EntitySelect pickers are visually consistent
|
||||
- Type badges/icons are clear and distinguishable
|
||||
- [ ] Task 3: Review Capture Audio Source card (relabeled)
|
||||
- Label/icon updates look correct
|
||||
- No visual regressions from the rename
|
||||
- [ ] Task 4: Review source type picker/creation flow
|
||||
- Type selector is clear and accessible
|
||||
- Transition between types is smooth
|
||||
- Empty states handled properly
|
||||
- [ ] Task 5: Review real-time audio preview UI
|
||||
- Spectrum visualization looks polished
|
||||
- Source picker and controls are well-placed
|
||||
- Loading/error states
|
||||
- [ ] Task 6: Fix all design issues found in Tasks 1-5
|
||||
- CSS adjustments for spacing, alignment, typography
|
||||
- Icon/color consistency
|
||||
- Dark mode compatibility (if applicable)
|
||||
- Hover/focus/active states on interactive elements
|
||||
- [ ] Task 7: Cross-browser spot-check (if applicable)
|
||||
|
||||
## Files to Modify/Create
|
||||
- `static/css/dashboard.css` — **modify** — design fixes
|
||||
- `static/js/features/audio-processing-templates.ts` — **modify** — UX fixes
|
||||
- `static/js/features/audio-processing-template-modal.ts` — **modify** — UX fixes
|
||||
- `static/js/features/audio-sources.ts` — **modify** — UX fixes
|
||||
- Any template/HTML files — **modify** — structural fixes
|
||||
|
||||
## Acceptance Criteria
|
||||
- All new UI sections are visually consistent with existing sections
|
||||
- No orphaned styles or visual regressions
|
||||
- Filter editor is intuitive and well-organized
|
||||
- Cards, modals, and controls follow existing design language
|
||||
- Interactive elements have proper hover/focus/active states
|
||||
|
||||
## Notes
|
||||
- This phase uses the frontend-design skill agent for review
|
||||
- Focus on consistency with existing UI, not a complete redesign
|
||||
- The project uses vanilla CSS (no framework) — fixes must use the existing stylesheet approach
|
||||
|
||||
## Review Checklist
|
||||
- [ ] All tasks completed
|
||||
- [ ] Code follows project conventions
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (new + existing)
|
||||
|
||||
## Handoff to Next Phase
|
||||
<!-- N/A — final phase -->
|
||||
@@ -28,6 +28,8 @@ from .routes.assets import router as assets_router
|
||||
from .routes.home_assistant import router as home_assistant_router
|
||||
from .routes.mqtt import router as mqtt_router
|
||||
from .routes.game_integration import router as game_integration_router
|
||||
from .routes.audio_processing_templates import router as audio_processing_templates_router
|
||||
from .routes.audio_filters import router as audio_filters_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(system_router)
|
||||
@@ -56,5 +58,7 @@ router.include_router(assets_router)
|
||||
router.include_router(home_assistant_router)
|
||||
router.include_router(mqtt_router)
|
||||
router.include_router(game_integration_router)
|
||||
router.include_router(audio_processing_templates_router)
|
||||
router.include_router(audio_filters_router)
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
@@ -37,6 +37,7 @@ from wled_controller.storage.game_integration_store import GameIntegrationStore
|
||||
from wled_controller.core.game_integration.event_bus import GameEventBus
|
||||
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
||||
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -163,6 +164,10 @@ def get_mqtt_manager() -> MQTTManager:
|
||||
return _get("mqtt_manager", "MQTT manager")
|
||||
|
||||
|
||||
def get_audio_processing_template_store() -> AudioProcessingTemplateStore:
|
||||
return _get("audio_processing_template_store", "Audio processing template store")
|
||||
|
||||
|
||||
def get_database() -> Database:
|
||||
return _get("database", "Database")
|
||||
|
||||
@@ -227,6 +232,7 @@ def init_dependencies(
|
||||
game_event_bus: GameEventBus | None = None,
|
||||
mqtt_store: MQTTSourceStore | None = None,
|
||||
mqtt_manager: MQTTManager | None = None,
|
||||
audio_processing_template_store: AudioProcessingTemplateStore | None = None,
|
||||
):
|
||||
"""Initialize global dependencies."""
|
||||
_deps.update(
|
||||
@@ -260,5 +266,6 @@ def init_dependencies(
|
||||
"game_event_bus": game_event_bus,
|
||||
"mqtt_store": mqtt_store,
|
||||
"mqtt_manager": mqtt_manager,
|
||||
"audio_processing_template_store": audio_processing_template_store,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Audio filter registry endpoint."""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import get_audio_processing_template_store
|
||||
from wled_controller.api.schemas.filters import (
|
||||
FilterOptionDefSchema,
|
||||
FilterTypeListResponse,
|
||||
FilterTypeResponse,
|
||||
)
|
||||
from wled_controller.core.audio.filters import AudioFilterRegistry
|
||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/audio-filters",
|
||||
response_model=FilterTypeListResponse,
|
||||
tags=["Audio Filters"],
|
||||
)
|
||||
async def list_audio_filter_types(
|
||||
_auth: AuthRequired,
|
||||
apt_store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
||||
):
|
||||
"""List all available audio filter types and their options schemas."""
|
||||
all_filters = AudioFilterRegistry.get_all()
|
||||
|
||||
# Pre-build template choices for the audio_filter_template filter
|
||||
template_choices = None
|
||||
if apt_store:
|
||||
try:
|
||||
templates = apt_store.get_all_templates()
|
||||
template_choices = [{"value": t.id, "label": t.name} for t in templates]
|
||||
except Exception:
|
||||
template_choices = []
|
||||
|
||||
responses = []
|
||||
for filter_id, filter_cls in all_filters.items():
|
||||
schema = filter_cls.get_options_schema()
|
||||
opt_schemas = []
|
||||
for opt in schema:
|
||||
d = opt.to_dict()
|
||||
# Dynamically populate template_id choices for audio_filter_template
|
||||
if (
|
||||
filter_id == "audio_filter_template"
|
||||
and opt.key == "template_id"
|
||||
and template_choices is not None
|
||||
):
|
||||
d["choices"] = template_choices
|
||||
opt_schemas.append(FilterOptionDefSchema(**d))
|
||||
responses.append(
|
||||
FilterTypeResponse(
|
||||
filter_id=filter_id,
|
||||
filter_name=filter_cls.filter_name,
|
||||
options_schema=opt_schemas,
|
||||
)
|
||||
)
|
||||
|
||||
return FilterTypeListResponse(filters=responses, count=len(responses))
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Audio processing template routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from wled_controller.api.auth import AuthRequired
|
||||
from wled_controller.api.dependencies import (
|
||||
fire_entity_event,
|
||||
get_audio_processing_template_store,
|
||||
)
|
||||
from wled_controller.api.schemas.audio_processing import (
|
||||
AudioProcessingTemplateCreate,
|
||||
AudioProcessingTemplateListResponse,
|
||||
AudioProcessingTemplateResponse,
|
||||
AudioProcessingTemplateUpdate,
|
||||
)
|
||||
from wled_controller.api.schemas.filters import FilterInstanceSchema
|
||||
from wled_controller.core.filters.filter_instance import FilterInstance
|
||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _apt_to_response(t) -> AudioProcessingTemplateResponse:
|
||||
"""Convert an AudioProcessingTemplate to its API response."""
|
||||
return AudioProcessingTemplateResponse(
|
||||
id=t.id,
|
||||
name=t.name,
|
||||
filters=[FilterInstanceSchema(filter_id=f.filter_id, options=f.options) for f in t.filters],
|
||||
created_at=t.created_at,
|
||||
updated_at=t.updated_at,
|
||||
description=t.description,
|
||||
tags=t.tags,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/audio-processing-templates",
|
||||
response_model=AudioProcessingTemplateListResponse,
|
||||
tags=["Audio Processing Templates"],
|
||||
)
|
||||
async def list_audio_processing_templates(
|
||||
_auth: AuthRequired,
|
||||
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
||||
):
|
||||
"""List all audio processing templates."""
|
||||
templates = store.get_all_templates()
|
||||
responses = [_apt_to_response(t) for t in templates]
|
||||
return AudioProcessingTemplateListResponse(templates=responses, count=len(responses))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/api/v1/audio-processing-templates",
|
||||
response_model=AudioProcessingTemplateResponse,
|
||||
tags=["Audio Processing Templates"],
|
||||
status_code=201,
|
||||
)
|
||||
async def create_audio_processing_template(
|
||||
data: AudioProcessingTemplateCreate,
|
||||
_auth: AuthRequired,
|
||||
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
||||
):
|
||||
"""Create a new audio processing template."""
|
||||
try:
|
||||
filters = [FilterInstance(f.filter_id, f.options) for f in data.filters]
|
||||
template = store.create_template(
|
||||
name=data.name,
|
||||
filters=filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("audio_processing_template", "created", template.id)
|
||||
return _apt_to_response(template)
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Failed to create audio processing template: %s", e, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/v1/audio-processing-templates/{template_id}",
|
||||
response_model=AudioProcessingTemplateResponse,
|
||||
tags=["Audio Processing Templates"],
|
||||
)
|
||||
async def get_audio_processing_template(
|
||||
template_id: str,
|
||||
_auth: AuthRequired,
|
||||
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
||||
):
|
||||
"""Get audio processing template by ID."""
|
||||
try:
|
||||
template = store.get_template(template_id)
|
||||
return _apt_to_response(template)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Audio processing template {template_id} not found"
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/api/v1/audio-processing-templates/{template_id}",
|
||||
response_model=AudioProcessingTemplateResponse,
|
||||
tags=["Audio Processing Templates"],
|
||||
)
|
||||
async def update_audio_processing_template(
|
||||
template_id: str,
|
||||
data: AudioProcessingTemplateUpdate,
|
||||
_auth: AuthRequired,
|
||||
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
||||
):
|
||||
"""Update an audio processing template."""
|
||||
try:
|
||||
filters = (
|
||||
[FilterInstance(f.filter_id, f.options) for f in data.filters]
|
||||
if data.filters is not None
|
||||
else None
|
||||
)
|
||||
template = store.update_template(
|
||||
template_id=template_id,
|
||||
name=data.name,
|
||||
filters=filters,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
)
|
||||
fire_entity_event("audio_processing_template", "updated", template_id)
|
||||
return _apt_to_response(template)
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Failed to update audio processing template: %s", e, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/api/v1/audio-processing-templates/{template_id}",
|
||||
status_code=204,
|
||||
tags=["Audio Processing Templates"],
|
||||
)
|
||||
async def delete_audio_processing_template(
|
||||
template_id: str,
|
||||
_auth: AuthRequired,
|
||||
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
||||
):
|
||||
"""Delete an audio processing template."""
|
||||
try:
|
||||
# TODO: Phase 3 will add reference checks against ProcessedAudioSource
|
||||
store.delete_template(template_id)
|
||||
fire_entity_event("audio_processing_template", "deleted", template_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except EntityNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete audio processing template: %s", e, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Audio processing template schemas."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .filters import FilterInstanceSchema
|
||||
|
||||
|
||||
class AudioProcessingTemplateCreate(BaseModel):
|
||||
"""Request to create an audio processing template."""
|
||||
|
||||
name: str = Field(description="Template name", min_length=1, max_length=100)
|
||||
filters: List[FilterInstanceSchema] = Field(
|
||||
default_factory=list, description="Ordered list of audio filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
|
||||
|
||||
class AudioProcessingTemplateUpdate(BaseModel):
|
||||
"""Request to update an audio processing template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
filters: Optional[List[FilterInstanceSchema]] = Field(
|
||||
None, description="Ordered list of audio filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
|
||||
class AudioProcessingTemplateResponse(BaseModel):
|
||||
"""Audio processing template information response."""
|
||||
|
||||
id: str = Field(description="Template ID")
|
||||
name: str = Field(description="Template name")
|
||||
filters: List[FilterInstanceSchema] = Field(
|
||||
description="Ordered list of audio filter instances"
|
||||
)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
|
||||
|
||||
class AudioProcessingTemplateListResponse(BaseModel):
|
||||
"""List of audio processing templates response."""
|
||||
|
||||
templates: List[AudioProcessingTemplateResponse] = Field(
|
||||
description="List of audio processing templates"
|
||||
)
|
||||
count: int = Field(description="Number of templates")
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Audio filter system.
|
||||
|
||||
Provides a pluggable filter architecture for audio analysis postprocessing.
|
||||
Import this package to ensure all built-in filters are registered.
|
||||
"""
|
||||
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
# Import individual filters to trigger auto-registration
|
||||
import wled_controller.core.audio.filters.audio_filter_template # noqa: F401
|
||||
|
||||
__all__ = [
|
||||
"AudioFilter",
|
||||
"AudioFilterOptionDef",
|
||||
"AudioFilterRegistry",
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Audio Filter Template meta-filter -- references another audio processing template.
|
||||
|
||||
This filter exists in the registry for UI discovery only. It is never
|
||||
instantiated at runtime: the audio processing pipeline expands it into the
|
||||
referenced template's filters when building the filter chain.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
from wled_controller.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
|
||||
|
||||
@AudioFilterRegistry.register
|
||||
class AudioFilterTemplateFilter(AudioFilter):
|
||||
"""Include another audio filter template's chain at this position."""
|
||||
|
||||
filter_id = "audio_filter_template"
|
||||
filter_name = "Audio Filter Template"
|
||||
|
||||
@classmethod
|
||||
def get_options_schema(cls) -> List[AudioFilterOptionDef]:
|
||||
return [
|
||||
AudioFilterOptionDef(
|
||||
key="template_id",
|
||||
label="Template",
|
||||
option_type="select",
|
||||
default="",
|
||||
min_value=None,
|
||||
max_value=None,
|
||||
step=None,
|
||||
choices=[], # populated dynamically by GET /api/v1/audio-filters
|
||||
),
|
||||
]
|
||||
|
||||
def process(self, analysis: AudioAnalysis) -> AudioAnalysis:
|
||||
# Never called -- expanded at pipeline build time.
|
||||
return analysis
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Base classes for the audio filter system."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from wled_controller.core.audio.analysis import AudioAnalysis
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioFilterOptionDef:
|
||||
"""Describes a single configurable option for an audio filter."""
|
||||
|
||||
key: str
|
||||
label: str
|
||||
option_type: str # "float" | "int" | "bool" | "select" | "string"
|
||||
default: Any
|
||||
min_value: Any
|
||||
max_value: Any
|
||||
step: Any
|
||||
choices: Optional[List[Dict[str, str]]] = None # for "select": [{value, label}]
|
||||
max_length: Optional[int] = None # for "string" type
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = {
|
||||
"key": self.key,
|
||||
"label": self.label,
|
||||
"type": self.option_type,
|
||||
"default": self.default,
|
||||
"min_value": self.min_value,
|
||||
"max_value": self.max_value,
|
||||
"step": self.step,
|
||||
}
|
||||
if self.choices is not None:
|
||||
d["choices"] = self.choices
|
||||
if self.max_length is not None:
|
||||
d["max_length"] = self.max_length
|
||||
return d
|
||||
|
||||
|
||||
class AudioFilter(ABC):
|
||||
"""Base class for all audio filters.
|
||||
|
||||
Each filter operates on an AudioAnalysis snapshot and returns
|
||||
a new (possibly transformed) AudioAnalysis.
|
||||
|
||||
Stateful filters (e.g. peak hold, envelope follower) must override
|
||||
``is_stateful`` to return True and implement ``reset()``.
|
||||
"""
|
||||
|
||||
filter_id: str = ""
|
||||
filter_name: str = ""
|
||||
|
||||
def __init__(self, options: Dict[str, Any]):
|
||||
"""Initialize filter with validated options."""
|
||||
self.options = self.validate_options(options)
|
||||
|
||||
@property
|
||||
def is_stateful(self) -> bool:
|
||||
"""Whether this filter maintains internal state across calls.
|
||||
|
||||
Stateful filters need per-stream instances and reset() support.
|
||||
"""
|
||||
return False
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset internal state. Override in stateful filters."""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_options_schema(cls) -> List[AudioFilterOptionDef]:
|
||||
"""Return the list of configurable options for this filter type."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def process(self, analysis: AudioAnalysis) -> AudioAnalysis:
|
||||
"""Process an audio analysis snapshot.
|
||||
|
||||
Args:
|
||||
analysis: Input AudioAnalysis snapshot.
|
||||
|
||||
Returns:
|
||||
New AudioAnalysis with transformations applied.
|
||||
"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def validate_options(cls, options: dict) -> dict:
|
||||
"""Validate and clamp options against the schema. Returns cleaned dict."""
|
||||
schema = cls.get_options_schema()
|
||||
cleaned = {}
|
||||
for opt_def in schema:
|
||||
raw = options.get(opt_def.key, opt_def.default)
|
||||
if opt_def.option_type == "float":
|
||||
val = float(raw)
|
||||
elif opt_def.option_type == "int":
|
||||
val = int(raw)
|
||||
elif opt_def.option_type == "bool":
|
||||
val = bool(raw) if not isinstance(raw, bool) else raw
|
||||
elif opt_def.option_type == "select":
|
||||
val = str(raw) if raw is not None else opt_def.default
|
||||
elif opt_def.option_type == "string":
|
||||
val = str(raw) if raw is not None else opt_def.default
|
||||
else:
|
||||
val = raw
|
||||
# Clamp to range (skip for non-numeric types)
|
||||
if opt_def.option_type not in ("bool", "select", "string"):
|
||||
if opt_def.min_value is not None and val < opt_def.min_value:
|
||||
val = opt_def.min_value
|
||||
if opt_def.max_value is not None and val > opt_def.max_value:
|
||||
val = opt_def.max_value
|
||||
cleaned[opt_def.key] = val
|
||||
return cleaned
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.options})"
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Audio filter registry for discovering and instantiating audio filters."""
|
||||
|
||||
from typing import Dict, Type
|
||||
|
||||
from wled_controller.core.audio.filters.base import AudioFilter
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AudioFilterRegistry:
|
||||
"""Singleton registry of all available audio filter types."""
|
||||
|
||||
_filters: Dict[str, Type[AudioFilter]] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, filter_cls: Type[AudioFilter]) -> Type[AudioFilter]:
|
||||
"""Register a filter class. Can be used as a decorator."""
|
||||
filter_id = filter_cls.filter_id
|
||||
if not filter_id:
|
||||
raise ValueError(f"Filter class {filter_cls.__name__} must define filter_id")
|
||||
if filter_id in cls._filters:
|
||||
logger.warning(f"Overwriting audio filter registration for '{filter_id}'")
|
||||
cls._filters[filter_id] = filter_cls
|
||||
logger.debug(f"Registered audio filter: {filter_id} ({filter_cls.__name__})")
|
||||
return filter_cls
|
||||
|
||||
@classmethod
|
||||
def get(cls, filter_id: str) -> Type[AudioFilter]:
|
||||
"""Get a filter class by ID.
|
||||
|
||||
Raises:
|
||||
ValueError: If filter_id is not registered.
|
||||
"""
|
||||
if filter_id not in cls._filters:
|
||||
raise ValueError(f"Unknown audio filter type: '{filter_id}'")
|
||||
return cls._filters[filter_id]
|
||||
|
||||
@classmethod
|
||||
def get_all(cls) -> Dict[str, Type[AudioFilter]]:
|
||||
"""Get all registered audio filter types."""
|
||||
return dict(cls._filters)
|
||||
|
||||
@classmethod
|
||||
def create_instance(cls, filter_id: str, options: dict) -> AudioFilter:
|
||||
"""Create a filter instance from a filter_id and options dict."""
|
||||
filter_cls = cls.get(filter_id)
|
||||
return filter_cls(options)
|
||||
|
||||
@classmethod
|
||||
def is_registered(cls, filter_id: str) -> bool:
|
||||
"""Check if a filter ID is registered."""
|
||||
return filter_id in cls._filters
|
||||
@@ -51,6 +51,8 @@ from wled_controller.core.game_integration.community_loader import register_comm
|
||||
from wled_controller.core.mqtt.mqtt_service import MQTTService
|
||||
from wled_controller.core.mqtt.mqtt_manager import MQTTManager
|
||||
from wled_controller.storage.mqtt_source_store import MQTTSourceStore
|
||||
from wled_controller.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
||||
import wled_controller.core.audio.filters # noqa: F401 — trigger audio filter auto-registration
|
||||
from wled_controller.core.devices.mqtt_client import set_mqtt_service
|
||||
from wled_controller.core.backup.auto_backup import AutoBackupEngine
|
||||
from wled_controller.core.processing.os_notification_listener import OsNotificationListener
|
||||
@@ -104,6 +106,7 @@ weather_manager = WeatherManager(weather_source_store)
|
||||
ha_store = HomeAssistantStore(db)
|
||||
ha_manager = HomeAssistantManager(ha_store)
|
||||
mqtt_source_store = MQTTSourceStore(db)
|
||||
audio_processing_template_store = AudioProcessingTemplateStore(db)
|
||||
game_integration_store = GameIntegrationStore(db)
|
||||
game_event_bus = GameEventBus()
|
||||
register_community_adapters()
|
||||
@@ -231,6 +234,7 @@ async def lifespan(app: FastAPI):
|
||||
game_event_bus=game_event_bus,
|
||||
mqtt_store=mqtt_source_store,
|
||||
mqtt_manager=mqtt_manager,
|
||||
audio_processing_template_store=audio_processing_template_store,
|
||||
)
|
||||
|
||||
# Register devices in processor manager for health monitoring
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Audio processing template data model."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
|
||||
from wled_controller.core.filters.filter_instance import FilterInstance
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioProcessingTemplate:
|
||||
"""Audio processing settings template containing an ordered list of audio filters."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
filters: List[FilterInstance]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert template to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"filters": [f.to_dict() for f in self.filters],
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"description": self.description,
|
||||
"tags": self.tags,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "AudioProcessingTemplate":
|
||||
"""Create template from dictionary."""
|
||||
filters = [FilterInstance.from_dict(f) for f in data.get("filters", [])]
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
filters=filters,
|
||||
created_at=(
|
||||
datetime.fromisoformat(data["created_at"])
|
||||
if isinstance(data.get("created_at"), str)
|
||||
else data.get("created_at", datetime.now(timezone.utc))
|
||||
),
|
||||
updated_at=(
|
||||
datetime.fromisoformat(data["updated_at"])
|
||||
if isinstance(data.get("updated_at"), str)
|
||||
else data.get("updated_at", datetime.now(timezone.utc))
|
||||
),
|
||||
description=data.get("description"),
|
||||
tags=data.get("tags", []),
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Audio processing template storage using SQLite."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
|
||||
from wled_controller.core.audio.filters.registry import AudioFilterRegistry
|
||||
from wled_controller.core.filters.filter_instance import FilterInstance
|
||||
from wled_controller.storage.audio_processing_template import AudioProcessingTemplate
|
||||
from wled_controller.storage.base_sqlite_store import BaseSqliteStore
|
||||
from wled_controller.storage.database import Database
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AudioProcessingTemplateStore(BaseSqliteStore[AudioProcessingTemplate]):
|
||||
"""Storage for audio processing templates.
|
||||
|
||||
All templates are persisted to the database.
|
||||
"""
|
||||
|
||||
_table_name = "audio_processing_templates"
|
||||
_entity_name = "Audio processing template"
|
||||
_version = "1.0.0"
|
||||
|
||||
def __init__(self, db: Database):
|
||||
super().__init__(db, AudioProcessingTemplate.from_dict)
|
||||
|
||||
# Backward-compatible aliases
|
||||
get_all_templates = BaseSqliteStore.get_all
|
||||
get_template = BaseSqliteStore.get
|
||||
delete_template = BaseSqliteStore.delete
|
||||
|
||||
def create_template(
|
||||
self,
|
||||
name: str,
|
||||
filters: Optional[List[FilterInstance]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
) -> AudioProcessingTemplate:
|
||||
self._check_name_unique(name)
|
||||
|
||||
if filters is None:
|
||||
filters = []
|
||||
|
||||
# Validate filter IDs
|
||||
for fi in filters:
|
||||
if not AudioFilterRegistry.is_registered(fi.filter_id):
|
||||
raise ValueError(f"Unknown audio filter type: '{fi.filter_id}'")
|
||||
|
||||
template_id = f"apt_{uuid.uuid4().hex[:8]}"
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
template = AudioProcessingTemplate(
|
||||
id=template_id,
|
||||
name=name,
|
||||
filters=filters,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
description=description,
|
||||
tags=tags or [],
|
||||
)
|
||||
|
||||
self._items[template_id] = template
|
||||
self._save_item(template_id, template)
|
||||
|
||||
logger.info(f"Created audio processing template: {name} ({template_id})")
|
||||
return template
|
||||
|
||||
def update_template(
|
||||
self,
|
||||
template_id: str,
|
||||
name: Optional[str] = None,
|
||||
filters: Optional[List[FilterInstance]] = None,
|
||||
description: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
) -> AudioProcessingTemplate:
|
||||
template = self.get(template_id)
|
||||
|
||||
if name is not None:
|
||||
self._check_name_unique(name, exclude_id=template_id)
|
||||
template.name = name
|
||||
if filters is not None:
|
||||
# Validate filter IDs
|
||||
for fi in filters:
|
||||
if not AudioFilterRegistry.is_registered(fi.filter_id):
|
||||
raise ValueError(f"Unknown audio filter type: '{fi.filter_id}'")
|
||||
template.filters = filters
|
||||
if description is not None:
|
||||
template.description = description
|
||||
if tags is not None:
|
||||
template.tags = tags
|
||||
|
||||
template.updated_at = datetime.now(timezone.utc)
|
||||
self._save_item(template_id, template)
|
||||
|
||||
logger.info(f"Updated audio processing template: {template_id}")
|
||||
return template
|
||||
|
||||
def resolve_filter_instances(self, filter_instances, _visited=None):
|
||||
"""Recursively resolve filter instances, expanding audio_filter_template references.
|
||||
|
||||
Returns a flat list of FilterInstance objects with no audio_filter_template entries.
|
||||
"""
|
||||
if _visited is None:
|
||||
_visited = set()
|
||||
resolved = []
|
||||
for fi in filter_instances:
|
||||
if fi.filter_id == "audio_filter_template":
|
||||
template_id = fi.options.get("template_id", "")
|
||||
if not template_id or template_id in _visited:
|
||||
continue
|
||||
try:
|
||||
ref_template = self.get_template(template_id)
|
||||
_visited.add(template_id)
|
||||
resolved.extend(self.resolve_filter_instances(ref_template.filters, _visited))
|
||||
_visited.discard(template_id)
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
f"Referenced audio filter template '{template_id}' not found, skipping"
|
||||
)
|
||||
else:
|
||||
resolved.append(fi)
|
||||
return resolved
|
||||
Reference in New Issue
Block a user