All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
474 lines
17 KiB
Python
474 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = []
|
|
# ///
|
|
"""
|
|
Map feedback dimension categories to Suno parameter adjustment recommendations.
|
|
|
|
Takes structured feedback dimensions (from parse-feedback.py or LLM triage)
|
|
and returns baseline parameter adjustment recommendations as structured JSON.
|
|
The LLM then refines these recommendations with contextual judgment.
|
|
|
|
Exit codes:
|
|
0 = adjustments generated successfully
|
|
1 = invalid input
|
|
2 = runtime error
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "_shared"))
|
|
from suno_constants import CRITICAL_ZONE, EXCLUSION_RECOMMENDED_MAX, PAID_TIERS
|
|
|
|
# Adjustment lookup tables
|
|
# Each dimension maps to a set of possible adjustments categorized by direction
|
|
|
|
STYLE_PROMPT_ADJUSTMENTS: dict[str, dict[str, dict[str, Any]]] = {
|
|
"instrumentation": {
|
|
"too_much": {
|
|
"add": ["minimal arrangement", "sparse instrumentation", "stripped-back"],
|
|
"remove_patterns": ["lush", "layered", "full", "dense", "wall of sound"],
|
|
"exclude_add": ["no dense layering"],
|
|
},
|
|
"too_little": {
|
|
"add": ["lush arrangement", "layered instrumentation", "full sound"],
|
|
"remove_patterns": ["minimal", "sparse", "stripped"],
|
|
"exclude_add": [],
|
|
},
|
|
"wrong_type": {
|
|
"add": [],
|
|
"remove_patterns": [],
|
|
"exclude_add": [],
|
|
"note": "Specify the unwanted instrument in exclusions and desired instrument in style prompt",
|
|
},
|
|
},
|
|
"vocals": {
|
|
"too_polished": {
|
|
"add": ["raw vocal", "imperfect delivery", "organic phrasing"],
|
|
"remove_patterns": ["polished", "clean vocal", "perfect"],
|
|
"exclude_add": ["no overproduced vocals"],
|
|
},
|
|
"too_rough": {
|
|
"add": ["polished vocal", "smooth delivery", "clean singing"],
|
|
"remove_patterns": ["raw", "rough", "gritty"],
|
|
"exclude_add": ["no raspy vocals"],
|
|
},
|
|
"too_quiet": {
|
|
"add": ["prominent vocals", "voice-forward mix"],
|
|
"remove_patterns": [],
|
|
"exclude_add": [],
|
|
},
|
|
"too_loud": {
|
|
"add": ["balanced mix", "instrument-forward"],
|
|
"remove_patterns": ["prominent vocal", "voice-forward"],
|
|
"exclude_add": [],
|
|
},
|
|
"wrong_character": {
|
|
"add": [],
|
|
"remove_patterns": [],
|
|
"exclude_add": [],
|
|
"note": "Specify desired vocal character: gender, age, tone, delivery style",
|
|
},
|
|
},
|
|
"energy": {
|
|
"too_high": {
|
|
"add": ["gentle", "soft", "understated", "subtle"],
|
|
"remove_patterns": ["high energy", "powerful", "driving", "intense"],
|
|
"exclude_add": [],
|
|
"slider": {"weirdness": "unchanged", "style_influence": "unchanged"},
|
|
},
|
|
"too_low": {
|
|
"add": ["high energy", "powerful", "dynamic", "driving"],
|
|
"remove_patterns": ["gentle", "soft", "subtle", "laid-back"],
|
|
"exclude_add": [],
|
|
"slider": {"style_influence": "decrease_slightly"},
|
|
},
|
|
"flat": {
|
|
"add": ["dynamic shifts", "building energy", "crescendo", "varied sections"],
|
|
"remove_patterns": [],
|
|
"exclude_add": [],
|
|
"slider": {"weirdness": "increase_slightly"},
|
|
},
|
|
},
|
|
"tempo": {
|
|
"too_fast": {
|
|
"add": ["slow tempo", "laid-back", "relaxed groove"],
|
|
"remove_patterns": ["uptempo", "fast", "driving rhythm", "energetic pace"],
|
|
"exclude_add": [],
|
|
},
|
|
"too_slow": {
|
|
"add": ["uptempo", "driving rhythm", "energetic pace"],
|
|
"remove_patterns": ["slow", "laid-back", "relaxed", "gentle pace"],
|
|
"exclude_add": [],
|
|
},
|
|
},
|
|
"production": {
|
|
"too_polished": {
|
|
"add": ["lo-fi", "raw production", "analog warmth", "rough edges"],
|
|
"remove_patterns": ["radio-ready", "clean production", "crisp", "polished"],
|
|
"exclude_add": [],
|
|
"slider": {"weirdness": "increase"},
|
|
},
|
|
"too_rough": {
|
|
"add": ["radio-ready mix", "clean production", "crisp", "polished"],
|
|
"remove_patterns": ["lo-fi", "raw", "rough", "analog"],
|
|
"exclude_add": [],
|
|
"slider": {"weirdness": "decrease"},
|
|
},
|
|
"too_reverb": {
|
|
"add": ["dry mix", "close mic", "intimate"],
|
|
"remove_patterns": ["spacious", "reverb", "ambient", "atmospheric"],
|
|
"exclude_add": [],
|
|
},
|
|
"too_dry": {
|
|
"add": ["spacious", "reverb", "ambient", "atmospheric"],
|
|
"remove_patterns": ["dry", "close mic"],
|
|
"exclude_add": [],
|
|
},
|
|
},
|
|
"vibe": {
|
|
"too_happy": {
|
|
"add": ["melancholic", "bittersweet", "minor key", "moody"],
|
|
"remove_patterns": ["uplifting", "bright", "happy", "cheerful", "major key"],
|
|
"exclude_add": [],
|
|
},
|
|
"too_dark": {
|
|
"add": ["uplifting", "bright", "major key", "hopeful"],
|
|
"remove_patterns": ["melancholic", "dark", "moody", "minor key"],
|
|
"exclude_add": [],
|
|
},
|
|
"too_generic": {
|
|
"add": ["distinctive", "unique", "unconventional"],
|
|
"remove_patterns": ["classic", "traditional", "conventional"],
|
|
"exclude_add": [],
|
|
"slider": {"weirdness": "increase_significantly"},
|
|
},
|
|
"too_weird": {
|
|
"add": ["familiar", "classic", "conventional", "straightforward"],
|
|
"remove_patterns": ["experimental", "unexpected", "unconventional"],
|
|
"exclude_add": [],
|
|
"slider": {"weirdness": "decrease_significantly"},
|
|
},
|
|
},
|
|
"music": {
|
|
"general_issue": {
|
|
"add": [],
|
|
"remove_patterns": [],
|
|
"exclude_add": [],
|
|
"note": "Music feedback requires further narrowing — which aspect of the music? Instrumentation, tempo, energy, production?",
|
|
},
|
|
},
|
|
"structure": {
|
|
"needs_bridge": {
|
|
"lyric_change": "Add [Bridge] section between second chorus and outro",
|
|
},
|
|
"chorus_weak": {
|
|
"lyric_change": "Add [Energy: High] before chorus, consider [Build-Up] section",
|
|
},
|
|
"too_long": {
|
|
"lyric_change": "Remove repeated sections or shorten verses",
|
|
},
|
|
"too_short": {
|
|
"lyric_change": "Add additional verse or extend instrumental sections",
|
|
},
|
|
},
|
|
"lyrics": {
|
|
"phrasing_unnatural": {
|
|
"lyric_change": "Run syllable counter, normalize line lengths within sections",
|
|
},
|
|
"content_mismatch": {
|
|
"lyric_change": "Review lyrics against intended mood/theme, revise for alignment",
|
|
},
|
|
"vocal_style_inconsistent": {
|
|
"lyric_change": "Add consistent [Vocal Style: ...] tags before each section",
|
|
},
|
|
},
|
|
"quality": {
|
|
"artifacts": {
|
|
"note": "Audio artifacts are generation-specific. Regenerate 3-5 times before modifying prompt. If persistent, simplify style prompt.",
|
|
},
|
|
"robotic_vocals": {
|
|
"add": ["natural vocal", "organic phrasing", "human delivery", "breathy"],
|
|
"remove_patterns": [],
|
|
"exclude_add": ["no auto-tune", "no robotic vocals"],
|
|
},
|
|
"clipping": {
|
|
"add": ["clean mix", "dynamic range", "headroom"],
|
|
"remove_patterns": ["heavy", "distorted", "loud", "wall of sound"],
|
|
"exclude_add": [],
|
|
},
|
|
"muffled": {
|
|
"add": ["crisp", "clear mix", "defined frequencies", "bright"],
|
|
"remove_patterns": ["warm", "lo-fi", "analog"],
|
|
"exclude_add": [],
|
|
},
|
|
},
|
|
"length": {
|
|
"too_short": {
|
|
"lyric_change": "Add sections in lyrics (additional verse, bridge, instrumental break) or use Suno extend feature",
|
|
},
|
|
"too_long": {
|
|
"lyric_change": "Remove repeated sections, trim [Outro] content, remove non-essential [Breakdown]",
|
|
},
|
|
"intro_too_long": {
|
|
"lyric_change": "Shorten or remove [Intro] content, add [Verse 1] tag earlier",
|
|
},
|
|
"outro_cuts_off": {
|
|
"lyric_change": "Add explicit [Outro] section with 2-4 lines, add [Fade Out] metatag",
|
|
},
|
|
"pacing_drags": {
|
|
"lyric_change": "Add [Energy: building] metatags, shorten dragging sections, add [Breakdown] or [Build-Up] for variety",
|
|
},
|
|
},
|
|
}
|
|
|
|
SLIDER_DIRECTION_MAP = {
|
|
"increase_slightly": "+5-10 from current",
|
|
"increase": "+15-20 from current",
|
|
"increase_significantly": "+25-35 from current (cap at 85)",
|
|
"decrease_slightly": "-5-10 from current",
|
|
"decrease": "-15-20 from current",
|
|
"decrease_significantly": "-25-35 from current (floor at 15)",
|
|
"unchanged": "no change recommended",
|
|
}
|
|
|
|
|
|
def generate_adjustments(
|
|
dimensions: list[dict[str, str]],
|
|
current_tier: str = "",
|
|
) -> dict[str, Any]:
|
|
"""Generate adjustment recommendations from feedback dimensions."""
|
|
style_add: list[str] = []
|
|
style_remove: list[str] = []
|
|
exclude_add: list[str] = []
|
|
slider_adjustments: dict[str, str] = {}
|
|
lyric_changes: list[str] = []
|
|
notes: list[str] = []
|
|
|
|
for dim_entry in dimensions:
|
|
dimension = dim_entry.get("dimension", "")
|
|
direction = dim_entry.get("direction", "")
|
|
|
|
if dimension not in STYLE_PROMPT_ADJUSTMENTS:
|
|
notes.append(f"Unknown dimension '{dimension}' — requires LLM judgment")
|
|
continue
|
|
|
|
dim_adjustments = STYLE_PROMPT_ADJUSTMENTS[dimension]
|
|
if direction not in dim_adjustments:
|
|
available = list(dim_adjustments.keys())
|
|
notes.append(
|
|
f"Unknown direction '{direction}' for dimension '{dimension}'. "
|
|
f"Available: {', '.join(available)}"
|
|
)
|
|
continue
|
|
|
|
adj = dim_adjustments[direction]
|
|
|
|
if "add" in adj:
|
|
style_add.extend(adj["add"])
|
|
if "remove_patterns" in adj:
|
|
style_remove.extend(adj["remove_patterns"])
|
|
if "exclude_add" in adj:
|
|
exclude_add.extend(adj["exclude_add"])
|
|
if "slider" in adj:
|
|
for slider_name, slider_dir in adj["slider"].items():
|
|
slider_adjustments[slider_name] = SLIDER_DIRECTION_MAP.get(
|
|
slider_dir, slider_dir
|
|
)
|
|
if "lyric_change" in adj:
|
|
lyric_changes.append(adj["lyric_change"])
|
|
if "note" in adj:
|
|
notes.append(adj["note"])
|
|
|
|
is_paid = current_tier.lower() in PAID_TIERS if current_tier else False
|
|
|
|
result: dict[str, Any] = {
|
|
"style_prompt": {
|
|
"add_descriptors": list(dict.fromkeys(style_add)), # dedupe preserving order
|
|
"remove_patterns": list(dict.fromkeys(style_remove)),
|
|
},
|
|
"exclusions": {
|
|
"add": list(dict.fromkeys(exclude_add)),
|
|
},
|
|
}
|
|
|
|
if slider_adjustments:
|
|
if is_paid:
|
|
result["sliders"] = slider_adjustments
|
|
else:
|
|
result["sliders"] = {
|
|
"note": "Slider adjustments recommended but not available on free tier. Compensate through style prompt wording.",
|
|
"recommended_if_upgraded": slider_adjustments,
|
|
}
|
|
|
|
if lyric_changes:
|
|
result["lyrics"] = {"changes": lyric_changes}
|
|
|
|
if notes:
|
|
result["notes"] = notes
|
|
|
|
consistency_warnings = check_adjustment_consistency(result)
|
|
if consistency_warnings:
|
|
if "notes" not in result:
|
|
result["notes"] = []
|
|
result["consistency_warnings"] = consistency_warnings
|
|
|
|
return result
|
|
|
|
|
|
def check_adjustment_consistency(adjustments: dict[str, Any]) -> list[dict[str, Any]]:
|
|
"""Check for internal contradictions in adjustment recommendations."""
|
|
warnings = []
|
|
|
|
style_add = set(adjustments.get("style_prompt", {}).get("add_descriptors", []))
|
|
style_remove = set(adjustments.get("style_prompt", {}).get("remove_patterns", []))
|
|
exclude_add = set(adjustments.get("exclusions", {}).get("add", []))
|
|
|
|
# Check for add/remove conflicts
|
|
conflicts = style_add & style_remove
|
|
if conflicts:
|
|
warnings.append({
|
|
"type": "add_remove_conflict",
|
|
"detail": f"Descriptors appear in both add and remove: {', '.join(conflicts)}",
|
|
})
|
|
|
|
# Check for add/exclude conflicts
|
|
for add_desc in style_add:
|
|
for excl in exclude_add:
|
|
# Simple substring check
|
|
if add_desc.lower() in excl.lower() or excl.replace("no ", "").lower() in add_desc.lower():
|
|
warnings.append({
|
|
"type": "add_exclude_conflict",
|
|
"detail": f"Adding '{add_desc}' conflicts with exclusion '{excl}'",
|
|
})
|
|
|
|
# Check style prompt estimated length
|
|
total_add_chars = sum(len(d) + 2 for d in style_add) # +2 for ", " separator
|
|
if total_add_chars > CRITICAL_ZONE:
|
|
warnings.append({
|
|
"type": "critical_zone_overflow",
|
|
"detail": f"Added descriptors total ~{total_add_chars} chars — prioritize most important for the first {CRITICAL_ZONE} chars of style prompt (critical zone)",
|
|
})
|
|
|
|
# Check exclusion estimated length
|
|
total_excl_chars = sum(len(e) + 2 for e in exclude_add)
|
|
if total_excl_chars > EXCLUSION_RECOMMENDED_MAX:
|
|
warnings.append({
|
|
"type": "exclusion_overflow",
|
|
"detail": f"Exclusion additions total ~{total_excl_chars} chars — keep total exclusions under ~{EXCLUSION_RECOMMENDED_MAX} chars, prioritize 2-3 most important",
|
|
})
|
|
|
|
return warnings
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Map feedback dimensions to Suno parameter adjustment recommendations.",
|
|
epilog="""
|
|
Input JSON schema:
|
|
Required:
|
|
dimensions (array of objects) - Each with:
|
|
dimension (string) - Feedback dimension (instrumentation, vocals, energy, tempo, production, vibe, music, structure, lyrics)
|
|
direction (string) - Direction of the issue within the dimension
|
|
|
|
Optional:
|
|
tier (string) - User's Suno tier (free, pro, premier) — affects slider recommendations
|
|
|
|
Dimension/Direction combinations:
|
|
instrumentation: too_much, too_little, wrong_type
|
|
vocals: too_polished, too_rough, too_quiet, too_loud, wrong_character
|
|
energy: too_high, too_low, flat
|
|
tempo: too_fast, too_slow
|
|
production: too_polished, too_rough, too_reverb, too_dry
|
|
vibe: too_happy, too_dark, too_generic, too_weird
|
|
music: general_issue
|
|
structure: needs_bridge, chorus_weak, too_long, too_short
|
|
lyrics: phrasing_unnatural, content_mismatch, vocal_style_inconsistent
|
|
|
|
Example:
|
|
echo '{"dimensions": [{"dimension": "vocals", "direction": "too_polished"}, {"dimension": "energy", "direction": "too_low"}], "tier": "pro"}' | python3 map-adjustments.py --stdin
|
|
""",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
input_group = parser.add_mutually_exclusive_group(required=True)
|
|
input_group.add_argument("--input", "-i", help="Path to dimensions JSON file")
|
|
input_group.add_argument("--stdin", action="store_true", help="Read JSON from stdin")
|
|
parser.add_argument("--output", "-o", help="Output file path (default: stdout)")
|
|
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output to stderr")
|
|
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
if args.stdin:
|
|
raw = sys.stdin.read()
|
|
else:
|
|
with open(args.input, "r") as f:
|
|
raw = f.read()
|
|
|
|
data = json.loads(raw)
|
|
except (json.JSONDecodeError, FileNotFoundError) as e:
|
|
print(json.dumps({
|
|
"script": "map-adjustments",
|
|
"version": "1.0.0",
|
|
"status": "fail",
|
|
"findings": [{
|
|
"severity": "critical",
|
|
"category": "structure",
|
|
"issue": str(e),
|
|
"fix": "Provide valid JSON input",
|
|
}],
|
|
"summary": {"total": 1, "critical": 1, "high": 0, "medium": 0, "low": 0},
|
|
}, indent=2))
|
|
sys.exit(1)
|
|
|
|
if not isinstance(data, dict) or "dimensions" not in data:
|
|
print(json.dumps({
|
|
"script": "map-adjustments",
|
|
"version": "1.0.0",
|
|
"status": "fail",
|
|
"findings": [{
|
|
"severity": "critical",
|
|
"category": "structure",
|
|
"issue": "Input must be a JSON object with a 'dimensions' array",
|
|
"fix": 'Provide {"dimensions": [{"dimension": "...", "direction": "..."}]}',
|
|
}],
|
|
"summary": {"total": 1, "critical": 1, "high": 0, "medium": 0, "low": 0},
|
|
}, indent=2))
|
|
sys.exit(1)
|
|
|
|
dimensions = data["dimensions"]
|
|
tier = data.get("tier", "")
|
|
|
|
adjustments = generate_adjustments(dimensions, tier)
|
|
|
|
result = {
|
|
"script": "map-adjustments",
|
|
"version": "1.0.0",
|
|
"status": "pass",
|
|
"adjustments": adjustments,
|
|
"input_dimensions": len(dimensions),
|
|
"findings": [],
|
|
"summary": {"total": 0, "critical": 0, "high": 0, "medium": 0, "low": 0},
|
|
}
|
|
|
|
if args.verbose:
|
|
print(f"[map-adjustments] Processed {len(dimensions)} dimensions", file=sys.stderr)
|
|
|
|
output_json = json.dumps(result, indent=2)
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
f.write(output_json)
|
|
else:
|
|
print(output_json)
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|