Compile complete Suno style prompts

This commit is contained in:
diyaa
2026-09-14 22:06:32 +02:00
parent 4fa477bdb0
commit 9e6e94e7cb
3 changed files with 123 additions and 3 deletions
@@ -49,11 +49,12 @@ public struct SongProjectPromptCompiler: PromptCompiling {
var components: [String] = [] var components: [String] = []
append(project.genres.map(\.name).joined(separator: " + "), to: &components) append(project.genres.map(\.name).joined(separator: " + "), to: &components)
append(project.moods.map(\.name).joined(separator: ", "), to: &components) append(moodPhrase(for: project), to: &components)
append(context.sunoStyleInstrumentPhrase, to: &components) append(context.sunoStyleInstrumentPhrase, to: &components)
append(vocalPhrase(for: project), to: &components) append(vocalPhrase(for: project), to: &components)
append(musicalParametersPhrase(for: project), to: &components) append(musicalParametersPhrase(for: project), to: &components)
append(project.productionDirections.map(\.text).joined(separator: ", "), to: &components) append(emotionPhrase(for: project), to: &components)
append(productionPhrase(for: project), to: &components)
return components.joined(separator: "; ") return components.joined(separator: "; ")
} }
@@ -88,6 +89,76 @@ public struct SongProjectPromptCompiler: PromptCompiling {
return parameters.joined(separator: ", ") return parameters.joined(separator: ", ")
} }
private func moodPhrase(for project: SongProject) -> String {
let moods = project.moods.map(\.name).compactMap(normalized)
guard !moods.isEmpty else { return "" }
return "mood: \(moods.joined(separator: ", "))"
}
private func emotionPhrase(for project: SongProject) -> String {
let sectionEmotions = project.orderedSections.compactMap { section in
directedPhrase(
sectionTitle: section.title,
emotion: section.emotion,
energy: section.energy,
direction: nil
)
}
let arcEmotions = project.emotionalArc.compactMap { point in
directedPhrase(
sectionTitle: sectionTitle(for: point.sectionID, in: project),
emotion: point.emotion,
energy: point.energy,
direction: point.direction
)
}
let details = unique(sectionEmotions + arcEmotions)
guard !details.isEmpty else { return "" }
return "emotion: \(details.joined(separator: "; "))"
}
private func productionPhrase(for project: SongProject) -> String {
let sectionDirections = project.orderedSections.compactMap { section in
directedPhrase(sectionTitle: section.title, direction: section.productionDirection)
}
let projectDirections = project.productionDirections.compactMap { direction in
directedPhrase(
sectionTitle: sectionTitle(for: direction.sectionID, in: project),
direction: direction.text
)
}
let details = unique(sectionDirections + projectDirections)
guard !details.isEmpty else { return "" }
return "production: \(details.joined(separator: "; "))"
}
private func directedPhrase(
sectionTitle: String?,
emotion: String? = nil,
energy: Int? = nil,
direction: String? = nil
) -> String? {
var details = [normalized(emotion), energy.map { "energy \($0)" }, normalized(direction)]
.compactMap { $0 }
guard !details.isEmpty else { return nil }
if let sectionTitle = normalized(sectionTitle) {
details.insert(sectionTitle, at: 0)
}
return details.joined(separator: ", ")
}
private func sectionTitle(for sectionID: String?, in project: SongProject) -> String? {
guard let sectionID else { return nil }
return project.sections.first { $0.id == sectionID }?.title
}
private func unique(_ values: [String]) -> [String] {
var seen = Set<String>()
return values.filter { seen.insert($0).inserted }
}
private func append(_ value: String?, to components: inout [String]) { private func append(_ value: String?, to components: inout [String]) {
guard let normalizedValue = normalized(value) else { return } guard let normalizedValue = normalized(value) else { return }
components.append(normalizedValue) components.append(normalizedValue)
@@ -93,4 +93,53 @@ final class SongProjectPromptCompilerTests: XCTestCase {
"[Intro - whispered lead]\nFirst line\n\n[Chorus]\nApproved hook" "[Intro - whispered lead]\nFirst line\n\n[Chorus]\nApproved hook"
) )
} }
func testCompilerIncludesStyleInputsFromTheSongProject() throws {
let project = SongProject(
title: "Full Style",
idea: "Compile every approved style input",
genres: [
GenreStyle(id: "folk", name: "Folk"),
GenreStyle(id: "electronic", name: "Electronic")
],
moods: [MoodTag(id: "warm", name: "Warm")],
emotionalArc: [
EmotionalArcPoint(
id: "chorus-arc",
sectionID: "chorus",
emotion: "joyful",
energy: 8,
direction: "lift into release"
)
],
bpm: ManualAutoValue(mode: .manual, value: 108),
key: ManualAutoValue(mode: .manual, value: "D minor"),
scale: ManualAutoValue(mode: .manual, value: "Dorian"),
sections: [
SongSection(
id: "verse",
type: .verse,
title: "Verse",
emotion: "yearning",
energy: 3,
productionDirection: "intimate opening"
),
SongSection(id: "chorus", type: .chorus, title: "Chorus", order: 1)
],
vocalists: [
Vocalist(id: "lead", label: "Lead", voiceType: "clear alto", performanceStyle: "expressive")
],
productionDirections: [
ProductionDirection(id: "chorus-width", sectionID: "chorus", text: "wide chorus"),
ProductionDirection(id: "master", text: "warm tape saturation")
]
)
let output = try SongProjectPromptCompiler().compile(project: project)
XCTAssertEqual(
output.stylePrompt,
"Folk + Electronic; mood: Warm; Lead clear alto expressive; 108 BPM, key D minor, Dorian scale; emotion: Verse, yearning, energy 3; Chorus, joyful, energy 8, lift into release; production: Verse, intimate opening; Chorus, wide chorus; warm tape saturation"
)
}
} }
+1 -1
View File
@@ -100,7 +100,7 @@ requirement is missing and blocks implementation, record it in
output. output.
- [x] Generate lyrics text with section/performance directives where - [x] Generate lyrics text with section/performance directives where
appropriate. appropriate.
- [ ] Generate style prompt from genre blend, instrumentation, vocals, - [x] Generate style prompt from genre blend, instrumentation, vocals,
tempo, harmony, emotion and production instructions. tempo, harmony, emotion and production instructions.
- [ ] Validate output before handoff. - [ ] Validate output before handoff.
- [ ] Keep compiler independent from UI and OpenAI client. - [ ] Keep compiler independent from UI and OpenAI client.