From e0ce9e4b6c103c43ebe19ef549b26798b46951ac Mon Sep 17 00:00:00 2001 From: Kagamine Rin Date: Sun, 31 May 2026 10:13:43 -0400 Subject: [PATCH] feat: Maestro Mode - context-aware prompt generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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' --- backend/src/main.py | 22 +++- backend/src/text_gen.py | 153 +++++++++++++++++++++- frontend/src/components/Generator.tsx | 64 ++++----- frontend/src/components/MaestroButton.tsx | 98 ++++++++++++++ 4 files changed, 304 insertions(+), 33 deletions(-) create mode 100644 frontend/src/components/MaestroButton.tsx diff --git a/backend/src/main.py b/backend/src/main.py index 6ab029d..ed9eb70 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -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) diff --git a/backend/src/text_gen.py b/backend/src/text_gen.py index 9e703e3..de0f449 100644 --- a/backend/src/text_gen.py +++ b/backend/src/text_gen.py @@ -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 = [ diff --git a/frontend/src/components/Generator.tsx b/frontend/src/components/Generator.tsx index a14ed40..96d1bec 100644 --- a/frontend/src/components/Generator.tsx +++ b/frontend/src/components/Generator.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import StructuredPromptEditor, { PromptStructure } from './StructuredPromptEditor' -import SuggestionsModal from './SuggestionsModal' +import MaestroButton from './MaestroButton' interface ModelInfo { name: string @@ -24,7 +24,7 @@ export default function Generator({ onGenerate }: Props) { const [models, setModels] = useState>({}) const [promptMode, setPromptMode] = useState('basic') const [structuredPrompt, setStructuredPrompt] = useState({}) - const [suggestionsOpen, setSuggestionsOpen] = useState(false) + useEffect(() => { fetch('/api/models') @@ -185,14 +185,16 @@ export default function Generator({ onGenerate }: Props) {
- + { + setPrompt(result) + if (mode === 'generate') { + setError('') + } + }} + onError={(msg) => setError(msg)} + />