- 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
- 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
- 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
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).
- 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...'
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.
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.
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.
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.
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.
- 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
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.
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.
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.
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
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.
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
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
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
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
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.