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
This commit is contained in:
rin.kagamine committed 2026-05-29 18:27:28 -04:00
1 parent 23672387e5
commit 8ba7133269
10 files changed
+1030 -59

No files matched your search

+42 -4
View File
@@ -1,11 +1,44 @@
from pydantic import BaseModel
from pydantic import BaseModel, field_validator
from datetime import datetime
from typing import Optional
# Supported models and their constraints
LYRIA_MODELS = {
"lyria-3-clip-preview": {
"name": "Clip Preview",
"max_duration": 30,
"default_duration": 30,
"description": "30-second clip, fastest generation",
},
"lyria-3-pro-preview": {
"name": "Pro",
"max_duration": 184,
"default_duration": 120,
"description": "Full songs up to ~3 min, structure-aware",
},
}
DEFAULT_MODEL = "lyria-3-clip-preview"
class GenerateRequest(BaseModel):
prompt: str
model: str = "lyria-3-clip-preview"
duration: int = 30
model: str = DEFAULT_MODEL
duration: Optional[int] = None # defaults to model default
@field_validator('model')
@classmethod
def validate_model(cls, v):
v = v.strip()
if v not in LYRIA_MODELS:
valid = ', '.join(LYRIA_MODELS.keys())
raise ValueError(f"Unknown model '{v}'. Supported: {valid}")
return v
def get_effective_duration(self) -> int:
"""Return duration clamped to model limits, defaulting to model default if not specified."""
info = LYRIA_MODELS[self.model]
dur = self.duration if self.duration is not None else info["default_duration"]
return min(dur, info["max_duration"])
class GenerateResponse(BaseModel):
id: str
@@ -14,15 +47,20 @@ class GenerateResponse(BaseModel):
class Clip(BaseModel):
id: str
title: Optional[str] = None
prompt: str
model: str
duration_seconds: int
filename: str
artwork_filename: Optional[str] = None
file_size_bytes: Optional[int] = None
status: str
error_message: Optional[str] = None
created_at: datetime
class RenameRequest(BaseModel):
title: str
class ClipList(BaseModel):
clips: list[Clip]
total: int
total: int