diff --git a/README.md b/README.md index f0a0852..3ece571 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Lyria Studio +# Kithara Studio A webapp for generating, playing, and storing music clips using Google's Lyria music generation models. @@ -23,7 +23,7 @@ npm run dev ## Architecture -- **Backend**: FastAPI (Python) — async Lyria API client, SQLite metadata, file storage +- **Backend**: FastAPI (Python) — async API client for Google's Lyria models, SQLite metadata, file storage - **Frontend**: React + Vite + TypeScript + Tailwind — dark theme, audio player - **Storage**: SQLite for clip metadata, local filesystem for MP3 files @@ -40,9 +40,9 @@ npm run dev ## Models -- **v0.1**: Lyria 3 Clip Preview (30s, REST) -- **v0.2**: Lyria 3 Pro (structure-aware, ~3min) -- **v0.3**: Lyria RealTime (WebSocket streaming) +- **v0.1**: Google Lyria 3 Clip Preview (30s, REST) +- **v0.2**: Google Lyria 3 Pro (structure-aware, ~3min) +- **v0.3**: Google Lyria RealTime (WebSocket streaming) ## License diff --git a/backend/src/database.py b/backend/src/database.py index fb0f4c6..108f667 100644 --- a/backend/src/database.py +++ b/backend/src/database.py @@ -4,7 +4,7 @@ import uuid from datetime import datetime, timezone from pathlib import Path -DB_PATH = Path(__file__).parent.parent / "data" / "lyria.db" +DB_PATH = Path(__file__).parent.parent / "data" / "kithara.db" DB_PATH.parent.mkdir(parents=True, exist_ok=True) # Timezone from TZ env var (default: America/New_York) diff --git a/backend/src/lyria_client.py b/backend/src/kithara_client.py similarity index 93% rename from backend/src/lyria_client.py rename to backend/src/kithara_client.py index 4753eb7..e1f7670 100644 --- a/backend/src/lyria_client.py +++ b/backend/src/kithara_client.py @@ -4,7 +4,7 @@ import os import logging from pathlib import Path -logger = logging.getLogger("lyria_client") +logger = logging.getLogger("kithara_client") LYRIA_API_KEY = os.getenv("LYRIA_API_KEY", "") LYRIA_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" @@ -50,11 +50,11 @@ async def generate_clip(prompt: str, model: str = "lyria-3-clip-preview", durati block_reason = data.get("blockReason", "") if block_reason: block_message = data.get("blockReasonMessage", "") - logger.warning(f"Lyria API blocked: {block_reason} — {block_message}") + logger.warning(f"Google Lyria API blocked: {block_reason} — {block_message}") return {"success": False, "error": f"Prompt was filtered. Try describing the sound using mood, instruments, and tempo instead of genre names. ({block_message or block_reason})"} # Extract base64 audio from response - # Lyria returns parts array: first part may be text (e.g. ""), + # Google Lyria returns parts array: first part may be text (e.g. ""), # audio is in inlineData of a subsequent part. candidates = data.get("candidates", []) if not candidates: @@ -103,7 +103,7 @@ async def generate_clip(prompt: str, model: str = "lyria-3-clip-preview", durati error_detail = e.response.json().get("error", {}).get("message", str(e)) except: error_detail = str(e) - return {"success": False, "error": f"Lyria API error: {error_detail}"} + return {"success": False, "error": f"Google Lyria API error: {error_detail}"} except Exception as e: return {"success": False, "error": f"Client error: {str(e)}"} diff --git a/backend/src/main.py b/backend/src/main.py index 660eb31..6ab029d 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -6,18 +6,18 @@ import os import shutil from dotenv import load_dotenv -# Load environment variables BEFORE importing lyria_client (it reads env at module level) +# Load environment variables BEFORE importing kithara_client (it reads env at module level) load_dotenv(Path(__file__).parent.parent / ".env") from models import GenerateRequest, GenerateResponse, ClipList, RenameRequest, MetadataRequest, GenerateArtworkRequest, 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, delete_all_clips, set_analysis, update_metadata -from lyria_client import generate_clip, save_clip_file +from kithara_client import generate_clip, save_clip_file from waveform import generate_waveform_svg, get_waveform_path from analysis import analyze_clip from image_gen import generate_artwork, get_image_gen_config, get_available_models, SAFETY_LEVELS from text_gen import suggest_prompts, get_text_gen_config, get_available_models as get_text_models -app = FastAPI(title="Lyria Studio", version="0.1.0") +app = FastAPI(title="Kithara Studio", version="0.1.0") app.add_middleware( CORSMiddleware, @@ -49,7 +49,7 @@ async def list_models(): return {"models": LYRIA_MODELS, "default": DEFAULT_MODEL} async def _generate_and_save(clip_id: str, prompt: str, model: str, duration: int): - """Background task: call Lyria API and save the result. + """Background task: call Google Lyria API and save the result. DR-LYRIA-001: Pre-generates waveform SVG after clip is ready, so users never wait for on-demand generation. diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 45e5766..37bdce4 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,4 +1,4 @@ -"""Shared test fixtures for Lyria Studio backend.""" +"""Shared test fixtures for Kithara Studio backend.""" import asyncio import os @@ -39,7 +39,7 @@ async def app_client(tmp_path): waveforms_dir.mkdir() artwork_dir = data_dir / "artwork" artwork_dir.mkdir() - test_db = data_dir / "lyria.db" + test_db = data_dir / "kithara.db" # Set env vars BEFORE import so module-level Path() calls use test dirs # (image_gen.py reads ARTWORK_DIR from env, main.py uses __file__-relative paths) diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 09de8be..5f16696 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -1,4 +1,4 @@ -"""Tests for Lyria Studio API endpoints.""" +"""Tests for Kithara Studio API endpoints.""" import subprocess import sys diff --git a/frontend/index.html b/frontend/index.html index d8470a8..336ab83 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,9 +4,9 @@ - Lyria Studio + Kithara Studio - +
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 857bf53..50cc99d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "lyria-studio-frontend", + "name": "kithara-studio-frontend", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "lyria-studio-frontend", + "name": "kithara-studio-frontend", "version": "0.1.0", "dependencies": { "fuse.js": "^7.0.0", diff --git a/frontend/package.json b/frontend/package.json index 01fc174..ddf79ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "lyria-studio-frontend", + "name": "kithara-studio-frontend", "private": true, "version": "0.1.0", "type": "module", diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000..85c5b73 Binary files /dev/null and b/frontend/public/logo.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 32d483b..32d1ef0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -237,19 +237,17 @@ function App() { } return ( -
+
-
- L -
-

Lyria Studio

- v0.1 + Kithara Studio +

Kithara Studio

+ v0.1
@@ -82,7 +82,7 @@ export default function ArtworkGenerationModal({ isOpen, onClose, onGenerate, tr @@ -191,7 +191,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
) : (
{ if (currentClipId === clip.id && isPlaying) { onTogglePlay?.() @@ -203,7 +203,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o 𝄞 - No album art + No album art
{currentClipId === clip.id && isPlaying ? ( @@ -246,11 +246,11 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o {/* Title — inline editable */}
- + {!isEditingTitle && ( @@ -264,7 +264,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o onChange={(e) => setEditTitle(e.target.value)} onBlur={saveTitle} onKeyDown={handleTitleKeyDown} - className="w-full bg-white/5 border border-lyria-accent/50 rounded-md px-3 py-2 text-white text-lg font-medium outline-none focus:border-lyria-accent focus:ring-1 focus:ring-lyria-accent/30 transition-all" + className="w-full bg-white/5 border border-kithara-accent/50 rounded-md px-3 py-2 text-white text-lg font-medium outline-none focus:border-kithara-accent focus:ring-1 focus:ring-kithara-accent/30 transition-all" placeholder="Track title..." /> ) : ( @@ -272,14 +272,14 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o onClick={startEditing} title="Click to rename" > - {clip.title || Untitled} + {clip.title || Untitled}

)}
{/* Prompt */}
- +

{clip.prompt}

@@ -288,7 +288,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o {/* Structured Prompt Breakdown */} {clip.prompt_structure && (
- +
{[ { key: 'style', label: 'Style' }, @@ -304,7 +304,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o if (!val) return null return (
- {label}: + {label}: {val}
) @@ -315,7 +315,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o {/* Status */}
- + {statusLabel(clip.status)} @@ -324,7 +324,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o {/* Error message */} {clip.status === 'failed' && clip.error_message && (
- +

{clip.error_message}

@@ -334,16 +334,16 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o {/* Metadata grid */}
- +

{modelName(clip.model)}

- +

{clip.duration_seconds}s

{clip.file_size_bytes && (
- +

{(clip.file_size_bytes / 1024).toFixed(0)} KB

)} @@ -351,7 +351,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o const c = getBPMColor(clip.bpm) return c ? (
- + - + - +

{formatDate(clip.created_at)}

@@ -405,8 +405,8 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o {/* ID (for reference) */}
- -

{clip.id}

+ +

{clip.id}

{/* Actions */} @@ -414,7 +414,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o @@ -466,7 +466,7 @@ function MetadataField({ label, value, placeholder, onSave }: { if (editing) { return (
- +
@@ -491,13 +491,13 @@ function MetadataField({ label, value, placeholder, onSave }: { className="group cursor-pointer" onClick={() => setEditing(true)} > - +
- + {value || placeholder} {parts.map((part, i) => part.toLowerCase() === query.toLowerCase() ? ( - {part} + {part} ) : ( {part} ) @@ -104,7 +104,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o // Saved searches const [savedSearches, setSavedSearches] = useState(() => { try { - const raw = localStorage.getItem('lyria-saved-searches') + const raw = localStorage.getItem('kithara-saved-searches') return raw ? JSON.parse(raw) : [] } catch { return [] @@ -195,7 +195,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o } const updated = [...savedSearches, newSearch] setSavedSearches(updated) - localStorage.setItem('lyria-saved-searches', JSON.stringify(updated)) + localStorage.setItem('kithara-saved-searches', JSON.stringify(updated)) }, [search, filters, savedSearches]) const loadSearch = useCallback((saved: SavedSearch) => { @@ -207,7 +207,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o const deleteSearch = useCallback((id: string) => { const updated = savedSearches.filter(s => s.id !== id) setSavedSearches(updated) - localStorage.setItem('lyria-saved-searches', JSON.stringify(updated)) + localStorage.setItem('kithara-saved-searches', JSON.stringify(updated)) }, [savedSearches]) const clearFilters = () => { @@ -261,14 +261,14 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o if (clips.length === 0) { return ( -
-

No clips yet. Generate your first one!

+
+

No clips yet. Generate your first one!

) } return ( -
+
{/* Header: title, search, view toggle */}
@@ -279,7 +279,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o {/* Search bar */}
@@ -289,12 +289,12 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o placeholder="Search tracks, artists, albums..." value={search} onChange={(e) => setSearch(e.target.value)} - className="w-full bg-white/5 border border-white/10 rounded-md pl-8 pr-3 py-1.5 text-sm text-white placeholder-lyria-muted focus:outline-none focus:border-lyria-accent/50 focus:ring-1 focus:ring-lyria-accent/30 transition-colors" + className="w-full bg-white/5 border border-white/10 rounded-md pl-8 pr-3 py-1.5 text-sm text-white placeholder-kithara-muted focus:outline-none focus:border-kithara-accent/50 focus:ring-1 focus:ring-kithara-accent/30 transition-colors" /> {search && (
@@ -391,7 +391,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o {/* Results count */} {(search || hasActiveFilters) && ( -

+

Showing {filtered.length} of {clips.length} clips

)} @@ -402,7 +402,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
- +
- +
- +
- +
setFilters(f => ({ ...f, bpmMin: e.target.value }))} placeholder="Min" - className="w-full bg-black/30 border border-white/10 rounded-md px-2 py-1.5 text-sm text-white placeholder:text-lyria-muted/30" + className="w-full bg-black/30 border border-white/10 rounded-md px-2 py-1.5 text-sm text-white placeholder:text-kithara-muted/30" /> - - + - setFilters(f => ({ ...f, bpmMax: e.target.value }))} placeholder="Max" - className="w-full bg-black/30 border border-white/10 rounded-md px-2 py-1.5 text-sm text-white placeholder:text-lyria-muted/30" + className="w-full bg-black/30 border border-white/10 rounded-md px-2 py-1.5 text-sm text-white placeholder:text-kithara-muted/30" />
- +
- + @@ -499,19 +499,19 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o {/* Saved searches dropdown */} {showSaved && savedSearches.length > 0 && (
-

Saved Searches

+

Saved Searches

{savedSearches.map(saved => (
@@ -606,9 +606,9 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o }`}> {clip.status} - {clip.duration_seconds}s - {modelName(clip.model)} - {formatDate(clip.created_at)} + {clip.duration_seconds}s + {modelName(clip.model)} + {formatDate(clip.created_at)} {clip.bpm && (() => { const c = getBPMColor(clip.bpm) return c ? ( @@ -645,7 +645,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o @@ -198,7 +198,7 @@ export default function Generator({ onGenerate }: Props) { value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Dark distorted kickdrum with atmospheric synth pads, 180 BPM..." - className="w-full h-24 bg-lyria-dark border border-white/10 rounded-md px-3 py-2 text-sm text-white placeholder:text-lyria-muted/50 focus:outline-none focus:border-lyria-accent resize-none" + className="w-full h-24 bg-kithara-dark border border-white/10 rounded-md px-3 py-2 text-sm text-white placeholder:text-kithara-muted/50 focus:outline-none focus:border-kithara-accent resize-none" />
)} @@ -207,12 +207,12 @@ export default function Generator({ onGenerate }: Props) { {promptMode === 'advanced' && isPro && (
- + @@ -231,7 +231,7 @@ export default function Generator({ onGenerate }: Props) { diff --git a/frontend/src/components/Player.tsx b/frontend/src/components/Player.tsx index ad2ddae..0bd4e91 100644 --- a/frontend/src/components/Player.tsx +++ b/frontend/src/components/Player.tsx @@ -167,12 +167,12 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose return ( <> -
+
{/* Play/Pause */}
{/* Model Selection */}
- + gen.update(key, e.target.value)} > @@ -228,7 +228,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel @@ -238,7 +238,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel )}
-

+

Changes apply immediately but reset on server restart. Set .env for persistence.

@@ -255,13 +255,13 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel /> {/* Modal */} -
+
{/* Header */}

Settings

@@ -75,11 +75,11 @@ export default function StructuredPromptEditor({ value, onChange }: Props) { }`} >
- - {label} + + {label}
updateField(key, e.target.value)} placeholder={placeholder} - className="w-full h-20 bg-black/20 border border-white/5 rounded-md px-3 py-2 text-sm text-white placeholder:text-lyria-muted/30 focus:outline-none focus:border-lyria-accent/50 resize-none" + className="w-full h-20 bg-black/20 border border-white/5 rounded-md px-3 py-2 text-sm text-white placeholder:text-kithara-muted/30 focus:outline-none focus:border-kithara-accent/50 resize-none" />
)} diff --git a/frontend/src/components/SuggestionsModal.tsx b/frontend/src/components/SuggestionsModal.tsx index 358a6af..21851a2 100644 --- a/frontend/src/components/SuggestionsModal.tsx +++ b/frontend/src/components/SuggestionsModal.tsx @@ -53,16 +53,16 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput onClick={onClose} /> -
+
{/* Header */}

Prompt Suggestions

-

{userInput}

+

{userInput}

@@ -95,10 +95,10 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput {!loading && !error && suggestions.length === 0 && (
-

No suggestions yet. Click below to generate.

+

No suggestions yet. Click below to generate.

@@ -113,13 +113,13 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput onSelect(suggestion) onClose() }} - className="w-full text-left p-4 bg-white/5 hover:bg-white/10 border border-white/5 hover:border-lyria-accent/30 rounded-lg transition-all group" + className="w-full text-left p-4 bg-white/5 hover:bg-white/10 border border-white/5 hover:border-kithara-accent/30 rounded-lg transition-all group" >
- {i + 1}. + {i + 1}.

{suggestion}

-
+
Click to use this prompt →
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 7dc31f6..39b34c0 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -7,12 +7,12 @@ export default { theme: { extend: { colors: { - 'lyria-dark': '#0a0a0f', - 'lyria-panel': '#12121a', - 'lyria-surface': '#1a1a24', - 'lyria-accent': '#ff6b35', - 'lyria-text': '#e0e0e0', - 'lyria-muted': '#6b6b7b', + 'kithara-dark': '#0a0a0f', + 'kithara-panel': '#12121a', + 'kithara-surface': '#1a1a24', + 'kithara-accent': '#ff6b35', + 'kithara-text': '#e0e0e0', + 'kithara-muted': '#6b6b7b', }, keyframes: { 'marquee-scroll': {