Files
music-assistant/Sources/MusicAssistantCore/Services/PromptCompiler/PromptCompiling.swift
T

101 lines
3.6 KiB
Swift

public protocol PromptCompiling: Sendable {
associatedtype Project
func compile(project: Project) throws -> CompiledSunoOutput
}
public struct CompiledSunoOutput: Equatable, Sendable {
public let lyricsText: String
public let stylePrompt: String
public init(lyricsText: String, stylePrompt: String) {
self.lyricsText = lyricsText
self.stylePrompt = stylePrompt
}
}
public struct SongProjectPromptCompiler: PromptCompiling {
public init() {}
public func compile(project: SongProject) throws -> CompiledSunoOutput {
CompiledSunoOutput(
lyricsText: lyricsText(for: project),
stylePrompt: stylePrompt(for: project)
)
}
private func lyricsText(for project: SongProject) -> String {
if !project.lyrics.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return project.lyrics.text
}
return project.orderedSections
.compactMap { sectionLyricsBlock(for: $0, project: project) }
.joined(separator: "\n\n")
}
private func sectionLyricsBlock(for section: SongSection, project: SongProject) -> String? {
let lyrics = project.lyrics.sectionTexts[section.id] ?? section.lyrics
let trimmedLyrics = lyrics.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedLyrics.isEmpty else { return nil }
let performanceDirective = normalized(section.vocalDirection)
let header = performanceDirective.map { "[\(section.title) - \($0)]" } ?? "[\(section.title)]"
return "\(header)\n\(trimmedLyrics)"
}
private func stylePrompt(for project: SongProject) -> String {
let context = SongGenerationContext(project: project)
var components: [String] = []
append(project.genres.map(\.name).joined(separator: " + "), to: &components)
append(project.moods.map(\.name).joined(separator: ", "), 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)
return components.joined(separator: "; ")
}
private func vocalPhrase(for project: SongProject) -> String {
project.vocalists
.map { vocalist in
[
vocalist.label,
vocalist.voiceType,
vocalist.genderSelection,
vocalist.performanceStyle
]
.compactMap { normalized($0) }
.joined(separator: " ")
}
.filter { !$0.isEmpty }
.joined(separator: ", ")
}
private func musicalParametersPhrase(for project: SongProject) -> String {
var parameters: [String] = []
if let bpm = project.bpm?.value {
parameters.append("\(bpm) BPM")
}
append(project.key?.value.map { "key \($0)" }, to: &parameters)
append(project.scale?.value.map { "\($0) scale" }, to: &parameters)
append(project.maqam?.value.map { "maqam \($0)" }, to: &parameters)
return parameters.joined(separator: ", ")
}
private func append(_ value: String?, to components: inout [String]) {
guard let normalizedValue = normalized(value) else { return }
components.append(normalizedValue)
}
private func normalized(_ value: String?) -> String? {
let trimmedValue = value?.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmedValue?.isEmpty == false ? trimmedValue : nil
}
}