Files
kithara-studio/backend/src/models.py
T
rin.kagamine a713488fb7 feat: Metadata fields (artist, album, comment) with ID3 tag export
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.
2026-05-29 22:55:01 -04:00

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