Add Arabic lyrics review

This commit is contained in:
diyaa
2026-09-13 22:42:50 +02:00
parent 3865f77dc0
commit 6a99266e75
6 changed files with 155 additions and 2 deletions
@@ -0,0 +1,108 @@
import MusicAssistantCore
import SwiftUI
struct ArabicLyricsReviewView: View {
@Binding var project: SongProject
private let processor = ArabicPronunciationProcessor()
@State private var processedLyrics = ""
@State private var processingNotes: [ArabicPronunciationProcessingNote] = []
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Arabic lyrics review")
.font(.headline)
Spacer()
Button(action: refreshProcessedLyrics) {
Label("Refresh processed lyrics", systemImage: "arrow.clockwise")
.labelStyle(.iconOnly)
}
.buttonStyle(.borderless)
.help("Refresh processed lyrics")
}
Text("Original lyrics")
.font(.subheadline)
.foregroundStyle(.secondary)
TextEditor(text: .constant(project.lyrics.text))
.font(.body)
.frame(minHeight: 96)
.disabled(true)
Text("Processed lyrics")
.font(.subheadline)
.foregroundStyle(.secondary)
TextEditor(text: $processedLyrics)
.font(.body)
.frame(minHeight: 96)
ForEach(Array(processingNotes.enumerated()), id: \.offset) { entry in
Label(entry.element.title, systemImage: entry.element.systemImage)
.font(.caption)
.foregroundStyle(.secondary)
}
Button(action: applyProcessedLyrics) {
Label("Use processed lyrics", systemImage: "checkmark.circle")
}
.disabled(!canApplyProcessedLyrics)
}
.onAppear(perform: refreshProcessedLyrics)
}
private var canApplyProcessedLyrics: Bool {
let trimmedLyrics = processedLyrics.trimmingCharacters(in: .whitespacesAndNewlines)
return !trimmedLyrics.isEmpty && processedLyrics != project.lyrics.text
}
private func refreshProcessedLyrics() {
let result = processor.process(
lyrics: project.lyrics.text,
settings: project.arabicPronunciationSettings
)
processedLyrics = result.text
processingNotes = result.notes
}
private func applyProcessedLyrics() {
project.applyReviewedArabicLyrics(processedLyrics)
}
}
private extension ArabicPronunciationProcessingNote {
var title: String {
switch self {
case .processingDisabled:
return "Arabic processing is disabled"
case .diacritizationPolicyUnspecified:
return "Select a diacritics policy to continue"
case .pronunciationTargetedDiacriticsNeedReview:
return "Targeted diacritics need review"
case .fullTashkeelNeedsReview:
return "Full tashkeel needs review"
case .tanweenAdditionNeedsReview:
return "Tanween addition needs review"
case let .preservedSpellingsProtected(count):
return "Protected \(count) preserved spelling\(count == 1 ? "" : "s")"
}
}
var systemImage: String {
switch self {
case .processingDisabled, .diacritizationPolicyUnspecified:
return "info.circle"
case .pronunciationTargetedDiacriticsNeedReview,
.fullTashkeelNeedsReview,
.tanweenAdditionNeedsReview:
return "exclamationmark.triangle"
case .preservedSpellingsProtected:
return "checkmark.shield"
}
}
}
@@ -9,6 +9,10 @@ struct FinalReviewView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
ArabicLyricsReviewView(project: $project)
Divider()
TextField("Approved lyrics", text: $project.lyrics.text, axis: .vertical)
.lineLimit(4...10)
@@ -199,6 +199,15 @@ public extension SongProject {
dialects.removeAll { $0.id == dialectID }
}
mutating func applyReviewedArabicLyrics(_ reviewedLyrics: String) {
let originalLyrics = lyrics.text
lyrics.text = reviewedLyrics
if sunoOutput?.lyricsText == originalLyrics {
sunoOutput?.lyricsText = reviewedLyrics
}
}
mutating func setBPMManually(_ value: Int) {
bpm = ManualAutoValue(mode: .manual, value: value)
}
@@ -369,6 +369,36 @@ final class SongProjectModelTests: XCTestCase {
XCTAssertEqual(project.languageIDs, ["english"])
}
func testApplyingReviewedArabicLyricsUpdatesMatchingSunoLyrics() {
var project = SongProject(
id: "project-arabic-review",
title: "Arabic Review",
idea: "Review pronunciation",
lyrics: Lyrics(text: "Original lyrics"),
sunoOutput: SunoOutput(lyricsText: "Original lyrics", stylePrompt: "Arabic pop")
)
project.applyReviewedArabicLyrics("Reviewed lyrics")
XCTAssertEqual(project.lyrics.text, "Reviewed lyrics")
XCTAssertEqual(project.sunoOutput?.lyricsText, "Reviewed lyrics")
}
func testApplyingReviewedArabicLyricsPreservesEditedSunoLyrics() {
var project = SongProject(
id: "project-arabic-review-manual-output",
title: "Arabic Review",
idea: "Review pronunciation",
lyrics: Lyrics(text: "Original lyrics"),
sunoOutput: SunoOutput(lyricsText: "Manually edited output", stylePrompt: "Arabic pop")
)
project.applyReviewedArabicLyrics("Reviewed lyrics")
XCTAssertEqual(project.lyrics.text, "Reviewed lyrics")
XCTAssertEqual(project.sunoOutput?.lyricsText, "Manually edited output")
}
func testMusicalParametersSupportManualAndAutoModes() {
var project = SongProject(
id: "project-8",
+3 -1
View File
@@ -68,7 +68,9 @@ 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. Before processing, it protects
the user's exact preserved spellings and dialect phrases so normalization
or tanween removal cannot alter them.
or tanween removal cannot alter them. The Presentation layer presents
both the original and processed lyrics in final review; the user may edit
and explicitly apply the processed text before any Suno handoff.
## Security
+1 -1
View File
@@ -70,7 +70,7 @@ requirement is missing and blocks implementation, record it in
- [x] Add Arabic-specific settings UI.
- [x] Support diacritics/harakat/tanween processing.
- [x] Preserve intentional spelling/dialect choices where possible.
- [ ] Allow user to compare/edit processed Arabic before Suno handoff.
- [x] Allow user to compare/edit processed Arabic before Suno handoff.
- [ ] Add Arabic test fixtures covering multiple dialects.
## Phase 6 --- Instrument Selector & Catalog