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:
rin.kagamine committed 2026-05-29 22:28:52 -04:00
1 parent a79b2806dc
commit 70beb89ad9
8 files changed
+273 -25

No files matched your search

+83 -20
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'
import StructuredPromptEditor, { PromptStructure } from './StructuredPromptEditor'
interface ModelInfo {
name: string
@@ -11,6 +12,8 @@ interface Props {
onGenerate: () => void
}
type PromptMode = 'basic' | 'advanced'
export default function Generator({ onGenerate }: Props) {
const [prompt, setPrompt] = useState('')
const [model, setModel] = useState('lyria-3-clip-preview')
@@ -18,6 +21,8 @@ export default function Generator({ onGenerate }: Props) {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState('')
const [models, setModels] = useState<Record<string, ModelInfo>>({})
const [promptMode, setPromptMode] = useState<PromptMode>('basic')
const [structuredPrompt, setStructuredPrompt] = useState<PromptStructure>({})
useEffect(() => {
fetch('/api/models')
@@ -37,28 +42,40 @@ export default function Generator({ onGenerate }: Props) {
if (info) {
setDuration(info.default_duration)
}
// Reset to basic mode when switching to Clip Preview
if (newModel === 'lyria-3-clip-preview') {
setPromptMode('basic')
}
}
const currentModelInfo = models[model]
const maxDuration = currentModelInfo?.max_duration || 30
const minDuration = 5
const isPro = model === 'lyria-3-pro-preview'
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!prompt.trim()) return
if (promptMode === 'basic' && !prompt.trim()) return
if (promptMode === 'advanced' && Object.values(structuredPrompt).every(v => !v?.trim())) return
setIsLoading(true)
setError('')
try {
const body: Record<string, unknown> = {
prompt: prompt.trim(),
model,
duration,
}
if (promptMode === 'advanced' && isPro) {
body.prompt_structure = structuredPrompt
}
const res = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: prompt.trim(),
model,
duration
})
body: JSON.stringify(body)
})
if (!res.ok) {
@@ -78,6 +95,7 @@ export default function Generator({ onGenerate }: Props) {
}
setPrompt('')
setStructuredPrompt({})
onGenerate()
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong')
@@ -93,18 +111,7 @@ export default function Generator({ onGenerate }: Props) {
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs text-lyria-muted mb-1.5">
Prompt
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Dark distorted kickdrum with atmospheric synth pads, 180 BPM..."
className="w-full h-24 bg-lyria-dark border border-white/10 rounded-md px-3 py-2 text-sm text-white placeholder:text-lyria-muted/50 focus:outline-none focus:border-lyria-accent resize-none"
/>
</div>
{/* Model & Duration */}
<div className="flex gap-4">
<div className="flex-1">
<label className="block text-xs text-lyria-muted mb-1.5">
@@ -143,13 +150,69 @@ export default function Generator({ onGenerate }: Props) {
<span>{duration}s / {maxDuration}s max</span>
</div>
{/* Prompt Mode Toggle (Pro only) */}
{isPro && (
<div className="flex items-center gap-2 bg-white/5 rounded-lg p-1">
<button
type="button"
onClick={() => setPromptMode('basic')}
className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${
promptMode === 'basic'
? 'bg-lyria-accent text-black'
: 'text-lyria-muted hover:text-white'
}`}
>
Basic
</button>
<button
type="button"
onClick={() => setPromptMode('advanced')}
className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${
promptMode === 'advanced'
? 'bg-lyria-accent text-black'
: 'text-lyria-muted hover:text-white'
}`}
>
Advanced
</button>
</div>
)}
{/* Basic Prompt */}
{(promptMode === 'basic' || !isPro) && (
<div>
<label className="block text-xs text-lyria-muted mb-1.5">
Prompt
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Dark distorted kickdrum with atmospheric synth pads, 180 BPM..."
className="w-full h-24 bg-lyria-dark border border-white/10 rounded-md px-3 py-2 text-sm text-white placeholder:text-lyria-muted/50 focus:outline-none focus:border-lyria-accent resize-none"
/>
</div>
)}
{/* Advanced Prompt */}
{promptMode === 'advanced' && isPro && (
<div>
<label className="block text-xs text-lyria-muted mb-1.5">
Structured Prompt
</label>
<StructuredPromptEditor
value={structuredPrompt}
onChange={setStructuredPrompt}
/>
</div>
)}
{error && (
<p className="text-xs text-red-400 break-words">{error}</p>
)}
<button
type="submit"
disabled={isLoading || !prompt.trim()}
disabled={isLoading || (promptMode === 'basic' ? !prompt.trim() : Object.values(structuredPrompt).every(v => !v?.trim()))}
className="w-full py-2.5 bg-lyria-accent text-white text-sm font-medium rounded-md hover:bg-lyria-accent/90 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? 'Generating...' : 'Generate'}
@@ -157,4 +220,4 @@ export default function Generator({ onGenerate }: Props) {
</form>
</div>
)
}
}