29 Commits
Author SHA1 Message Date
rin.kagamine 89f02a8de5 fix: auth API response format and test suite green
- API: login/register now return OAuth2-standard {access_token, token_type: 'bearer'}
  instead of {token} — matches frontend auth.ts expectations
- API: added has_api_key to login/register user responses
- API: password minimum changed from 8 to 6 characters (matches frontend)
- Tests: all 14 auth tests passing
  - Fixed token field name (access_token)
  - Fixed invite code insertion (use auth_mod DB path)
  - Fixed duplicate username test (400 not 409)
  - Added auth_mod.DB_PATH patch to both fixtures
  - Added aiosqlite direct DB inserts for test data
- Removed system Python pollution from earlier --break-system-packages
- Set up project venv (.venv) with all deps for local test runs
2026-05-31 13:31:25 -04:00
rin.kagamine 674abbbe8f test: add auth endpoint tests for single/multi mode
- 14 tests covering auth flow in both modes:
  - TestSingleMode: health shows single, /me returns null user, clips accessible without auth
  - TestMultiModeAuth: health shows multi, /me requires auth, clips require auth,
    admin login, wrong password, nonexistent user, valid token /me,
    clips with valid token, register with invite, invalid invite, duplicate username
- Tests require librosa (same as existing test_api.py) — pass in Docker CI
- Conftest fixture pattern matches existing app_client fixture
2026-05-31 12:24:22 -04:00
rin.kagamine c571e28725 feat: Phase 1 frontend - auth UI with KITHARA_MODE switch
- Auth API client (src/api/auth.ts): login, register, me, api-key endpoints
- AuthContext: React context managing auth state, mode detection, JWT tokens
- LoginPage: clean dark-themed login form with error handling
- RegisterPage: invite code + registration with password validation
- App.tsx: BrowserRouter + AuthProvider wrapper, mode-conditional routing
  - Single mode: straight to app, no auth required
  - Multi mode: redirect to /login, auth-gated access
- main.tsx: BrowserRouter + AuthProvider initialization
- react-router-dom added as dependency
- docker-compose.yml: KITHARA_MODE, KITHARA_ADMIN_USER, KITHARA_ADMIN_PASS env vars
- requirements.txt: pinned bcrypt==4.0.1 for passlib compatibility
- Tested: multi-mode admin bootstrap, JWT login, auth/me round-trip all confirmed
2026-05-31 12:15:13 -04:00
rin.kagamine b3f273942e feat: Phase 1 - multi-user auth foundation
- New auth.py module: users table, invite_codes table, JWT auth,
  password hashing (bcrypt), auth dependencies for FastAPI
- KITHARA_MODE=single|multi in .env (default: single)
- Single mode: no auth required, no login, user_id='default'
- Multi mode: full JWT auth, login/register endpoints, admin bootstrap
- Health endpoint now reports mode: {status, version, mode}
- Auth endpoints: POST /api/auth/login, /register, GET /me
- Admin endpoints: GET /admin/users, POST /admin/users,
  POST /admin/invites, GET /admin/invites,
  DELETE /admin/users/:id, PUT /admin/users/:id/role
- BYOK: PUT /api/auth/api-key for per-user Lyria keys
- DB migrations: user_id, shared, remixed_from columns on clips
- DB indexes for user-based and shared-content queries
- requirements.txt: added PyJWT and passlib[bcrypt]
- .env.example: documented all new config options
2026-05-31 11:59:07 -04:00
rin.kagamine b008a6790a feat: DB-backed favorites replace localStorage
- New clip_favorites table (clip_id, user_id DEFAULT 'default', created_at)
- API: POST /clips/:id/favorite (toggle), GET /favorites, GET /clips/:id/favorite
- Frontend: loads favorites on mount from API, toggles via POST
- Removes localStorage dependency - favorites survive cache clears
- Ready for multi-user: just add real user_id when auth arrives
2026-05-31 11:35:01 -04:00
rin.kagamine 6ec5cf4b1b feat: env-configurable default ID3 tags for artist, album, genre
DEFAULT_ID3_ARTIST, DEFAULT_ID3_ALBUM, DEFAULT_ID3_GENRE in .env.
Clip-level metadata always takes priority. Env defaults fill in blanks.
Current defaults: Kagamine Rin / Kithara Studio / Hardcore Techno.

Also updated .env.example with all three new vars (commented out).
2026-05-31 11:21:14 -04:00
rin.kagamine bef707f9a4 feat: genre field in DetailPanel and ID3 TCON tag
- Added genre column to clips table (auto-migration)
- Genre field in DetailPanel metadata section (between Album and Comment)
- Genre saved via PATCH /api/clips/:id/metadata
- Genre embedded in ID3 TCON tag on download
- Placeholder text: 'e.g. Hardcore Techno, Phonk, Dark Ambient...'
2026-05-31 11:18:37 -04:00
rin.kagamine 1903d8bd2a feat: embed album art in ID3 tags on download
APIC tag now embeds the track's artwork (if it exists) into the MP3
file when downloading. Supports PNG, JPEG, and WebP. Key and BPM were
already being written — this was the missing piece.
2026-05-31 11:15:52 -04:00
rin.kagamine fb3c3c7c05 fix: soften Lyria API error messages
Error messages no longer over-attribute failures to user prompt phrasing.
The Lyria API has strict/loose constraints we haven't fully mapped yet,
so blaming the user's prompt when the API silently returns no audio is
misleading. Updated messages to acknowledge the API as the source.
2026-05-31 11:01:08 -04:00
rin.kagamine 78c13ccf01 fix: move punctuation logic to LLM - backend returns complete joined text
The frontend no longer does separator/punctuation logic. Both Maestro
modes (generate and complete) now simply setPrompt(result). The LLM
is instructed to return the user's exact words + continuation, properly
joined. This is the right layer for text joining decisions.
2026-05-31 10:51:50 -04:00
rin.kagamine 050c104201 fix: Keep Composing appends continuation instead of replacing prompt
Two fixes:
- Backend: MAESTRO_COMPLETE_PROMPT now instructs Gemini to output ONLY
  the continuation text, not a rewrite. No repeating user's words.
- Frontend: complete mode appends result to existing prompt with smart
  separator logic (lowercase start = space, uppercase = period+space).
  generate mode still replaces the empty prompt as before.
2026-05-31 10:48:05 -04:00
rin.kagamine da5e63817f fix: Keep Composing now incorporates user's text instead of replacing it
The MAESTRO_COMPLETE_PROMPT was telling Gemini to NOT repeat the user's
text, which caused the frontend to replace the entire prompt with only
the completion. Changed the prompt to instruct Gemini to rewrite and
expand the user's idea into a complete, polished prompt that incorporates
their words naturally. This way setPrompt(result) replaces with
something the user recognizes as their own idea, expanded.
2026-05-31 10:45:00 -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 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 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 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 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 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 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