feat: Maestro Mode - context-aware prompt generation

- Backend: POST /api/text-gen/maestro endpoint with dual modes
  - Empty prompt → generate from scratch (temp 1.0)
  - Non-empty prompt → complete/extend (temp 0.85)
  - structured=true → returns JSON fields for Advanced/Pro mode
  - Three system prompts: MAESTRO_GENERATE, MAESTRO_COMPLETE, MAESTRO_STRUCTURED
  - Trailing sentence cleanup for plain text mode
  - JSON fallback parsing for structured mode

- Frontend: MaestroButton component
  - Context-aware labels: ♪ Maestro Mode (empty) / ♪ Keep Composing (has content)
  - Loading states: animating ♩ with Dreaming.../Composing...
  - structured prop for Pro mode → populates structured fields
  - onStructuredResult callback merges fields into StructuredPrompt

- UX: Replaced 'Get Suggestions ✨' with Maestro Mode
  - Suggestions required text in the box; Maestro works with or without
  - Single button, context-aware behavior, cleaner UI
  - Advanced/Pro mode gets structured fields from Maestro
  - Removed SuggestionsModal dependency

- Rename: 'Compose for Me' → 'Keep Composing'
This commit is contained in:
rin.kagamine committed 2026-05-31 10:13:43 -04:00
1 parent 816f8fbd2f
commit e0ce9e4b6c
4 files changed
+304 -33

No files matched your search

