From 9e6e94e7cbc0aef93537479f5d1c9477fd756343 Mon Sep 17 00:00:00 2001 From: diyaa Date: Mon, 14 Sep 2026 22:06:32 +0200 Subject: [PATCH] Compile complete Suno style prompts --- .../PromptCompiler/PromptCompiling.swift | 75 ++++++++++++++++++- .../SongProjectPromptCompilerTests.swift | 49 ++++++++++++ docs/TASKS.md | 2 +- 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/Sources/MusicAssistantCore/Services/PromptCompiler/PromptCompiling.swift b/Sources/MusicAssistantCore/Services/PromptCompiler/PromptCompiling.swift index a4dc16d..ee1964b 100644 --- a/Sources/MusicAssistantCore/Services/PromptCompiler/PromptCompiling.swift +++ b/Sources/MusicAssistantCore/Services/PromptCompiler/PromptCompiling.swift @@ -49,11 +49,12 @@ public struct SongProjectPromptCompiler: PromptCompiling { var components: [String] = [] 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(vocalPhrase(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: "; ") } @@ -88,6 +89,76 @@ public struct SongProjectPromptCompiler: PromptCompiling { 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() + return values.filter { seen.insert($0).inserted } + } + private func append(_ value: String?, to components: inout [String]) { guard let normalizedValue = normalized(value) else { return } components.append(normalizedValue) diff --git a/Tests/MusicAssistantCoreTests/SongProjectPromptCompilerTests.swift b/Tests/MusicAssistantCoreTests/SongProjectPromptCompilerTests.swift index a6389da..59aa9d5 100644 --- a/Tests/MusicAssistantCoreTests/SongProjectPromptCompilerTests.swift +++ b/Tests/MusicAssistantCoreTests/SongProjectPromptCompilerTests.swift @@ -93,4 +93,53 @@ final class SongProjectPromptCompilerTests: XCTestCase { "[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" + ) + } } diff --git a/docs/TASKS.md b/docs/TASKS.md index 91cabe8..9721042 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -100,7 +100,7 @@ requirement is missing and blocks implementation, record it in output. - [x] Generate lyrics text with section/performance directives where appropriate. -- [ ] Generate style prompt from genre blend, instrumentation, vocals, +- [x] Generate style prompt from genre blend, instrumentation, vocals, tempo, harmony, emotion and production instructions. - [ ] Validate output before handoff. - [ ] Keep compiler independent from UI and OpenAI client.