Commit Graph
45 Commits
Author SHA1 Message Date
rin.kagamine ca18f03a0f fix: nginx audio streaming - pass Range headers for proper playback
The nginx proxy was stripping Range and If-Range headers, forcing the
browser to buffer entire MP3 files before playback could start.
Added Range header passthrough, disabled proxy_buffering for audio
streaming, and increased read timeout.
2026-05-31 10:19:06 -04:00
rin.kagamine e0ce9e4b6c feat: Maestro Mode - context-aware prompt generation
- Backend: POST /api/text-gen/maestro endpoint with dual modes
  - Empty prompt → generate from scratch (temp 1.0)
  - Non-empty prompt → complete/extend (temp 0.85)
  - structured=true → returns JSON fields for Advanced/Pro mode
  - Three system prompts: MAESTRO_GENERATE, MAESTRO_COMPLETE, MAESTRO_STRUCTURED
  - Trailing sentence cleanup for plain text mode
  - JSON fallback parsing for structured mode

- Frontend: MaestroButton component
  - Context-aware labels: ♪ Maestro Mode (empty) / ♪ Keep Composing (has content)
  - Loading states: animating ♩ with Dreaming.../Composing...
  - structured prop for Pro mode → populates structured fields
  - onStructuredResult callback merges fields into StructuredPrompt

- UX: Replaced 'Get Suggestions ✨' with Maestro Mode
  - Suggestions required text in the box; Maestro works with or without
  - Single button, context-aware behavior, cleaner UI
  - Advanced/Pro mode gets structured fields from Maestro
  - Removed SuggestionsModal dependency

- Rename: 'Compose for Me' → 'Keep Composing'
2026-05-31 10:13:43 -04:00
rin.kagamine 816f8fbd2f refactor: rename project Lyria Studio → Kithara Studio
- App name, UI text, page titles → Kithara Studio
- Python module: lyria_client.py → kithara_client.py
- Tailwind color classes: lyria-* → kithara-*
- CSS custom properties: --lyria-* → --kithara-*
- Database: lyria.db → kithara.db
- Package name: kithara-studio-frontend
- localStorage key: kithara-saved-searches
- Add project logo (lyre icon, black on transparent, invert via CSS)

Preserved as Google's property:
- LYRIA_API_KEY, LYRIA_BASE_URL, LYRIA_DEFAULT_MODEL env vars
- LYRIA_MODELS dict and model IDs (lyria-3-clip-preview, lyria-3-pro-preview)
- Google Lyria API references in error messages and docstrings
2026-05-31 09:45:22 -04:00
rin.kagamine 6c1e1b4509 Add test suite for Lyria Studio backend
- conftest.py: isolated async test fixtures with temp DB and directories
- test_waveform.py: waveform generation, downsampling, caching, SVG output
- test_api.py: full API endpoint tests (health, models, clips CRUD,
  waveform endpoint, admin cache clearing)
- test_analysis.py: BPM detection, key detection, clip analysis
- test_models.py: request validation, model registry, prompt building

- Fixed off-by-one in waveform.py downsampling (linspace upper bound)
- All 40 tests passing
2026-05-31 08:26:01 -04:00
rin.kagamine 3500c73167 DR-LYRIA-001: Waveform performance overhaul
- Convert generate_waveform_svg() from sync to async
  (subprocess.run -> asyncio.create_subprocess_exec)
- Add early downsampling to ~2000 samples before numpy processing
  (~95% memory reduction for 120s tracks)
- Pre-generate waveform on clip completion in _generate_and_save()
- Add Cache-Control headers (public, max-age=86400) to waveform endpoint
- Preserve on-demand generation as lazy fallback
2026-05-31 08:14:54 -04:00
rin.kagamine f8969d8585 feat: Complete search overhaul — fuzzy, filters, highlighting, debounce, saved searches
Search improvements (all 7 recommendations implemented):

1. Search all metadata fields:
   - Fuzzy search now includes: title, prompt, artist, album, comment,
     model, status, error_message, key, BPM, duration, structured prompt fields
   - Built _searchText concatenated field for Fuse.js indexing

