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.
101 lines
3.1 KiB
Python
101 lines
3.1 KiB
Python
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 = DEFAULT_MODEL
|
|
duration: Optional[int] = None # defaults to model default
|
|
prompt_structure: Optional[dict] = None # structured prompt fields for Pro mode
|
|
|
|
@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"])
|
|
|
|
def build_prompt(self) -> str:
|
|
"""Build the final prompt from structured fields or return the simple prompt."""
|
|
if self.prompt_structure:
|
|
parts = ["Generate a track with the following elements:"]
|
|
field_order = [
|
|
("style", "Style"),
|
|
("timbre", "Timbre"),
|
|
("structure", "Structure"),
|
|
("mood", "Mood"),
|
|
("tempo", "Tempo"),
|
|
("instrumentation", "Instrumentation"),
|
|
("effects", "Effects"),
|
|
("additional", "Additional Notes"),
|
|
]
|
|
for key, label in field_order:
|
|
if key in self.prompt_structure and self.prompt_structure[key]:
|
|
parts.append(f"**{label}:** {self.prompt_structure[key]}")
|
|
return "\n\n".join(parts)
|
|
return self.prompt
|
|
|
|
class GenerateResponse(BaseModel):
|
|
id: str
|
|
status: str
|
|
message: str
|
|
|
|
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
|
|
prompt_structure: Optional[dict] = None
|
|
bpm: Optional[float] = None
|
|
key: Optional[str] = None
|
|
artist: Optional[str] = None
|
|
album: Optional[str] = None
|
|
comment: Optional[str] = None
|
|
|
|
class RenameRequest(BaseModel):
|
|
title: str
|
|
|
|
class MetadataRequest(BaseModel):
|
|
artist: Optional[str] = None
|
|
album: Optional[str] = None
|
|
comment: Optional[str] = None
|
|
|
|
class GenerateArtworkRequest(BaseModel):
|
|
prompt: str
|
|
|
|
class ClipList(BaseModel):
|
|
clips: list[Clip]
|
|
total: int |