Compile complete Suno style prompts
This commit is contained in:
@@ -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
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user