import Foundation public struct SongProject: Codable, Equatable, Identifiable, Sendable { public let id: String public var title: String public var idea: String public var duration: SongDuration? public var conversationMode: ConversationMode public var languages: [LanguageSelection] public var dialects: [DialectSelection] public var arabicPronunciationSettings: ArabicPronunciationSettings public var genres: [GenreStyle] public var moods: [MoodTag] public var emotionalArc: [EmotionalArcPoint] public var bpm: ManualAutoValue? public var key: ManualAutoValue? public var scale: ManualAutoValue? public var maqam: ManualAutoValue? public var sections: [SongSection] public var instruments: [InstrumentTrack] public var vocalists: [Vocalist] public var lyrics: Lyrics public var productionDirections: [ProductionDirection] public var sunoOutput: SunoOutput? public var versions: [SongVersion] public let createdAt: Date public var updatedAt: Date public init( id: String = UUID().uuidString, title: String, idea: String, duration: SongDuration? = nil, conversationMode: ConversationMode = .auto, languages: [LanguageSelection] = [], dialects: [DialectSelection] = [], arabicPronunciationSettings: ArabicPronunciationSettings = ArabicPronunciationSettings(), genres: [GenreStyle] = [], moods: [MoodTag] = [], emotionalArc: [EmotionalArcPoint] = [], bpm: ManualAutoValue? = nil, key: ManualAutoValue? = nil, scale: ManualAutoValue? = nil, maqam: ManualAutoValue? = nil, sections: [SongSection] = [], instruments: [InstrumentTrack] = [], vocalists: [Vocalist] = [], lyrics: Lyrics = Lyrics(), productionDirections: [ProductionDirection] = [], sunoOutput: SunoOutput? = nil, versions: [SongVersion] = [], createdAt: Date = Date(), updatedAt: Date = Date() ) { self.id = id self.title = title self.idea = idea self.duration = duration self.conversationMode = conversationMode self.languages = languages self.dialects = dialects self.arabicPronunciationSettings = arabicPronunciationSettings self.genres = genres self.moods = moods self.emotionalArc = emotionalArc self.bpm = bpm self.key = key self.scale = scale self.maqam = maqam self.sections = sections self.instruments = instruments self.vocalists = vocalists self.lyrics = lyrics self.productionDirections = productionDirections self.sunoOutput = sunoOutput self.versions = versions self.createdAt = createdAt self.updatedAt = updatedAt } } public extension SongProject { var orderedSections: [SongSection] { sections .enumerated() .sorted { left, right in if left.element.order == right.element.order { return left.offset < right.offset } return left.element.order < right.element.order } .map(\.element) } var selectedGenreStyleIDs: Set { Set(genres.map(\.id)) } func isGenreStyleSelected(_ genreStyle: GenreStyle) -> Bool { selectedGenreStyleIDs.contains(genreStyle.id) } mutating func selectGenreStyle(_ genreStyle: GenreStyle) { guard !isGenreStyleSelected(genreStyle) else { return } genres.append(genreStyle) } mutating func deselectGenreStyle(id: String) { genres.removeAll { $0.id == id } } var languageIDs: Set { Set(languages.map(\.id)) } var dialectIDs: Set { Set(dialects.map(\.id)) } func language(for languageID: String) -> LanguageSelection? { languages.first { $0.id == languageID } } func dialect(for dialectID: String) -> DialectSelection? { dialects.first { $0.id == dialectID } } func dialects(forLanguageID languageID: String) -> [DialectSelection] { dialects.filter { $0.languageID == languageID } } mutating func upsertLanguage(_ language: LanguageSelection) { if let index = languages.firstIndex(where: { $0.id == language.id }) { languages[index] = language } else { languages.append(language) } } mutating func removeLanguage(id languageID: String) { languages.removeAll { $0.id == languageID } dialects.removeAll { $0.languageID == languageID } } mutating func upsertDialect(_ dialect: DialectSelection) { if let index = dialects.firstIndex(where: { $0.id == dialect.id }) { dialects[index] = dialect } else { dialects.append(dialect) } } mutating func removeDialect(id dialectID: String) { dialects.removeAll { $0.id == dialectID } } mutating func setBPMManually(_ value: Int) { bpm = ManualAutoValue(mode: .manual, value: value) } mutating func setBPMAutomatic() { bpm = ManualAutoValue(mode: .auto) } mutating func setKeyManually(_ value: String) { key = ManualAutoValue(mode: .manual, value: value) } mutating func setKeyAutomatic() { key = ManualAutoValue(mode: .auto) } mutating func setScaleManually(_ value: String) { scale = ManualAutoValue(mode: .manual, value: value) } mutating func setScaleAutomatic() { scale = ManualAutoValue(mode: .auto) } mutating func setMaqamManually(_ value: String) { maqam = ManualAutoValue(mode: .manual, value: value) } mutating func setMaqamAutomatic() { maqam = ManualAutoValue(mode: .auto) } func emotionalArcPoint(for pointID: String) -> EmotionalArcPoint? { emotionalArc.first { $0.id == pointID } } func emotionalArcPoints(forSectionID sectionID: String) -> [EmotionalArcPoint] { emotionalArc.filter { $0.sectionID == sectionID } } mutating func upsertEmotionalArcPoint(_ point: EmotionalArcPoint) { if let index = emotionalArc.firstIndex(where: { $0.id == point.id }) { emotionalArc[index] = point } else { emotionalArc.append(point) } } mutating func removeEmotionalArcPoint(id pointID: String) { emotionalArc.removeAll { $0.id == pointID } } func productionDirection(for directionID: String) -> ProductionDirection? { productionDirections.first { $0.id == directionID } } func productionDirections(forSectionID sectionID: String) -> [ProductionDirection] { productionDirections.filter { $0.sectionID == sectionID } } mutating func upsertProductionDirection(_ direction: ProductionDirection) { if let index = productionDirections.firstIndex(where: { $0.id == direction.id }) { productionDirections[index] = direction } else { productionDirections.append(direction) } } mutating func removeProductionDirection(id directionID: String) { productionDirections.removeAll { $0.id == directionID } } var selectedInstrumentTracks: [InstrumentTrack] { instruments.filter(\.selected) } var selectedInstrumentIDs: Set { Set(selectedInstrumentTracks.map(\.instrumentId)) } func instrumentTrack(for instrumentId: String) -> InstrumentTrack? { instruments.first { $0.instrumentId == instrumentId } } func isInstrumentSelected(id instrumentId: String) -> Bool { instrumentTrack(for: instrumentId)?.selected == true } mutating func selectInstrument(_ instrument: InstrumentTrack) { var selectedInstrument = instrument selectedInstrument.selected = true if let index = instruments.firstIndex(where: { $0.instrumentId == instrument.instrumentId }) { instruments[index] = selectedInstrument } else { instruments.append(selectedInstrument) } } mutating func deselectInstrument(id instrumentId: String) { guard let index = instruments.firstIndex(where: { $0.instrumentId == instrumentId }) else { return } instruments[index].selected = false } mutating func configureInstrument( id instrumentId: String, variant: String?, playingStyle: String?, role: String?, autoArrangementEnabled: Bool ) { guard let index = instruments.firstIndex(where: { $0.instrumentId == instrumentId }) else { return } instruments[index].variant = variant instruments[index].playingStyle = playingStyle instruments[index].role = role instruments[index].autoArrangementEnabled = autoArrangementEnabled } mutating func addInstrumentPlacement(_ placement: InstrumentPlacement, to instrumentId: String) { if let index = instruments.firstIndex(where: { $0.instrumentId == instrumentId }) { instruments[index].placements.append(placement) } else { instruments.append( InstrumentTrack( instrumentId: instrumentId, selected: true, placements: [placement] ) ) } } var vocalistIDs: Set { Set(vocalists.map(\.id)) } func vocalist(for vocalistID: String) -> Vocalist? { vocalists.first { $0.id == vocalistID } } func vocalistsAssigned(toSectionID sectionID: String) -> [Vocalist] { vocalists.filter { $0.assignedSections.contains(sectionID) } } mutating func upsertVocalist(_ vocalist: Vocalist) { var normalizedVocalist = vocalist normalizedVocalist.assignedSections = Self.uniqueOrderedIDs(vocalist.assignedSections) if let index = vocalists.firstIndex(where: { $0.id == vocalist.id }) { vocalists[index] = normalizedVocalist } else { vocalists.append(normalizedVocalist) } } mutating func removeVocalist(id vocalistID: String) { vocalists.removeAll { $0.id == vocalistID } } mutating func assignSection(_ sectionID: String, toVocalistID vocalistID: String) { guard let index = vocalists.firstIndex(where: { $0.id == vocalistID }) else { return } guard !vocalists[index].assignedSections.contains(sectionID) else { return } vocalists[index].assignedSections.append(sectionID) } mutating func unassignSection(_ sectionID: String, fromVocalistID vocalistID: String) { guard let index = vocalists.firstIndex(where: { $0.id == vocalistID }) else { return } vocalists[index].assignedSections.removeAll { $0 == sectionID } } mutating func createVersion( id: String = UUID().uuidString, label: String, createdAt: Date = Date() ) -> SongVersion { let version = SongVersion( id: id, label: label, snapshot: SongProjectSnapshot(project: self), createdAt: createdAt ) versions.append(version) return version } func version(id versionID: String) -> SongVersion? { versions.first { $0.id == versionID } } mutating func restoreVersion(id versionID: String, restoredAt: Date = Date()) -> Bool { guard let version = version(id: versionID) else { return false } restoreSnapshot(version.snapshot, updatedAt: restoredAt) return true } mutating func restoreSnapshot(_ snapshot: SongProjectSnapshot, updatedAt: Date = Date()) { title = snapshot.title idea = snapshot.idea duration = snapshot.duration conversationMode = snapshot.conversationMode languages = snapshot.languages dialects = snapshot.dialects arabicPronunciationSettings = snapshot.arabicPronunciationSettings genres = snapshot.genres moods = snapshot.moods emotionalArc = snapshot.emotionalArc bpm = snapshot.bpm key = snapshot.key scale = snapshot.scale maqam = snapshot.maqam sections = snapshot.sections instruments = snapshot.instruments vocalists = snapshot.vocalists lyrics = snapshot.lyrics productionDirections = snapshot.productionDirections sunoOutput = snapshot.sunoOutput self.updatedAt = updatedAt } mutating func appendSection(_ section: SongSection) { var orderedSection = section orderedSection.order = nextSectionOrder sections.append(orderedSection) } mutating func moveSection(id: String, toOrder targetOrder: Int) { var ordered = orderedSections guard let sourceIndex = ordered.firstIndex(where: { $0.id == id }) else { return } let section = ordered.remove(at: sourceIndex) let boundedTargetOrder = min(max(targetOrder, 0), ordered.count) ordered.insert(section, at: boundedTargetOrder) sections = Self.sectionsWithNormalizedOrder(ordered) } mutating func normalizeSectionOrder() { sections = Self.sectionsWithNormalizedOrder(orderedSections) } private var nextSectionOrder: Int { (sections.map(\.order).max() ?? -1) + 1 } private static func sectionsWithNormalizedOrder(_ sections: [SongSection]) -> [SongSection] { sections.enumerated().map { index, section in var orderedSection = section orderedSection.order = index return orderedSection } } private static func uniqueOrderedIDs(_ ids: [String]) -> [String] { var seenIDs = Set() return ids.filter { seenIDs.insert($0).inserted } } } public enum ConversationMode: String, Codable, Equatable, Sendable { case auto case discuss } public enum ControlMode: String, Codable, Equatable, Sendable { case manual case auto } public struct ManualAutoValue: Codable, Equatable, Sendable { public var mode: ControlMode public var value: Value? public init(mode: ControlMode, value: Value? = nil) { self.mode = mode self.value = value } } public struct SongDuration: Codable, Equatable, Sendable { public var seconds: Int? public var description: String? public init(seconds: Int? = nil, description: String? = nil) { self.seconds = seconds self.description = description } } public struct LanguageSelection: Codable, Equatable, Identifiable, Sendable { public let id: String public var name: String public init(id: String = UUID().uuidString, name: String) { self.id = id self.name = name } } public struct DialectSelection: Codable, Equatable, Identifiable, Sendable { public let id: String public var name: String public var languageID: String? public init(id: String = UUID().uuidString, name: String, languageID: String? = nil) { self.id = id self.name = name self.languageID = languageID } } public struct ArabicPronunciationSettings: Codable, Equatable, Sendable { public var isEnabled: Bool public var diacritizationPolicy: ArabicDiacritizationPolicy public var tanweenPolicy: ArabicTanweenPolicy public var userNotes: String? public var preservedSpellings: [String] public init( isEnabled: Bool = false, diacritizationPolicy: ArabicDiacritizationPolicy = .unspecified, tanweenPolicy: ArabicTanweenPolicy = .unspecified, userNotes: String? = nil, preservedSpellings: [String] = [] ) { self.isEnabled = isEnabled self.diacritizationPolicy = diacritizationPolicy self.tanweenPolicy = tanweenPolicy self.userNotes = userNotes self.preservedSpellings = preservedSpellings } } public enum ArabicDiacritizationPolicy: String, Codable, Equatable, Sendable { case unspecified case pronunciationTargeted case fullTashkeel } public enum ArabicTanweenPolicy: String, Codable, Equatable, Sendable { case unspecified case preserveUserInput case addWhenPronunciationRequires case removeWhenUnwanted } public struct GenreStyle: Codable, Equatable, Identifiable, Sendable { public let id: String public var name: String public var aliases: [String] public init(id: String = UUID().uuidString, name: String, aliases: [String] = []) { self.id = id self.name = name self.aliases = aliases } } public struct MoodTag: Codable, Equatable, Identifiable, Sendable { public let id: String public var name: String public init(id: String = UUID().uuidString, name: String) { self.id = id self.name = name } } public struct EmotionalArcPoint: Codable, Equatable, Identifiable, Sendable { public let id: String public var sectionID: String? public var emotion: String public var energy: Int? public var direction: String? public init( id: String = UUID().uuidString, sectionID: String? = nil, emotion: String, energy: Int? = nil, direction: String? = nil ) { self.id = id self.sectionID = sectionID self.emotion = emotion self.energy = energy self.direction = direction } } public struct Lyrics: Codable, Equatable, Sendable { public var text: String public var sectionTexts: [String: String] public init(text: String = "", sectionTexts: [String: String] = [:]) { self.text = text self.sectionTexts = sectionTexts } } public struct ProductionDirection: Codable, Equatable, Identifiable, Sendable { public let id: String public var sectionID: String? public var text: String public init(id: String = UUID().uuidString, sectionID: String? = nil, text: String) { self.id = id self.sectionID = sectionID self.text = text } } public struct InstrumentCatalogItem: Codable, Equatable, Identifiable, Sendable { public let id: String public var name: String public var familyCategory: String public var regionOrigin: String? public var aliases: [String] public var searchTerms: [String] public init( id: String, name: String, familyCategory: String, regionOrigin: String? = nil, aliases: [String] = [], searchTerms: [String] = [] ) { self.id = id self.name = name self.familyCategory = familyCategory self.regionOrigin = regionOrigin self.aliases = aliases self.searchTerms = searchTerms } } public struct SongSection: Codable, Equatable, Identifiable, Sendable { public let id: String public var type: SongSectionType public var title: String public var order: Int public var startTime: TimeInterval? public var endTime: TimeInterval? public var lyrics: String public var emotion: String? public var energy: Int? public var vocalDirection: String? public var productionDirection: String? public init( id: String = UUID().uuidString, type: SongSectionType, title: String, order: Int = 0, startTime: TimeInterval? = nil, endTime: TimeInterval? = nil, lyrics: String = "", emotion: String? = nil, energy: Int? = nil, vocalDirection: String? = nil, productionDirection: String? = nil ) { self.id = id self.type = type self.title = title self.order = order self.startTime = startTime self.endTime = endTime self.lyrics = lyrics self.emotion = emotion self.energy = energy self.vocalDirection = vocalDirection self.productionDirection = productionDirection } } public enum SongSectionType: String, Codable, Equatable, Sendable { case intro case verse case preChorus case chorus case bridge case outro case custom } public struct InstrumentTrack: Codable, Equatable, Sendable { public var instrumentId: String public var selected: Bool public var variant: String? public var playingStyle: String? public var role: String? public var autoArrangementEnabled: Bool public var placements: [InstrumentPlacement] public init( instrumentId: String, selected: Bool = true, variant: String? = nil, playingStyle: String? = nil, role: String? = nil, autoArrangementEnabled: Bool = true, placements: [InstrumentPlacement] = [] ) { self.instrumentId = instrumentId self.selected = selected self.variant = variant self.playingStyle = playingStyle self.role = role self.autoArrangementEnabled = autoArrangementEnabled self.placements = placements } } public struct InstrumentPlacement: Codable, Equatable, Sendable { public var sectionId: String? public var startTime: TimeInterval? public var endTime: TimeInterval? public var direction: String? public init( sectionId: String? = nil, startTime: TimeInterval? = nil, endTime: TimeInterval? = nil, direction: String? = nil ) { self.sectionId = sectionId self.startTime = startTime self.endTime = endTime self.direction = direction } } public struct Vocalist: Codable, Equatable, Identifiable, Sendable { public let id: String public var label: String public var voiceType: String? public var genderSelection: String? public var performanceStyle: String? public var assignedSections: [String] public init( id: String = UUID().uuidString, label: String, voiceType: String? = nil, genderSelection: String? = nil, performanceStyle: String? = nil, assignedSections: [String] = [] ) { self.id = id self.label = label self.voiceType = voiceType self.genderSelection = genderSelection self.performanceStyle = performanceStyle self.assignedSections = assignedSections } } public struct SunoOutput: Codable, Equatable, Sendable { public var lyricsText: String public var stylePrompt: String public var additionalFields: [String: String] public var generatedAt: Date public init( lyricsText: String, stylePrompt: String, additionalFields: [String: String] = [:], generatedAt: Date = Date() ) { self.lyricsText = lyricsText self.stylePrompt = stylePrompt self.additionalFields = additionalFields self.generatedAt = generatedAt } } public struct SongVersion: Codable, Equatable, Identifiable, Sendable { public let id: String public var label: String public var snapshot: SongProjectSnapshot public let createdAt: Date public init( id: String = UUID().uuidString, label: String, snapshot: SongProjectSnapshot, createdAt: Date = Date() ) { self.id = id self.label = label self.snapshot = snapshot self.createdAt = createdAt } } public struct SongProjectSnapshot: Codable, Equatable, Sendable { public var title: String public var idea: String public var duration: SongDuration? public var conversationMode: ConversationMode public var languages: [LanguageSelection] public var dialects: [DialectSelection] public var arabicPronunciationSettings: ArabicPronunciationSettings public var genres: [GenreStyle] public var moods: [MoodTag] public var emotionalArc: [EmotionalArcPoint] public var bpm: ManualAutoValue? public var key: ManualAutoValue? public var scale: ManualAutoValue? public var maqam: ManualAutoValue? public var sections: [SongSection] public var instruments: [InstrumentTrack] public var vocalists: [Vocalist] public var lyrics: Lyrics public var productionDirections: [ProductionDirection] public var sunoOutput: SunoOutput? public init( title: String, idea: String, duration: SongDuration? = nil, conversationMode: ConversationMode = .auto, languages: [LanguageSelection] = [], dialects: [DialectSelection] = [], arabicPronunciationSettings: ArabicPronunciationSettings = ArabicPronunciationSettings(), genres: [GenreStyle] = [], moods: [MoodTag] = [], emotionalArc: [EmotionalArcPoint] = [], bpm: ManualAutoValue? = nil, key: ManualAutoValue? = nil, scale: ManualAutoValue? = nil, maqam: ManualAutoValue? = nil, sections: [SongSection] = [], instruments: [InstrumentTrack] = [], vocalists: [Vocalist] = [], lyrics: Lyrics = Lyrics(), productionDirections: [ProductionDirection] = [], sunoOutput: SunoOutput? = nil ) { self.title = title self.idea = idea self.duration = duration self.conversationMode = conversationMode self.languages = languages self.dialects = dialects self.arabicPronunciationSettings = arabicPronunciationSettings self.genres = genres self.moods = moods self.emotionalArc = emotionalArc self.bpm = bpm self.key = key self.scale = scale self.maqam = maqam self.sections = sections self.instruments = instruments self.vocalists = vocalists self.lyrics = lyrics self.productionDirections = productionDirections self.sunoOutput = sunoOutput } public init(project: SongProject) { self.init( title: project.title, idea: project.idea, duration: project.duration, conversationMode: project.conversationMode, languages: project.languages, dialects: project.dialects, arabicPronunciationSettings: project.arabicPronunciationSettings, genres: project.genres, moods: project.moods, emotionalArc: project.emotionalArc, bpm: project.bpm, key: project.key, scale: project.scale, maqam: project.maqam, sections: project.sections, instruments: project.instruments, vocalists: project.vocalists, lyrics: project.lyrics, productionDirections: project.productionDirections, sunoOutput: project.sunoOutput ) } }