2. Fuzzy search via fuse.js:
   - Installed fuse.js@7.0.0
   - threshold: 0.4 (balanced precision/recall)
   - ignoreLocation: true (matches anywhere in text)
   - includeScore for ranking

3. Advanced filters panel:
   - Model: All / Clip Preview / Pro
   - Status: All / Ready / Generating / Failed
   - Artwork: All / Has Artwork / No Artwork
   - BPM Range: Min/Max number inputs
   - Date Range: From/To date pickers
   - Active filter chips shown below search bar
   - Clear all button

4. Search term highlighting:
   - HighlightText component wraps matched terms in
     bg-lyria-accent/30 background
   - Applied to title and prompt in both ribbon and tile views

5. Debounced input (300ms):
   - useDebounce hook delays search execution
   - Prevents filtering on every keystroke
   - Smoother UX, less CPU usage

6. Saved searches:
   - Save current search + filters with custom name
   - Stored in localStorage (lyria-saved-searches)
   - Load saved search to restore query + filters
   - Delete saved searches
   - Bookmark icon in search bar

7. Backend search endpoint:
   - GET /api/clips/search?q=&model=&status=&bpm_min=&bpm_max=&date_from=&date_to=&limit=&offset=
   - Full pagination support for large libraries
   - Same fuzzy logic as frontend, server-side

Gallery component completely rewritten with:
- Cleaner header layout with search, filters, saved searches, view toggle
- Results count showing filtered/total
- Empty state when no matches
- Pagination with page info
2026-05-29 23:15:08 -04:00
rin.kagamine b3807f0f60 fix: Danger Zone collapsible in SettingsModal
Previously the Danger Zone was always expanded, unlike the Image Generation
and Text Generation sections which were collapsible.

Now:
- Danger Zone has a clickable header with warning icon + chevron
- Collapsed by default (consistent with other sections)
- Click to expand and reveal Clear Waveforms + Delete All buttons
- Chevron rotates on expand/collapse
- Red styling maintained for visual distinction

This keeps the settings modal clean and uncluttered by default.
2026-05-29 23:03:51 -04:00
rin.kagamine fe6ec467c6 fix: SuggestionsModal now fetches fresh suggestions every time
Bug: SuggestionsModal only fetched on first open because it checked
if suggestions.length === 0 before fetching. This meant changing the
prompt and re-opening the modal showed stale suggestions.

Fix: useEffect now:
- Clears suggestions and error on every open
- Always fetches fresh suggestions when isOpen changes
- Also re-fetches when userInput changes while modal is open
- Dependency array: [isOpen, userInput]

Result: Every click of 'Get Suggestions' generates new, relevant
suggestions based on the current prompt.
2026-05-29 22:57:14 -04:00
rin.kagamine a713488fb7 feat: Metadata fields (artist, album, comment) with ID3 tag export
Backend:
- Added artist, album, comment columns to clips DB (migration in init_db)
- New update_metadata() function in database.py
- New PATCH /api/clips/{id}/metadata endpoint
- Updated download endpoint to write ID3 tags using mutagen:
  * TIT2 (Title) — from clip title
  * TPE1 (Artist) — from clip artist
  * TALB (Album) — from clip album
  * COMM (Comment) — from clip comment
  * TBPM (BPM) — from detected BPM
  * TKEY (Key) — from detected key
  * TIT1 (Content Group) — prompt for reference
- Added mutagen==1.47.0 to requirements.txt
- Creates temporary copy with tags, original file untouched

Frontend:
- Clip interface updated with artist, album, comment fields
- New MetadataField component in DetailPanel:
  * Inline editing with click-to-edit
  * Input field with lyria-accent border
  * Enter to save, Escape to cancel, blur to save
  * Pencil icon on hover
  * Shows placeholder when empty
- App.tsx: handleUpdateMetadata() function, passed to DetailPanel
- Updates state across clips, currentClip, selectedClip

Architecture KB and Tasks KB updated.
2026-05-29 22:55:01 -04:00
rin.kagamine e687f7d457 feat: Text generation suggestions via Gemini models
Backend:
- New text_gen.py module with Gemini text generation client
  - suggest_prompts(): generates 3 prompt suggestions from user input
  - get_text_gen_config(): loads config from .env (model, API key, safety)
  - get_available_models(): returns list of Gemini text models
  - Safety settings: harassment, hate_speech, sexually_explicit, dangerous
    Configurable via TEXT_GEN_SAFETY_* env vars (default: BLOCK_ONLY_HIGH)
