feat: Add themed FFT waveform visualization to detail panel

Backend:
- Install ffmpeg + numpy in Docker image
- New waveform.py: FFT-based SVG generator with 64 frequency bands
- GET /api/clips/{id}/waveform — generates on demand, caches to /data/waveforms/
- DELETE /api/clips/{id} now cascade-removes waveform SVG
- Warm amber-to-orange HSL gradient (25° to 35° hue)
- Mirrored bars: top full opacity, bottom 60% opacity

Frontend:
- DetailPanel shows waveform image beneath album art (80px height)
- Only shown for clips with status='ready'

Storage: /data/waveforms/ persisted via existing ./data volume mount
This commit is contained in:
rin.kagamine committed 2026-05-29 20:11:01 -04:00
1 parent 3c6a5d1a84
commit 8f5c8a87d1
6 files changed
+330

No files matched your search

+24
View File
@@ -12,6 +12,7 @@ load_dotenv(Path(__file__).parent.parent / ".env")
from models import GenerateRequest, GenerateResponse, ClipList, RenameRequest, LYRIA_MODELS, DEFAULT_MODEL
from database import init_db, create_clip, get_clips, get_clip, delete_clip, update_clip_status, rename_clip, set_artwork, remove_artwork
from lyria_client import generate_clip, save_clip_file
from waveform import generate_waveform_svg, get_waveform_path
app = FastAPI(title="Lyria Studio", version="0.1.0")
@@ -124,10 +125,33 @@ async def remove_clip(clip_id: str):
artwork_path = ARTWORK_DIR / clip["artwork_filename"]
if artwork_path.exists():
artwork_path.unlink()
# Also delete waveform if it exists
waveform_path = get_waveform_path(clip_id)
if waveform_path and waveform_path.exists():
waveform_path.unlink()
await delete_clip(clip_id)
return {"deleted": True}
@app.get("/api/clips/{clip_id}/waveform")
async def get_waveform(clip_id: str):
clip = await get_clip(clip_id)
if not clip:
raise HTTPException(status_code=404, detail="Clip not found")
# Check if waveform already exists
svg_path = get_waveform_path(clip_id)
if svg_path and svg_path.exists():
return FileResponse(svg_path, media_type="image/svg+xml")
# Generate on demand
try:
svg_path_str = generate_waveform_svg(clip_id, clip["filename"])
return FileResponse(svg_path_str, media_type="image/svg+xml")
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Audio file not found")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Waveform generation failed: {str(e)}")
@app.get("/api/clips/{clip_id}/artwork")
async def get_artwork(clip_id: str):
clip = await get_clip(clip_id)