feat: Structured prompt editing for Pro model
Backend: - Added prompt_structure JSON column to clips DB (migration in init_db) - Updated GenerateRequest model with optional prompt_structure dict - build_prompt() method combines structured fields into single prompt: 'Generate a track with the following elements:\n\n**Style:** [style]\n...' - create_clip() accepts and stores prompt_structure JSON - get_clips()/get_clip() parse prompt_structure back to dict Frontend: - New StructuredPromptEditor component with 8 collapsible fields: Style, Timbre, Structure, Mood, Tempo, Instrumentation, Effects, Additional - Fields show filled/empty state with colored dots - Expand/collapse individual fields - 'Expand All' button - Counter: 'X of 8 fields filled' - Generator component updated with Basic/Advanced mode toggle (Pro only) - Basic: single textarea (works for both models) - Advanced: StructuredPromptEditor (Pro only) - Mode resets to Basic when switching to Clip Preview - DetailPanel shows structured prompt breakdown when present - Each filled field shown in its own labeled box - Style, Timbre, Structure, Mood, Tempo, Instrumentation, Effects, Additional - Clip interface updated with prompt_structure field across all components Architecture KB and Tasks KB updated.
This commit is contained in:
1 parent
a79b2806dc
commit
70beb89ad9
8 files changed
+273
-25
No files matched your search
+22
-3
@@ -78,15 +78,20 @@ async def init_db():
|
||||
await db.execute("ALTER TABLE clips ADD COLUMN key TEXT")
|
||||
except Exception:
|
||||
pass # Column already exists
|
||||
# Migration: add prompt_structure column if it doesn't exist
|
||||
try:
|
||||
await db.execute("ALTER TABLE clips ADD COLUMN prompt_structure TEXT")
|
||||
except Exception:
|
||||
pass # Column already exists
|
||||
await db.commit()
|
||||
|
||||
async def create_clip(prompt: str, model: str, duration: int) -> str:
|
||||
async def create_clip(prompt: str, model: str, duration: int, prompt_structure: str = None) -> str:
|
||||
clip_id = str(uuid.uuid4())
|
||||
filename = f"{clip_id}.mp3"
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute(
|
||||
"INSERT INTO clips (id, prompt, model, duration_seconds, filename, status) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(clip_id, prompt, model, duration, filename, "generating")
|
||||
"INSERT INTO clips (id, prompt, model, duration_seconds, filename, status, prompt_structure) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(clip_id, prompt, model, duration, filename, "generating", prompt_structure)
|
||||
)
|
||||
await db.commit()
|
||||
return clip_id
|
||||
@@ -121,6 +126,13 @@ async def get_clips(limit: int = 50, offset: int = 0):
|
||||
clips = [dict(row) for row in rows]
|
||||
for clip in clips:
|
||||
clip["created_at"] = format_timestamp(clip.get("created_at"))
|
||||
# Parse prompt_structure JSON
|
||||
if clip.get("prompt_structure"):
|
||||
try:
|
||||
import json
|
||||
clip["prompt_structure"] = json.loads(clip["prompt_structure"])
|
||||
except Exception:
|
||||
clip["prompt_structure"] = None
|
||||
return clips
|
||||
|
||||
async def get_clip(clip_id: str):
|
||||
@@ -131,6 +143,13 @@ async def get_clip(clip_id: str):
|
||||
if row:
|
||||
clip = dict(row)
|
||||
clip["created_at"] = format_timestamp(clip.get("created_at"))
|
||||
# Parse prompt_structure JSON
|
||||
if clip.get("prompt_structure"):
|
||||
try:
|
||||
import json
|
||||
clip["prompt_structure"] = json.loads(clip["prompt_structure"])
|
||||
except Exception:
|
||||
clip["prompt_structure"] = None
|
||||
return clip
|
||||
return None
|
||||
|
||||
|
||||
+7
-2
@@ -77,13 +77,18 @@ async def _generate_and_save(clip_id: str, prompt: str, model: str, duration: in
|
||||
@app.post("/api/generate", response_model=GenerateResponse)
|
||||
async def generate(request: GenerateRequest, background_tasks: BackgroundTasks):
|
||||
duration = request.get_effective_duration()
|
||||
clip_id = await create_clip(request.prompt, request.model, duration)
|
||||
# Build prompt from structured fields if present
|
||||
final_prompt = request.build_prompt()
|
||||
# Serialize structured prompt for DB storage
|
||||
import json
|
||||
prompt_structure_json = json.dumps(request.prompt_structure) if request.prompt_structure else None
|
||||
clip_id = await create_clip(final_prompt, request.model, duration, prompt_structure_json)
|
||||
|
||||
# Kick off generation in background
|
||||
background_tasks.add_task(
|
||||
_generate_and_save,
|
||||
clip_id,
|
||||
request.prompt,
|
||||
final_prompt,
|
||||
request.model,
|
||||
duration
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ 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
|
||||
@@ -40,6 +41,26 @@ class GenerateRequest(BaseModel):
|
||||
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
|
||||
@@ -57,6 +78,9 @@ class Clip(BaseModel):
|
||||
status: str
|
||||
error_message: Optional[str] = None
|
||||
created_at: datetime
|
||||
prompt_structure: Optional[dict] = None
|
||||
bpm: Optional[float] = None
|
||||
key: Optional[str] = None
|
||||
|
||||
class RenameRequest(BaseModel):
|
||||
title: str
|
||||
|
||||
Reference in new issue
Block a user