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:
rin.kagamine committed 2026-05-29 22:05:47 -04:00
1 parent fa3515acc9
commit 934d7775a8
8 files changed
+477 -6

No files matched your search

+48
View File
@@ -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}
/>
)}