+21 -1
View File
@@ -15,7 +15,7 @@ from kithara_client import generate_clip, save_clip_file
from waveform import generate_waveform_svg, get_waveform_path
from analysis import analyze_clip
from image_gen import generate_artwork, get_image_gen_config, get_available_models, SAFETY_LEVELS
from text_gen import suggest_prompts, get_text_gen_config, get_available_models as get_text_models
from text_gen import suggest_prompts, maestro_prompt, get_text_gen_config, get_available_models as get_text_models
app = FastAPI(title="Kithara Studio", version="0.1.0")
@@ -447,6 +447,26 @@ async def get_suggestions(request: dict):
except Exception as e:
raise HTTPException(status_code=500, detail=f"Suggestion generation failed: {str(e)}")
@app.post("/api/text-gen/maestro")
async def maestro(request: dict):
"""Generate or complete a music prompt using Maestro Mode.
If hint is empty/missing: generates a creative prompt from scratch ("Dream for Me").
If hint is provided: completes the user's existing prompt ("Keep Composing").
If structured=true: returns JSON fields for Advanced/Pro mode instead of plain text.
"""
hint = request.get("hint", "").strip()
model = request.get("model") # Optional text model override
structured = request.get("structured", False) # Return structured fields for Pro mode
try:
config = get_text_gen_config()
result = await maestro_prompt(hint=hint, model=model, config=config, structured=structured)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Maestro generation failed: {str(e)}")
@app.patch("/api/clips/{clip_id}")
async def rename_clip_endpoint(clip_id: str, request: RenameRequest):
clip = await get_clip(clip_id)
+152 -1
View File
@@ -1,6 +1,8 @@
"""Text generation client using Google Gemini models for prompt suggestions."""
import json
import os
import re
from typing import Optional
from google import genai
@@ -93,7 +95,6 @@ Format your response as a numbered list (1., 2., 3.) with just the prompts, no e
suggestions = []
# Split by numbered lines (1., 2., 3.)
import re
# Find all text after numbers
matches = re.findall(r'(?:^|\n)\d+\.\s*(.*?)(?=\n\d+\.|$)', text, re.DOTALL)
if matches:
@@ -106,6 +107,156 @@ Format your response as a numbered list (1., 2., 3.) with just the prompts, no e
return suggestions[:3] # Ensure max 3
# ── Maestro Mode ──────────────────────────────────────────────────────────
MAESTRO_GENERATE_PROMPT = """You are Maestro, an expert music prompt writer for AI music generation models (specifically Google Lyria).
Your task: Write a SINGLE compelling, detailed music generation prompt from scratch. The user has given you no starting point — be creative and bold.
Pick any genre, style, or mood that inspires you. The prompt should be:
- A single paragraph, vivid and evocative
- Rich in musical detail (genre, instrumentation, mood, tempo, structure, production style)
- Written in terminology that music producers use
- Between 2-4 sentences long
- Something that would produce an INTERESTING piece of music — surprise the user
Vary your choices! Don't default to the same genre or mood every time. Draw from the full spectrum of music: electronic, orchestral, jazz, world, experimental, ambient, industrial, folk, hip-hop, and beyond.
Output ONLY the prompt text. No numbers, no labels, no commentary."""
MAESTRO_COMPLETE_PROMPT = """You are Maestro, an expert music prompt writer for AI music generation models (specifically Google Lyria).
The user has started writing a prompt and wants you to complete it. Here is their prompt so far:
"{user_input}"
Complete this prompt into a detailed, evocative music generation prompt. Your completion should:
- Build on the user's existing idea naturally
- Add specific details about genre, instrumentation, mood, tempo, structure, or production style
- Result in a single paragraph that's ready to use as-is
- Be between 2-4 sentences total (including what the user already wrote)
Do NOT repeat what the user wrote — continue from where they left off and blend it into a complete, polished prompt.
Output ONLY the completed prompt text. No numbers, no labels, no commentary."""
MAESTRO_STRUCTURED_PROMPT = """You are Maestro, an expert music prompt writer for AI music generation models (specifically Google Lyria).
{context_instruction}
Generate a detailed music generation prompt, filling in as many of these fields as appropriate:
- style: Genre and style (e.g., "Industrial techno", "Cinematic orchestral", "Lo-fi hip hop")
- timbre: Sound texture and tone quality (e.g., "Warm analog synths with gritty distortion layers")
- structure: Song structure (e.g., "Intro-Build-Drop-Breakdown-Drop-Outro")
- mood: Emotional quality and energy (e.g., "Dark and aggressive, euphoric climax")
- tempo: BPM and rhythmic feel (e.g., "180 BPM, driving four-on-the-floor")
- instrumentation: Specific instruments and sounds (e.g., "Distorted 909 kick, reverbed vocal chops, sub-bass")
- effects: Production effects and processing (e.g., "Heavy sidechain compression, bitcrushed melodies")
- additional: Any extra details, references, or instructions
You MUST output valid JSON with those exact field names. Only include fields where you have specific, meaningful content. Leave fields as empty strings if you don't have a good detail for them. Do NOT include any fields beyond these 8.
Be creative and varied! Draw from the full spectrum of music — electronic, orchestral, jazz, world, experimental, ambient, industrial, folk, hip-hop, and beyond.
Output ONLY the JSON object. No markdown, no commentary, no explanation."""
async def maestro_prompt(hint: str = "", model: str = None, config: dict = None, structured: bool = False) -> dict:
"""Generate or complete a music prompt using Maestro Mode.
Args:
hint: User's existing text. Empty = generate from scratch, non-empty = complete.
model: Text model to use. Defaults to configured model.
config: Optional config override.
structured: If True, return structured JSON fields for Advanced/Pro mode.
Returns:
Plain: dict with "prompt" (text) and "mode" ("generate" | "complete")
Structured: dict with "fields" (JSON) and "mode" ("generate" | "complete")
"""
cfg = config or get_text_gen_config()
client = genai.Client(api_key=cfg["api_key"])
safety = build_safety_settings(cfg)
# Auto-select mode based on input
mode = "generate" if not hint.strip() else "complete"
if structured:
# Structured mode: return JSON fields for Advanced/Pro prompt
context_instruction = (
"The user wants you to generate a creative music prompt from scratch. Surprise them with something interesting."
if mode == "generate"
else f"The user has started describing music they want. Here's what they have so far:\n\n\"{hint.strip()}\"\n\nBuild on their idea, filling in the structured fields with rich, specific details."
)
system = MAESTRO_STRUCTURED_PROMPT.format(context_instruction=context_instruction)
user_content = "Generate structured music prompt fields."
temperature = 0.95 if mode == "generate" else 0.8
max_tokens = 1536
response = client.models.generate_content(
model=model or cfg["model"],
contents=user_content,
config=types.GenerateContentConfig(
system_instruction=system,
safety_settings=safety,
temperature=temperature,
max_output_tokens=max_tokens,
response_mime_type="application/json",
),
)
try:
fields = json.loads(response.text.strip())
except json.JSONDecodeError:
# Fallback: try to extract JSON from markdown code blocks
match = re.search(r'```(?:json)?\s*([\s\S]*?)```', response.text)
if match:
fields = json.loads(match.group(1).strip())
else:
# Last resort: put everything in additional
fields = {"additional": response.text.strip()}
return {"fields": fields, "mode": mode}
else:
# Plain text mode: original behavior for Basic prompt
if mode == "generate":
system = MAESTRO_GENERATE_PROMPT
user_content = "Write me a music generation prompt. Surprise me."
temperature = 1.0
max_tokens = 1024
else:
system = MAESTRO_COMPLETE_PROMPT.format(user_input=hint.strip())
user_content = "Complete this prompt into a finished, ready-to-use music generation prompt. End with a complete sentence."
temperature = 0.85
max_tokens = 1536
response = client.models.generate_content(
model=model or cfg["model"],
contents=user_content,
config=types.GenerateContentConfig(
system_instruction=system,
safety_settings=safety,
temperature=temperature,
max_output_tokens=max_tokens,
),
)
text = response.text.strip()
# Remove any leading numbering or labels the model might add
text = re.sub(r'^\d+\.\s*', '', text)
# Clean up trailing incomplete sentences (cut at last period)
if text and not text[-1] in '.!?':
last_period = max(text.rfind('.'), text.rfind('!'), text.rfind('?'))
if last_period > len(text) // 2: # Only cut if we're past halfway
text = text[:last_period + 1]
return {"prompt": text, "mode": mode}
# ── Available Models ───────────────────────────────────────────────────────
AVAILABLE_TEXT_MODELS = [