Files
wled-screen-controller-mixed/server/src/wled_controller/api/routes/audio_templates.py
alexei.dolgolyov 7380b33b9b
Some checks failed
Lint & Test / test (push) Failing after 9s
fix: resolve all 153 ruff lint errors for CI
Auto-fixed 138 unused imports and f-string issues. Manually fixed:
ambiguous variable names (l→layer), availability-check imports using
importlib.util.find_spec, unused Color import, ImagePool forward ref
via TYPE_CHECKING, multi-statement semicolons, and E402 suppression.
2026-03-22 01:29:26 +03:00

246 lines
9.2 KiB
Python

"""Audio capture template and engine routes."""
import asyncio
from fastapi import APIRouter, HTTPException, Depends, Query
from starlette.websockets import WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import fire_entity_event, get_audio_template_store, get_audio_source_store, get_processor_manager
from wled_controller.api.schemas.audio_templates import (
AudioEngineInfo,
AudioEngineListResponse,
AudioTemplateCreate,
AudioTemplateListResponse,
AudioTemplateResponse,
AudioTemplateUpdate,
)
from wled_controller.core.audio.factory import AudioEngineRegistry
from wled_controller.storage.audio_template_store import AudioTemplateStore
from wled_controller.storage.audio_source_store import AudioSourceStore
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError
logger = get_logger(__name__)
router = APIRouter()
# ===== AUDIO TEMPLATE ENDPOINTS =====
@router.get("/api/v1/audio-templates", response_model=AudioTemplateListResponse, tags=["Audio Templates"])
async def list_audio_templates(
_auth: AuthRequired,
store: AudioTemplateStore = Depends(get_audio_template_store),
):
"""List all audio capture templates."""
try:
templates = store.get_all_templates()
responses = [
AudioTemplateResponse(
id=t.id, name=t.name, engine_type=t.engine_type,
engine_config=t.engine_config, tags=t.tags,
created_at=t.created_at,
updated_at=t.updated_at, description=t.description,
)
for t in templates
]
return AudioTemplateListResponse(templates=responses, count=len(responses))
except Exception as e:
logger.error(f"Failed to list audio templates: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/v1/audio-templates", response_model=AudioTemplateResponse, tags=["Audio Templates"], status_code=201)
async def create_audio_template(
data: AudioTemplateCreate,
_auth: AuthRequired,
store: AudioTemplateStore = Depends(get_audio_template_store),
):
"""Create a new audio capture template."""
try:
template = store.create_template(
name=data.name, engine_type=data.engine_type,
engine_config=data.engine_config, description=data.description,
tags=data.tags,
)
fire_entity_event("audio_template", "created", template.id)
return AudioTemplateResponse(
id=template.id, name=template.name, engine_type=template.engine_type,
engine_config=template.engine_config, tags=template.tags,
created_at=template.created_at,
updated_at=template.updated_at, description=template.description,
)
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(f"Failed to create audio template: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/v1/audio-templates/{template_id}", response_model=AudioTemplateResponse, tags=["Audio Templates"])
async def get_audio_template(
template_id: str,
_auth: AuthRequired,
store: AudioTemplateStore = Depends(get_audio_template_store),
):
"""Get audio template by ID."""
try:
t = store.get_template(template_id)
except ValueError:
raise HTTPException(status_code=404, detail=f"Audio template {template_id} not found")
return AudioTemplateResponse(
id=t.id, name=t.name, engine_type=t.engine_type,
engine_config=t.engine_config, tags=t.tags,
created_at=t.created_at,
updated_at=t.updated_at, description=t.description,
)
@router.put("/api/v1/audio-templates/{template_id}", response_model=AudioTemplateResponse, tags=["Audio Templates"])
async def update_audio_template(
template_id: str,
data: AudioTemplateUpdate,
_auth: AuthRequired,
store: AudioTemplateStore = Depends(get_audio_template_store),
):
"""Update an audio template."""
try:
t = store.update_template(
template_id=template_id, name=data.name,
engine_type=data.engine_type, engine_config=data.engine_config,
description=data.description, tags=data.tags,
)
fire_entity_event("audio_template", "updated", template_id)
return AudioTemplateResponse(
id=t.id, name=t.name, engine_type=t.engine_type,
engine_config=t.engine_config, tags=t.tags,
created_at=t.created_at,
updated_at=t.updated_at, description=t.description,
)
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(f"Failed to update audio template: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/api/v1/audio-templates/{template_id}", status_code=204, tags=["Audio Templates"])
async def delete_audio_template(
template_id: str,
_auth: AuthRequired,
store: AudioTemplateStore = Depends(get_audio_template_store),
audio_source_store: AudioSourceStore = Depends(get_audio_source_store),
):
"""Delete an audio template."""
try:
store.delete_template(template_id, audio_source_store=audio_source_store)
fire_entity_event("audio_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(f"Failed to delete audio template: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ===== AUDIO ENGINE ENDPOINTS =====
@router.get("/api/v1/audio-engines", response_model=AudioEngineListResponse, tags=["Audio Templates"])
async def list_audio_engines(_auth: AuthRequired):
"""List all registered audio capture engines."""
try:
available_set = set(AudioEngineRegistry.get_available_engines())
all_engines = AudioEngineRegistry.get_all_engines()
engines = []
for engine_type, engine_class in all_engines.items():
engines.append(
AudioEngineInfo(
type=engine_type,
name=engine_type.upper(),
default_config=engine_class.get_default_config(),
available=(engine_type in available_set),
)
)
return AudioEngineListResponse(engines=engines, count=len(engines))
except Exception as e:
logger.error(f"Failed to list audio engines: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ===== REAL-TIME AUDIO TEMPLATE TEST WEBSOCKET =====
@router.websocket("/api/v1/audio-templates/{template_id}/test/ws")
async def test_audio_template_ws(
websocket: WebSocket,
template_id: str,
token: str = Query(""),
device_index: int = Query(-1),
is_loopback: int = Query(1),
):
"""WebSocket for real-time audio spectrum test of a template with a chosen device.
Auth via ?token=<api_key>. Device specified via ?device_index=N&is_loopback=0|1.
Streams AudioAnalysis snapshots as JSON at ~20 Hz.
"""
from wled_controller.api.auth import verify_ws_token
if not verify_ws_token(token):
await websocket.close(code=4001, reason="Unauthorized")
return
# Resolve template
store = get_audio_template_store()
try:
template = store.get_template(template_id)
except ValueError:
await websocket.close(code=4004, reason="Template not found")
return
# Acquire shared audio stream
manager = get_processor_manager()
audio_mgr = manager.audio_capture_manager
loopback = is_loopback != 0
try:
stream = audio_mgr.acquire(device_index, loopback, template.engine_type, template.engine_config)
except RuntimeError as e:
await websocket.close(code=4003, reason=str(e))
return
await websocket.accept()
logger.info(f"Audio template test WS connected: template={template_id} device={device_index} loopback={loopback}")
last_ts = 0.0
try:
while True:
analysis = stream.get_latest_analysis()
if analysis is not None and analysis.timestamp != last_ts:
last_ts = analysis.timestamp
await websocket.send_json({
"spectrum": analysis.spectrum.tolist(),
"rms": round(analysis.rms, 4),
"peak": round(analysis.peak, 4),
"beat": analysis.beat,
"beat_intensity": round(analysis.beat_intensity, 4),
})
await asyncio.sleep(0.05)
except WebSocketDisconnect:
pass
except Exception as e:
logger.error(f"Audio template test WS error: {e}")
finally:
audio_mgr.release(device_index, loopback, template.engine_type)
logger.info(f"Audio template test WS disconnected: template={template_id}")