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:
@@ -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")
|
||||
Reference in New Issue
Block a user