Initial commit

This commit is contained in:
diyaa
2026-09-13 20:21:10 +02:00
commit 18d7d56345
42 changed files with 5459 additions and 0 deletions
@@ -0,0 +1,170 @@
import Foundation
public protocol AIService: Sendable {
func generateSongProject(from request: SongProjectGenerationRequest) async throws -> SongProjectGenerationResult
func reviseLyrics(from request: LyricsRevisionRequest) async throws -> LyricsRevisionResult
func proposeProjectUpdate(from request: SongProjectUpdateRequest) async throws -> SongProjectUpdateResult
}
public struct AIConversationMessage: Equatable, Identifiable, Sendable {
public let id: String
public var role: AIConversationRole
public var content: String
public init(
id: String = UUID().uuidString,
role: AIConversationRole,
content: String
) {
self.id = id
self.role = role
self.content = content
}
}
public enum AIConversationRole: String, Equatable, Sendable {
case user
case assistant
case system
}
public struct AIRequestContext: Equatable, Sendable {
public var userInstruction: String
public var conversation: [AIConversationMessage]
public var localeIdentifier: String?
internal var privateApplicationRules: PrivateApplicationRules?
public init(
userInstruction: String,
conversation: [AIConversationMessage] = [],
localeIdentifier: String? = nil
) {
self.userInstruction = userInstruction
self.conversation = conversation
self.localeIdentifier = localeIdentifier
self.privateApplicationRules = nil
}
internal mutating func injectPrivateApplicationRules(_ rules: PrivateApplicationRules?) {
privateApplicationRules = rules
}
}
public struct SongProjectGenerationRequest: Equatable, Sendable {
public var context: AIRequestContext
public var seedProject: SongProject?
public var discussionMode: ConversationMode
public init(
context: AIRequestContext,
seedProject: SongProject? = nil,
discussionMode: ConversationMode = .auto
) {
self.context = context
self.seedProject = seedProject
self.discussionMode = discussionMode
}
}
public struct SongProjectGenerationResult: Equatable, Sendable {
public var project: SongProject
public var notes: [String]
public var followUpQuestions: [String]
public init(
project: SongProject,
notes: [String] = [],
followUpQuestions: [String] = []
) {
self.project = project
self.notes = notes
self.followUpQuestions = followUpQuestions
}
}
public struct LyricsRevisionRequest: Equatable, Sendable {
public var context: AIRequestContext
public var project: SongProject
public var sourceLyrics: Lyrics
public var mode: LyricsRevisionMode
public init(
context: AIRequestContext,
project: SongProject,
sourceLyrics: Lyrics,
mode: LyricsRevisionMode
) {
self.context = context
self.project = project
self.sourceLyrics = sourceLyrics
self.mode = mode
}
}
public enum LyricsRevisionMode: String, Equatable, Sendable {
case writeFromIdea
case correct
case improve
case restructure
case complete
}
public struct LyricsRevisionResult: Equatable, Sendable {
public var lyrics: Lyrics
public var notes: [String]
public init(lyrics: Lyrics, notes: [String] = []) {
self.lyrics = lyrics
self.notes = notes
}
}
public struct SongProjectUpdateRequest: Equatable, Sendable {
public var context: AIRequestContext
public var project: SongProject
public var allowedScopes: [SongProjectUpdateScope]
public init(
context: AIRequestContext,
project: SongProject,
allowedScopes: [SongProjectUpdateScope]
) {
self.context = context
self.project = project
self.allowedScopes = allowedScopes
}
}
public enum SongProjectUpdateScope: String, Codable, Equatable, Sendable {
case duration
case genres
case instruments
case vocals
case languages
case lyrics
case structure
case arrangement
case musicalParameters
case emotionalArc
case productionDirections
case sunoOutput
}
public struct SongProjectUpdateResult: Equatable, Sendable {
public var project: SongProject
public var changedScopes: [SongProjectUpdateScope]
public var notes: [String]
public var followUpQuestions: [String]
public init(
project: SongProject,
changedScopes: [SongProjectUpdateScope] = [],
notes: [String] = [],
followUpQuestions: [String] = []
) {
self.project = project
self.changedScopes = changedScopes
self.notes = notes
self.followUpQuestions = followUpQuestions
}
}
@@ -0,0 +1,73 @@
import Foundation
public struct PrivateApplicationRules: Equatable, Sendable {
public let content: String
public init?(content: String?) {
guard let content else { return nil }
let trimmedContent = content.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedContent.isEmpty else { return nil }
self.content = trimmedContent
}
}
public protocol ApplicationRuleProviding: Sendable {
func privateApplicationRules() throws -> PrivateApplicationRules?
}
public struct StaticApplicationRuleProvider: ApplicationRuleProviding {
private let rules: PrivateApplicationRules?
public init(rules: PrivateApplicationRules?) {
self.rules = rules
}
public func privateApplicationRules() throws -> PrivateApplicationRules? {
rules
}
}
public final class ApplicationRuleInjectingAIService: AIService, Sendable {
private let baseService: any AIService
private let ruleProvider: any ApplicationRuleProviding
public init(
baseService: any AIService,
ruleProvider: any ApplicationRuleProviding
) {
self.baseService = baseService
self.ruleProvider = ruleProvider
}
public func generateSongProject(from request: SongProjectGenerationRequest) async throws -> SongProjectGenerationResult {
try await baseService.generateSongProject(from: requestWithInjectedRules(request))
}
public func reviseLyrics(from request: LyricsRevisionRequest) async throws -> LyricsRevisionResult {
try await baseService.reviseLyrics(from: requestWithInjectedRules(request))
}
public func proposeProjectUpdate(from request: SongProjectUpdateRequest) async throws -> SongProjectUpdateResult {
try await baseService.proposeProjectUpdate(from: requestWithInjectedRules(request))
}
private func requestWithInjectedRules(_ request: SongProjectGenerationRequest) throws -> SongProjectGenerationRequest {
var request = request
request.context.injectPrivateApplicationRules(try ruleProvider.privateApplicationRules())
return request
}
private func requestWithInjectedRules(_ request: LyricsRevisionRequest) throws -> LyricsRevisionRequest {
var request = request
request.context.injectPrivateApplicationRules(try ruleProvider.privateApplicationRules())
return request
}
private func requestWithInjectedRules(_ request: SongProjectUpdateRequest) throws -> SongProjectUpdateRequest {
var request = request
request.context.injectPrivateApplicationRules(try ruleProvider.privateApplicationRules())
return request
}
}
@@ -0,0 +1,37 @@
import Foundation
public struct StructuredSongProjectUpdateResponse: Codable, Equatable, Sendable {
public static let schemaName = "song_project_update_response"
public var project: SongProjectSnapshot
public var changedScopes: [SongProjectUpdateScope]
public var notes: [String]
public var followUpQuestions: [String]
public init(
project: SongProjectSnapshot,
changedScopes: [SongProjectUpdateScope] = [],
notes: [String] = [],
followUpQuestions: [String] = []
) {
self.project = project
self.changedScopes = changedScopes
self.notes = notes
self.followUpQuestions = followUpQuestions
}
public func makeUpdateResult(
updating existingProject: SongProject,
updatedAt: Date = Date()
) -> SongProjectUpdateResult {
var updatedProject = existingProject
updatedProject.restoreSnapshot(project, updatedAt: updatedAt)
return SongProjectUpdateResult(
project: updatedProject,
changedScopes: changedScopes,
notes: notes,
followUpQuestions: followUpQuestions
)
}
}
@@ -0,0 +1,65 @@
import Foundation
public struct ProjectAutosavePolicy: Equatable, Sendable {
public static let `default` = ProjectAutosavePolicy()
public var minimumVersionInterval: TimeInterval
public var versionLabel: String
public init(
minimumVersionInterval: TimeInterval = 300,
versionLabel: String = "Autosave checkpoint"
) {
self.minimumVersionInterval = minimumVersionInterval
self.versionLabel = versionLabel
}
}
public struct ProjectAutosaveResult: Equatable, Sendable {
public var project: SongProject
public var didCreateVersion: Bool
public init(project: SongProject, didCreateVersion: Bool) {
self.project = project
self.didCreateVersion = didCreateVersion
}
}
public actor ProjectAutosaveService {
private let store: any ProjectStore
private let policy: ProjectAutosavePolicy
public init(store: any ProjectStore, policy: ProjectAutosavePolicy = .default) {
self.store = store
self.policy = policy
}
public func autosave(_ project: SongProject, at savedAt: Date = Date()) async throws -> ProjectAutosaveResult {
var autosavedProject = project
var didCreateVersion = false
if shouldCreateAutosaveVersion(for: autosavedProject, at: savedAt) {
_ = autosavedProject.createVersion(label: policy.versionLabel, createdAt: savedAt)
didCreateVersion = true
}
autosavedProject.updatedAt = savedAt
try await store.save(autosavedProject)
return ProjectAutosaveResult(project: autosavedProject, didCreateVersion: didCreateVersion)
}
private func shouldCreateAutosaveVersion(for project: SongProject, at savedAt: Date) -> Bool {
let currentSnapshot = SongProjectSnapshot(project: project)
guard let latestVersion = project.versions.last else {
return true
}
guard latestVersion.snapshot != currentSnapshot else {
return false
}
return savedAt.timeIntervalSince(latestVersion.createdAt) >= policy.minimumVersionInterval
}
}
@@ -0,0 +1,15 @@
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
}
}