- New endpoints:
  * POST /api/text-gen/suggest — generates 3 prompt suggestions
  * GET /api/text-gen/models — lists available text models
  * GET /api/text-gen/config — returns safe config (API key masked)
  * POST /api/text-gen/config — updates config in-memory
- Default model: gemini-2.5-flash
- Fallback API key: LYRIA_API_KEY if TEXT_GEN_API_KEY not set
- System prompt: expert music producer persona for rich musical detail

Frontend:
- New SuggestionsModal component for displaying generated suggestions
  - Auto-fetches when opened
  - Shows loading spinner, error state, or suggestion list
  - Each suggestion clickable to populate the prompt field
  - Hover effect: 'Click to use this prompt →'
- Generator component updated with 'Get Suggestions ✨' button
  - Appears next to prompt label in both Basic and Advanced modes
  - Disabled when no input present
  - Clicking suggestion populates the prompt/structured fields
- SettingsModal expanded with Text Generation section (collapsible)
  - Same pattern as Image Generation: model, API key status, safety filters
  - Editable dropdowns with Save Changes button
  - Refactored to use shared useGenSettings hook for both sections

Models Available:
- gemini-2.5-flash (GA, default) — best price-to-performance
- gemini-2.5-pro (GA) — highest quality reasoning
- gemini-3.5-flash (Preview) — next-gen Flash
- gemini-3.1-flash (Preview) — latest with thinking

Architecture KB and Tasks KB updated.
2026-05-29 22:37:35 -04:00
rin.kagamine 70beb89ad9 feat: Structured prompt editing for Pro model
Backend:
- Added prompt_structure JSON column to clips DB (migration in init_db)
- Updated GenerateRequest model with optional prompt_structure dict
- build_prompt() method combines structured fields into single prompt:
  'Generate a track with the following elements:\n\n**Style:** [style]\n...'
- create_clip() accepts and stores prompt_structure JSON
- get_clips()/get_clip() parse prompt_structure back to dict

Frontend:
- New StructuredPromptEditor component with 8 collapsible fields:
  Style, Timbre, Structure, Mood, Tempo, Instrumentation, Effects, Additional
  - Fields show filled/empty state with colored dots
  - Expand/collapse individual fields
  - 'Expand All' button
  - Counter: 'X of 8 fields filled'
- Generator component updated with Basic/Advanced mode toggle (Pro only)
  - Basic: single textarea (works for both models)
  - Advanced: StructuredPromptEditor (Pro only)
  - Mode resets to Basic when switching to Clip Preview
- DetailPanel shows structured prompt breakdown when present
  - Each filled field shown in its own labeled box
  - Style, Timbre, Structure, Mood, Tempo, Instrumentation, Effects, Additional
- Clip interface updated with prompt_structure field across all components

Architecture KB and Tasks KB updated.
2026-05-29 22:28:52 -04:00
rin.kagamine a79b2806dc feat: Editable safety settings in SettingsModal
Backend:
- New POST /api/image-gen/config endpoint to update image generation config
  in-memory (applies immediately, resets on server restart)
- Accepts: model, safety_harassment, safety_hate_speech,
  safety_sexually_explicit, safety_dangerous

Frontend:
- SettingsModal safety filters are now editable dropdowns (no longer disabled)
- Model selection is also editable
- Added 'Save Changes' button with visual feedback:
  * Green 'Settings saved!' on success
  * Red 'Failed to save' on error
  * Disabled when no changes made
- Tracks edited state vs. saved state
- Note: changes apply immediately but reset on server restart
  (set .env for persistence)

