From c3ebf95e537738d6d772a8d1377bd2cf0d13c7f4 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 13 Sep 2026 13:29:15 +0200 Subject: [PATCH] Model language and pronunciation settings --- .../Domain/SongProject.swift | 70 ++++++++++++++++++- .../SongProjectModelTests.swift | 44 ++++++++++++ docs/TASKS.md | 2 +- 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/Sources/MusicAssistantCore/Domain/SongProject.swift b/Sources/MusicAssistantCore/Domain/SongProject.swift index c6435a5..41de062 100644 --- a/Sources/MusicAssistantCore/Domain/SongProject.swift +++ b/Sources/MusicAssistantCore/Domain/SongProject.swift @@ -109,6 +109,51 @@ public extension SongProject { 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 } + } + var selectedInstrumentTracks: [InstrumentTrack] { instruments.filter(\.selected) } @@ -299,16 +344,39 @@ public struct DialectSelection: Codable, Equatable, Identifiable, Sendable { 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, userNotes: String? = nil, 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 diff --git a/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift b/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift index e86d8e1..787b920 100644 --- a/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift +++ b/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift @@ -235,4 +235,48 @@ final class SongProjectModelTests: XCTestCase { XCTAssertNil(project.vocalist(for: "harmony")) } + + func testLanguagesDialectsAndArabicPronunciationSettingsAreModeled() { + var project = SongProject( + id: "project-7", + title: "Multilingual Song", + idea: "Arabic and English hook" + ) + let arabic = LanguageSelection(id: "arabic", name: "Arabic") + let english = LanguageSelection(id: "english", name: "English") + let levantine = DialectSelection(id: "levantine", name: "Levantine", languageID: arabic.id) + let gulf = DialectSelection(id: "gulf", name: "Gulf", languageID: arabic.id) + + project.upsertLanguage(arabic) + project.upsertLanguage(english) + project.upsertLanguage(LanguageSelection(id: "english", name: "English")) + project.upsertDialect(levantine) + project.upsertDialect(gulf) + project.arabicPronunciationSettings = ArabicPronunciationSettings( + isEnabled: true, + diacritizationPolicy: .pronunciationTargeted, + tanweenPolicy: .preserveUserInput, + userNotes: "Preserve dialect phrasing", + preservedSpellings: ["ya habibi"] + ) + + XCTAssertEqual(project.languageIDs, ["arabic", "english"]) + XCTAssertEqual(project.dialectIDs, ["levantine", "gulf"]) + XCTAssertEqual(project.language(for: arabic.id), arabic) + XCTAssertEqual(project.dialects(forLanguageID: arabic.id), [levantine, gulf]) + XCTAssertTrue(project.arabicPronunciationSettings.isEnabled) + XCTAssertEqual(project.arabicPronunciationSettings.diacritizationPolicy, .pronunciationTargeted) + XCTAssertEqual(project.arabicPronunciationSettings.tanweenPolicy, .preserveUserInput) + + project.removeDialect(id: gulf.id) + + XCTAssertNil(project.dialect(for: gulf.id)) + XCTAssertEqual(project.dialects(forLanguageID: arabic.id), [levantine]) + + project.removeLanguage(id: arabic.id) + + XCTAssertNil(project.language(for: arabic.id)) + XCTAssertEqual(project.dialects(forLanguageID: arabic.id), []) + XCTAssertEqual(project.languageIDs, ["english"]) + } } diff --git a/docs/TASKS.md b/docs/TASKS.md index 6cd85bf..51d64f3 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -25,7 +25,7 @@ requirement is missing and blocks implementation, record it in - [x] Model song sections with order and optional timestamps. - [x] Model instruments, variants, roles and placements. - [x] Model multiple vocalists and section assignments. -- [ ] Model language/dialect and Arabic pronunciation settings. +- [x] Model language/dialect and Arabic pronunciation settings. - [ ] Model BPM, key, scale and maqam with Manual/Auto state. - [ ] Model emotional arc and production directions. - [ ] Add serialization tests.