Add Arabic lyrics review
This commit is contained in:
@@ -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 {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
ArabicLyricsReviewView(project: $project)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
TextField("Approved lyrics", text: $project.lyrics.text, axis: .vertical)
|
TextField("Approved lyrics", text: $project.lyrics.text, axis: .vertical)
|
||||||
.lineLimit(4...10)
|
.lineLimit(4...10)
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,15 @@ public extension SongProject {
|
|||||||
dialects.removeAll { $0.id == dialectID }
|
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) {
|
mutating func setBPMManually(_ value: Int) {
|
||||||
bpm = ManualAutoValue(mode: .manual, value: value)
|
bpm = ManualAutoValue(mode: .manual, value: value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,6 +369,36 @@ final class SongProjectModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(project.languageIDs, ["english"])
|
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() {
|
func testMusicalParametersSupportManualAndAutoModes() {
|
||||||
var project = SongProject(
|
var project = SongProject(
|
||||||
id: "project-8",
|
id: "project-8",
|
||||||
|
|||||||
@@ -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
|
later user-review flow can present the unresolved text. This processor
|
||||||
does not use external services or APIs. Before processing, it protects
|
does not use external services or APIs. Before processing, it protects
|
||||||
the user's exact preserved spellings and dialect phrases so normalization
|
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
|
## Security
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ requirement is missing and blocks implementation, record it in
|
|||||||
- [x] Add Arabic-specific settings UI.
|
- [x] Add Arabic-specific settings UI.
|
||||||
- [x] Support diacritics/harakat/tanween processing.
|
- [x] Support diacritics/harakat/tanween processing.
|
||||||
- [x] Preserve intentional spelling/dialect choices where possible.
|
- [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.
|
- [ ] Add Arabic test fixtures covering multiple dialects.
|
||||||
|
|
||||||
## Phase 6 --- Instrument Selector & Catalog
|
## Phase 6 --- Instrument Selector & Catalog
|
||||||
|
|||||||
Reference in New Issue
Block a user