Fixed TypeScript issues:
- getConfigValue() properly typed to return string only
- Proper union type for config keys excluding has_api_key
2026-05-29 22:15:34 -04:00
rin.kagamine 6410e16912 fix: Darker modal backdrops for better contrast
SettingsModal + ArtworkGenerationModal:
- Backdrop: bg-black/60 → bg-black/80 (darker, less bleed-through)
- Backdrop blur: backdrop-blur-sm → backdrop-blur-md (stronger blur effect)
- Modal surface: added lyria-surface color (#1a1a24) to tailwind config
  for solid, opaque modal backgrounds

Result: Background elements are much less visible behind modals,
improving readability and focus.
2026-05-29 22:10:21 -04:00
rin.kagamine 934d7775a8 feat: Album artwork generation via Gemini Image models
Backend:
- New image_gen.py module with Gemini image generation client
  - generate_artwork(): generates PNG from text prompt using Google GenAI
  - get_image_gen_config(): loads config from .env (model, API key, safety)
  - get_available_models(): returns list of Gemini image models
  - Safety settings: harassment, hate_speech, sexually_explicit, dangerous
    Configurable via IMAGE_GEN_SAFETY_* env vars (default: BLOCK_ONLY_HIGH)
- New endpoints:
  * POST /api/clips/{id}/generate-artwork — generates and saves artwork
  * GET /api/image-gen/models — lists available models
  * GET /api/image-gen/config — returns safe config (API key masked)
- Default model: gemini-3.1-flash-image-preview
- Fallback API key: LYRIA_API_KEY if IMAGE_GEN_API_KEY not set
- Added google-genai==1.0.0 to requirements.txt

Frontend:
- New ArtworkGenerationModal component for prompt input
  - Textarea with placeholder, loading spinner during generation
  - Styled consistent with SettingsModal (backdrop blur, rounded, dark)
- SettingsModal expanded with Image Generation section (collapsible)
  - Shows current model, API key status, safety filter levels
  - Read-only display — values configured via .env
- DetailPanel: 'Generate' button added next to Upload/Change
  - Passes onGenerateArtwork prop to App.tsx
- App.tsx: handles artwork generation flow, updates state on success

Architecture KB and Tasks KB updated.
2026-05-29 22:05:47 -04:00
rin.kagamine fa3515acc9 feat: Smart play/pause overlay on DetailPanel album art
DetailPanel.tsx:
- Added isPlaying and currentClipId props to track playback state
- Added onTogglePlay prop for pausing currently-playing track
- Album art click handler now smart:
  * If this track IS playing → pause it (onTogglePlay)
  * If this track is NOT playing → play it (onPlay)
- Hover overlay icon adapts dynamically:
  * Currently playing + hover → shows PAUSE icon (⏸)
  * Not playing + hover → shows PLAY icon (▶)
- Works for both artwork images and placeholder state

App.tsx:
- Passes isPlaying, currentClipId, and onTogglePlay to DetailPanel

Visual states:
- Different track in panel: ▶ on hover (click to play)
- Same track playing: ⏸ on hover (click to pause)
- Same track paused: ▶ on hover (click to resume)
2026-05-29 21:49:36 -04:00
rin.kagamine 4808406113 feat: Click album art in DetailPanel to play track
DetailPanel.tsx:
- Added optional onPlay prop to interface
- Album art image now wrapped in clickable container with cursor-pointer
- Hover overlay: semi-transparent black bg with white play triangle icon
- Works for both artwork images and placeholder (treble clef) state
- Placeholder gets hover:bg-white/10 for subtle feedback
- App.tsx: passes handlePlay to DetailPanel onPlay prop

Visual feedback:
- group-hover opacity transition on overlay (0 → 1)
- Play icon: w-12 h-12 white triangle (▶) centered
- Smooth 150ms transition on overlay opacity
2026-05-29 21:46:58 -04:00
rin.kagamine dff48eac13 fix: Add margin-right to player download button for better spacing
Player.tsx: Download button now has mr-2 (margin-right: 0.5rem) to
create more breathing room between the download icon and the album art
thumbnail.
2026-05-29 21:44:10 -04:00
rin.kagamine 4ca89577d7 feat: Add download button to player bar
Player.tsx: Download button added to the left of the album art thumbnail
in the track info section (hidden on mobile). Uses window.open() to trigger
browser download. Styled as text-lyria-muted/60 base, hover to lyria-accent.
Consistent with secondary action tier styling.
2026-05-29 21:42:29 -04:00
rin.kagamine 905201f239 fix: Establish clear button color hierarchy in Gallery
Button color tiers (all views):
- Tier 1 (Primary actions): Download, Delete — text-lyria-muted base,
  hover to accent/red. Always visible.
- Tier 2 (Secondary actions): Rename, Artwork upload — text-lyria-muted/60
  base, hover to white. Subtler but still discoverable.

Previously all buttons were text-lyria-muted/40 (barely visible) or mixed
inconsistently. Now there's a clear visual hierarchy:
  Primary > Secondary > Background elements

Applies to both ribbon view (w-4 h-4 icons) and tile view (w-3 h-3 icons).
2026-05-29 21:36:20 -04:00
rin.kagamine 119b75fbcd feat: MP3 download/export capability
Backend:
- New endpoint: GET /api/clips/{id}/download
  - Returns MP3 with Content-Disposition: attachment header
  - Filename derived from clip title (sanitized) or prompt, falls back to clip ID
  - Sanitization: alphanumeric + spaces/hyphens/underscores only, max 60 chars
  - Uses FastAPI FileResponse with media_type=audio/mpeg

Frontend:
- Gallery ribbons: Download button (downward arrow icon) next to delete button
  - Uses window.open() to trigger browser download
  - Hover: lyria-accent color
- Gallery tiles: Download button in tile footer action row
  - Small icon (w-3 h-3), same hover behavior
- DetailPanel: Full-width 'Download MP3' button above Delete Track
  - lyria-accent styled, with download icon
  - Uses <a href> with download attribute for native browser handling

Architecture KB updated with Export/Download section.
2026-05-29 21:32:41 -04:00
rin.kagamine 57b9f9d78e fix: Correct key/BPM colors and cleaner badge text
Key color fix:
- Previous offset put D# major at green (~150°) instead of orange (~30°)
- New offset: (position + 8) * 30° so position 5 (D#/Eb) = 390° mod 360 = 30° orange
- Position 8 (C) = 480° mod 360 = 120° green. Adjacent positions = adjacent hues.

BPM color fix:
- Previous wrap-around math went blue→purple→red, missing cyan/green/yellow/orange
- New: simple linear interpolation 240° → 0° (long way: 240→180→120→60→0)
- 60 BPM = blue (240°), 120 BPM = green (120°), 180 BPM = red (0°)

Badge text cleanup:
- BPM: just the number (e.g., '140'), no 'BPM' text. Tooltip shows full value.
- Key: shortened format — 'D#m' for minor, 'D#' for major. Unicode flats/sharps.
  Tooltip shows full 'D# major' / 'D# minor'.
- DetailPanel: same short format for consistency.

Auto-fetch analysis:
- App.tsx fetchClips() now fetches analysis for any ready clips missing BPM/key
- Runs in background after clips load, updates state silently
- No more needing to click into detail panel to see colors
2026-05-29 21:27:12 -04:00
rin.kagamine 2dcb297087 feat: Camelot wheel key colors + BPM gradient for all badges
New musicColors.ts utility:
- getKeyColor(): Maps musical key to Camelot wheel position → hue.
  12 positions × 30° = full color wheel. Major = brighter/more saturated.
  Minor = slightly darker/less saturated. Returns {bg, text, border}.
- getBPMColor(): Gradient from slow (blue, 240°) to fast (red, 0°).
  Default range: 60 BPM → 180 BPM. Configurable via BPMRange.
  Clamps outside range. Returns {bg, text, border}.

Gallery.tsx:
- Ribbons: BPM and key badges now use dynamic HSL colors
- Tiles: Same colored badges in tile footer
- Both use inline styles for bg/text/border

DetailPanel.tsx:
- BPM and key metadata items now show as colored badges
- Same color system as gallery badges

All badges: subtle translucent background, colored text, thin colored border.
Consistent visual language across ribbon, tile, and detail views.
2026-05-29 21:16:47 -04:00
rin.kagamine 1e21b7dcc8 feat: BPM and musical key detection for all clips
Backend:
- New analysis.py module using librosa for BPM + key detection
  - detect_bpm(): librosa.beat.beat_track() returns tempo
  - detect_key(): Krumhansl-Schmuckler key profiles on chroma features
  - analyze_clip(): combined call returning {bpm, key}
- New dependency: librosa==0.10.2 + soundfile==0.12.1
- Database migrations: bpm (REAL) and key (TEXT) columns added
- New set_analysis() function in database.py
- New endpoint: GET /api/clips/{id}/analysis
  - Returns cached analysis if available
  - Computes on-demand if missing, caches result
  - 400 if clip not ready, 404 if no audio file

Frontend:
- Clip interface extended with bpm?: number | null, key?: string | null
- Gallery: BPM and key badges shown in ribbon and tile views
  - BPM badge: lyria-accent background
  - Key badge: white/5 background
- DetailPanel: BPM and key shown in metadata grid when available
- App.tsx: handleSelectClip() fetches analysis on-demand when opening
  detail panel if not already cached

Docs:
- Architecture KB updated with Audio Analysis section
- Tasks KB: T-028 marked In Progress → will mark Done after commit
2026-05-29 21:09:04 -04:00
rin.kagamine 2d47f77820 tweak: Even faster marquee scroll
Min duration: 4s → 3s
Per-character multiplier: 0.2s → 0.15s

A 20-character title now scrolls in ~6s.
2026-05-29 20:55:25 -04:00
rin.kagamine 4f30245850 tweak: Faster marquee scroll speed
Reduced marquee animation duration:
- Min duration: 6s → 4s
- Per-character multiplier: 0.3s → 0.2s

A 20-character title now scrolls in ~8s instead of ~12s.
2026-05-29 20:53:40 -04:00
rin.kagamine 17ff0b2157 fix: Make marquee text visible on hover by removing overflow clip
The marquee animation was working but the text was still clipped by
overflow:hidden on both the parent container and the text div itself.
Added group-hover:overflow-visible to both containers so the full
text becomes visible when the marquee animation starts on hover.
2026-05-29 20:52:28 -04:00
rin.kagamine a14ec40872 fix: Register marquee animation in Tailwind config
The marquee scroll effect wasn't working because 'animate-marquee'
was defined as an inline CSS class, not a Tailwind utility. Tailwind's
JIT compiler strips unknown classes, so group-hover:animate-marquee
was being removed at build time.

Fix: Added 'marquee-scroll' keyframes and 'marquee' animation to
tailwind.config.js theme.extend. Now 'animate-marquee' is a proper
Tailwind utility that works with group-hover and responsive variants.

Also removed the inline <style> block from Player.tsx since the
animation is now handled by Tailwind.
2026-05-29 20:50:36 -04:00
rin.kagamine a33df8a4ef chore: Add artwork and waveforms to .gitignore
Prevent generated files (album art images and waveform SVGs) from
accidentally being committed to the repo.
2026-05-29 20:46:17 -04:00
rin.kagamine 5e18d77e29 feat: Marquee scrolling track name in player bar
Player.tsx:
- Track name now scrolls horizontally when hovered (marquee effect)
- Uses CSS @keyframes animation with duplicated text for seamless loop
- Animation duration scales with text length (min 6s, ~0.3s per char)
- Default state: truncate with ellipsis. Hover: whitespace-nowrap + animate
- Wrapped player in React fragment with inline <style> for keyframes
- Uses Tailwind group-hover to trigger animation only on hover
2026-05-29 20:46:05 -04:00
rin.kagamine 56bb484fd9 feat: Inline title editing in DetailPanel — no more JS prompt
DetailPanel.tsx:
- Replaced window.prompt with native inline editing
- Click title text or 'Rename' button to enter edit mode
- Input field with lyria-accent border, focus ring, and placeholder
- Auto-focus + select-all on enter edit mode (useEffect)
- Enter to save, Escape to cancel, blur to save
- Title text has hover:bg-white/5 for visual feedback
- Non-editing title is clickable (cursor-pointer, title tooltip)
- Uses useState for edit mode and draft text, useRef for input focus
2026-05-29 20:41:16 -04:00
rin.kagamine 16f2dd6788 fix: Add bottom padding to main content for player clearance
App.tsx main element now has pb-24 (96px bottom padding) so the
gallery content always has breathing room above the fixed player bar.
Prevents content from sitting flush against the player controls.
2026-05-29 20:39:12 -04:00
rin.kagamine d77d9100fb feat: Add album art thumbnail to player bar
Player.tsx track info section now shows a 32x32 album art thumbnail
left of the track name. Uses the clip's artwork endpoint with an
onError fallback to the treble clef placeholder (same SVG text
approach as gallery placeholders). Hidden on mobile (sm breakpoint).
Layout: flex row with 10px gap between thumbnail and title text.
2026-05-29 20:36:57 -04:00
rin.kagamine b0755b9063 feat: Add repeat button and fix skip-to-beginning icon
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.
2026-05-29 20:35:00 -04:00
rin.kagamine 8218234eb9 feat: Full audio player with controls and scrubber
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
2026-05-29 20:28:20 -04:00
rin.kagamine 4fab947d6c fix: Clean up SettingsModal layout
- 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.'
2026-05-29 20:23:33 -04:00
rin.kagamine e74c89e1b0 feat: Settings modal with danger zone admin operations
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
2026-05-29 20:21:05 -04:00
rin.kagamine 744f0ae6e5 feat: Redesign waveform as amplitude-over-time visualization
Replaces 64-band FFT frequency spectrum with 200-bar amplitude envelope:
- RMS amplitude per time slice — shows actual track dynamics
- Quiet intros, loud drops, silence gaps all visible
- Per-track normalization — each track uses its full dynamic range
- Gentle compression (power 0.7) so quiet parts remain visible
- 3-point weighted smoothing preserves dynamics while looking polished
- Time-progressive HSL gradient: deep amber (20°) → bright orange (40°)
- Single centered bars with rounded caps — classic waveform look
- 160px height, 800px width, tight 1px gap for dense visualization
- Cleared cached waveforms so all clips regenerate with new algorithm
2026-05-29 20:15:50 -04:00
rin.kagamine 8f5c8a87d1 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
2026-05-29 20:11:01 -04:00
rin.kagamine 3c6a5d1a84 fix: Add onSelect click handler to ribbon view rows
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.
2026-05-29 19:57:07 -04:00
rin.kagamine 5868be5f93 fix: Move album art upload button out of ribbon thumbnail
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.
2026-05-29 19:55:06 -04:00
rin.kagamine 069170398f fix: Use proper Unicode treble clef (𝄞) for album art placeholders
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)
2026-05-29 19:41:49 -04:00
rin.kagamine c21a5f50e0 feat: Replace placeholder icons with treble clef SVG
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
2026-05-29 18:58:34 -04:00
rin.kagamine 8ba7133269 feat: Add album artwork support
- Backend: artwork_filename column on clips table (auto-migration)
- Backend: PATCH /api/clips/{id}/artwork (upload image, 5MB limit)
- Backend: GET /api/clips/{id}/artwork (serve image with correct content-type)
- Backend: DELETE /api/clips/{id}/artwork (remove artwork file + null out DB)
- Backend: DELETE /api/clips/{id} now cascade-removes artwork file
- Frontend: Clip interface includes artwork_filename
- Frontend: Gallery ribbons show artwork thumbnail with hover overlay (play + upload)
- Frontend: Gallery tiles show artwork with hover overlay (play + upload)
- Frontend: DetailPanel shows full artwork with upload/change/remove controls
- Frontend: App.tsx handleUploadArtwork + handleRemoveArtwork handlers
- Storage: /data/artwork/ directory for image files
2026-05-29 18:27:28 -04:00
rin.kagamine 23672387e5 Fix Lyria API response parsing — audio in parts[1], not parts[0]
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.
2026-05-29 16:59:28 -04:00
rin.kagamine a2bc785b14 Initial scaffold: FastAPI backend + React frontend + Docker Compose
- Backend: FastAPI with SQLite, clip CRUD, health check
- Frontend: React + Vite + TypeScript + Tailwind, dark theme
- Components: Generator, Gallery, Player
- Docker: Compose setup for backend + frontend
- Architecture docs in Palace KB
2026-05-26 18:18:55 -04:00