Add Arabic pronunciation processing

This commit is contained in:
diyaa
2026-09-13 21:44:42 +02:00
parent 49a03a0700
commit f5ab97e6a0
4 changed files with 182 additions and 1 deletions
@@ -0,0 +1,104 @@
import Foundation
public struct ArabicPronunciationProcessor: Sendable {
public init() {}
public func process(
lyrics: String,
settings: ArabicPronunciationSettings
) -> ArabicPronunciationProcessingResult {
guard settings.isEnabled else {
return ArabicPronunciationProcessingResult(
text: lyrics,
notes: [.processingDisabled]
)
}
var processedText = lyrics.precomposedStringWithCanonicalMapping
var notes = processingNotes(for: processedText, settings: settings)
if settings.tanweenPolicy == .removeWhenUnwanted {
processedText = removeTanween(from: processedText)
}
if settings.tanweenPolicy == .addWhenPronunciationRequires,
containsArabicLetter(in: processedText),
!containsTanween(in: processedText) {
notes.append(.tanweenAdditionNeedsReview)
}
return ArabicPronunciationProcessingResult(
text: processedText,
notes: notes
)
}
private func processingNotes(
for text: String,
settings: ArabicPronunciationSettings
) -> [ArabicPronunciationProcessingNote] {
guard containsArabicLetter(in: text) else {
return []
}
switch settings.diacritizationPolicy {
case .unspecified:
return [.diacritizationPolicyUnspecified]
case .pronunciationTargeted where !containsDiacritics(in: text):
return [.pronunciationTargetedDiacriticsNeedReview]
case .fullTashkeel where !containsDiacritics(in: text):
return [.fullTashkeelNeedsReview]
default:
return []
}
}
private func removeTanween(from text: String) -> String {
String(text.unicodeScalars.filter { scalar in
!isTanween(scalar)
})
}
private func containsArabicLetter(in text: String) -> Bool {
text.unicodeScalars.contains { scalar in
let isArabicBlock = (0x0600...0x06FF).contains(scalar.value)
|| (0x0750...0x077F).contains(scalar.value)
|| (0x08A0...0x08FF).contains(scalar.value)
return isArabicBlock && CharacterSet.letters.contains(scalar)
}
}
private func containsDiacritics(in text: String) -> Bool {
text.unicodeScalars.contains(where: isArabicDiacritic)
}
private func containsTanween(in text: String) -> Bool {
text.unicodeScalars.contains(where: isTanween)
}
private func isArabicDiacritic(_ scalar: Unicode.Scalar) -> Bool {
(0x064B...0x065F).contains(scalar.value) || scalar.value == 0x0670
}
private func isTanween(_ scalar: Unicode.Scalar) -> Bool {
(0x064B...0x064D).contains(scalar.value)
}
}
public struct ArabicPronunciationProcessingResult: Equatable, Sendable {
public var text: String
public var notes: [ArabicPronunciationProcessingNote]
public init(text: String, notes: [ArabicPronunciationProcessingNote] = []) {
self.text = text
self.notes = notes
}
}
public enum ArabicPronunciationProcessingNote: Equatable, Sendable {
case processingDisabled
case diacritizationPolicyUnspecified
case pronunciationTargetedDiacriticsNeedReview
case fullTashkeelNeedsReview
case tanweenAdditionNeedsReview
}
@@ -0,0 +1,67 @@
import MusicAssistantCore
import XCTest
final class ArabicPronunciationProcessorTests: XCTestCase {
private let processor = ArabicPronunciationProcessor()
func testDisabledProcessingLeavesLyricsUntouched() {
let lyrics = "\u{0645}\u{0651}\u{064E}\u{0631}\u{062D}\u{064E}\u{0628}\u{064B}\u{0627}"
let result = processor.process(
lyrics: lyrics,
settings: ArabicPronunciationSettings(isEnabled: false)
)
XCTAssertEqual(result.text, lyrics)
XCTAssertEqual(result.notes, [.processingDisabled])
}
func testProcessingNormalizesSuppliedDiacritics() {
let lyrics = "\u{0645}\u{0651}\u{064E}\u{0631}\u{062D}\u{064E}\u{0628}\u{064B}\u{0627}"
let result = processor.process(
lyrics: lyrics,
settings: ArabicPronunciationSettings(
isEnabled: true,
diacritizationPolicy: .pronunciationTargeted
)
)
XCTAssertEqual(
result.text,
"\u{0645}\u{064E}\u{0651}\u{0631}\u{062D}\u{064E}\u{0628}\u{064B}\u{0627}"
)
XCTAssertTrue(result.notes.isEmpty)
}
func testRemovingTanweenKeepsOtherDiacritics() {
let result = processor.process(
lyrics: "\u{0634}\u{064F}\u{0643}\u{0652}\u{0631}\u{064B}\u{0627}",
settings: ArabicPronunciationSettings(
isEnabled: true,
tanweenPolicy: .removeWhenUnwanted
)
)
XCTAssertEqual(result.text, "\u{0634}\u{064F}\u{0643}\u{0652}\u{0631}\u{0627}")
}
func testFullTashkeelDoesNotInventMissingMarks() {
let lyrics = "\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}"
let result = processor.process(
lyrics: lyrics,
settings: ArabicPronunciationSettings(
isEnabled: true,
diacritizationPolicy: .fullTashkeel,
tanweenPolicy: .addWhenPronunciationRequires
)
)
XCTAssertEqual(result.text, lyrics)
XCTAssertEqual(
result.notes,
[.fullTashkeelNeedsReview, .tanweenAdditionNeedsReview]
)
}
}
+10
View File
@@ -58,6 +58,16 @@ retry delay when available, and propagates cancellation without retrying.
A deterministic layer converts the approved SongProject into the final A deterministic layer converts the approved SongProject into the final
Suno-facing lyrics/style content. User choices override AI suggestions. Suno-facing lyrics/style content. User choices override AI suggestions.
## Arabic Pronunciation Processor
The Arabic Pronunciation Processor is a local Application Service. It
normalizes user-supplied Arabic diacritics and applies the selected
tanween policy without choosing a diacritization policy for the user.
It must not invent missing vowel marks or tanween when no reliable
linguistic source is available; instead, it returns a review note so a
later user-review flow can present the unresolved text. This processor
does not use external services or APIs.
## Security ## Security
- Never commit API keys to source control. - Never commit API keys to source control.
+1 -1
View File
@@ -68,7 +68,7 @@ requirement is missing and blocks implementation, record it in
## Phase 5 --- Arabic Lyrics Processing ## Phase 5 --- Arabic Lyrics Processing
- [x] Add Arabic-specific settings UI. - [x] Add Arabic-specific settings UI.
- [ ] Support diacritics/harakat/tanween processing. - [x] Support diacritics/harakat/tanween processing.
- [ ] Preserve intentional spelling/dialect choices where possible. - [ ] Preserve intentional spelling/dialect choices where possible.
- [ ] Allow user to compare/edit processed Arabic before Suno handoff. - [ ] Allow user to compare/edit processed Arabic before Suno handoff.
- [ ] Add Arabic test fixtures covering multiple dialects. - [ ] Add Arabic test fixtures covering multiple dialects.