Player.tsx updates:
- Repeat button: ISO repeat indefinitely icon with glowing toggle state.
When active: lyria-accent color, bg-lyria-accent/10, orange glow shadow.
When inactive: muted gray, hover to white. Toggles audio.loop.
- Skip to beginning: Now uses ISO skip-backwards icon (filled, two bars
pointing left) instead of circular arrow. Title: 'Skip to beginning'.
- Track end handler respects repeat state — if repeating, stays playing
and loops; if not, pauses as before.
- New state: isRepeating (useState), toggleRepeat function sets both
React state and audioRef.current.loop.
Player.tsx completely rewritten with professional controls:
- Play/Pause: large circular button with play/pause icons
- Stop: square icon, resets audio to 0 and pauses
- Restart: circular arrow, jumps to beginning and plays
- Time display: elapsed / total (M:SS format, tabular-nums)
- Scrubber: clickable progress bar with drag support
- Visual progress fill (lyria-accent)
- White scrubber handle on hover
- Mouse drag anywhere on page while holding
- Volume: mute toggle + range slider (0-1, step 0.01)
- Three volume icons: full, half, muted
- Custom styled range input with white thumb
- Track info: title or prompt (hidden on mobile)
- Close button: X icon
- All buttons have title tooltips
- Uses useRef for audio element, useState for UI state
- useCallback for seek handler, useEffect for drag events
- Responsive: track info hidden on small screens
- Both danger zone rows now use flex items-start gap-4 for consistent
text+button alignment with proper breathing room
- Added shrink-0 to buttons so they don't compress
- Added whitespace-nowrap to Delete All button so text doesn't wrap
- Shortened confirm button text to just 'Confirm' (was 'Confirm Delete')
- Moved confirmation warning inline under the description text instead
of as a separate block below, keeping the row compact
- Shortened description text: 'Remove N clips. Cannot be undone.'
Backend:
- POST /api/admin/clear-waveforms — deletes all cached waveform SVGs
- POST /api/admin/delete-all-clips — nuclear option: deletes all clips,
MP3s, artwork, and waveforms. Returns deleted count.
- database.py: delete_all_clips() for bulk DB wipe
- Clip deletion cascade-removes waveform SVGs
Frontend:
- SettingsModal.tsx: center-stage modal with backdrop blur (bg-black/60
backdrop-blur-sm). Danger zone with red-styled border and header.
- Two-step confirmation for Delete All (click once to arm, click again
to confirm). Shows clip count dynamically.
- Gear icon trigger in header bar
- App.tsx: handleClearWaveforms + handleDeleteAll handlers
Docs:
- Architecture KB updated with album art, waveform, settings modal sections
- Tasks KB updated with T-021 (waveform) and TASK-001 (album art) marked done
- T-026 (settings modal) added to backlog
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
Ribbon view was missing the onClick handler that opens the detail panel.
Tile view had it; ribbon view did not. Now both views open the slide-out
panel when clicking a clip row/card.
Ribbon view artwork hover now shows only play button (larger, cleaner).
Upload art button moved to action row alongside rename/delete.
Order: rename → upload art → delete. All buttons 4x4, consistent spacing.
Replaces broken SVG path with Unicode U+1D11E treble clef symbol
rendered via SVG text element. Applied to all three locations:
- Gallery ribbon: small treble clef (5x5)
- Gallery tile: medium treble clef (10x10)
- DetailPanel: large treble clef (14x14)
Album art placeholders now show a treble clef (Rin's signature)
instead of generic icons. Applied to all three locations:
- Gallery ribbon: small treble clef (5x5)
- Gallery tile: medium treble clef (10x10)
- DetailPanel: large treble clef (14x14) with 'No album art' label
The Lyria API returns a candidates[].content.parts[] array where:
- parts[0] = text metadata (e.g. '<instrumental>')
- parts[1] = inlineData with the actual audio payload
Previous code only checked parts[0] for inlineData, which always failed
with 'No audio data in response'. Fixed by iterating all parts to find
the inlineData entry.
Successfully generated first real audio clip (lo-fi hip hop, 30s, 728KB MP3).
Also includes: .env.example, requirements updates, docker-compose fixes,
frontend vite config, logger setup.