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.
This commit is contained in:
1 parent
fa3515acc9
commit
934d7775a8
8 files changed
+477
-6
No files matched your search
@@ -4,6 +4,7 @@ import Gallery from './components/Gallery'
|
||||
import Player from './components/Player'
|
||||
import DetailPanel from './components/DetailPanel'
|
||||
import SettingsModal from './components/SettingsModal'
|
||||
import ArtworkGenerationModal from './components/ArtworkGenerationModal'
|
||||
|
||||
interface Clip {
|
||||
id: string
|
||||
@@ -26,6 +27,8 @@ function App() {
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [selectedClip, setSelectedClip] = useState<Clip | null>(null)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [artworkGenOpen, setArtworkGenOpen] = useState(false)
|
||||
const [artworkGenClip, setArtworkGenClip] = useState<Clip | null>(null)
|
||||
|
||||
const fetchClips = async () => {
|
||||
try {
|
||||
@@ -134,6 +137,38 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleGenerateArtwork = async (clip: Clip) => {
|
||||
setArtworkGenClip(clip)
|
||||
setArtworkGenOpen(true)
|
||||
}
|
||||
|
||||
const handleArtworkGenSubmit = async (prompt: string) => {
|
||||
if (!artworkGenClip) return
|
||||
try {
|
||||
const res = await fetch(`/api/clips/${artworkGenClip.id}/generate-artwork`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt }),
|
||||
})
|
||||
if (res.ok) {
|
||||
const updated = await res.json()
|
||||
setClips(prev => prev.map(c => c.id === updated.id ? updated : c))
|
||||
if (currentClip?.id === updated.id) {
|
||||
setCurrentClip(updated)
|
||||
}
|
||||
if (selectedClip?.id === updated.id) {
|
||||
setSelectedClip(updated)
|
||||
}
|
||||
} else {
|
||||
const err = await res.json()
|
||||
alert(err.detail || 'Failed to generate artwork')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to generate artwork:', err)
|
||||
alert('Failed to generate artwork')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectClip = async (clip: Clip) => {
|
||||
setSelectedClip(clip)
|
||||
// Fetch analysis if not already cached
|
||||
@@ -238,6 +273,19 @@ function App() {
|
||||
onRemoveArtwork={handleRemoveArtwork}
|
||||
onPlay={handlePlay}
|
||||
onTogglePlay={() => setIsPlaying(!isPlaying)}
|
||||
onGenerateArtwork={handleGenerateArtwork}
|
||||
/>
|
||||
)}
|
||||
|
||||
{artworkGenOpen && artworkGenClip && (
|
||||
<ArtworkGenerationModal
|
||||
isOpen={artworkGenOpen}
|
||||
onClose={() => {
|
||||
setArtworkGenOpen(false)
|
||||
setArtworkGenClip(null)
|
||||
}}
|
||||
onGenerate={handleArtworkGenSubmit}
|
||||
trackName={artworkGenClip.title || artworkGenClip.prompt}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user