Files
Momento/.agents/skills/suno-feedback-elicitor/scripts/map-adjustments.py
Antigravity bd495be965
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
- 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
2026-05-16 12:59:30 +00:00

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()