Remove external music generation handoff

This commit is contained in:
diyaa
2026-09-15 12:16:08 +02:00
parent cd066d0a82
commit 1ebb5aadc3
11 changed files with 22 additions and 409 deletions
@@ -1,141 +0,0 @@
import MusicAssistantCore
import SwiftUI
import WebKit
struct SunoHandoffSheet: View {
let payload: SunoHandoffPayload
let onComplete: (SunoFieldFillReport) -> Void
@Environment(\.dismiss) private var dismiss
@State private var report: SunoFieldFillReport?
var body: some View {
VStack(spacing: 0) {
HStack {
Text("Send to Suno")
.font(.headline)
Spacer()
Button("Close") {
dismiss()
}
}
.padding(12)
Divider()
SunoCreateWebView(payload: payload) { report in
self.report = report
onComplete(report)
}
if let report {
HStack {
Label(statusText(for: report), systemImage: statusIcon(for: report))
.font(.caption)
.foregroundStyle(statusColor(for: report))
Spacer()
}
.padding(12)
}
}
.frame(minWidth: 900, minHeight: 650)
}
private func statusText(for report: SunoFieldFillReport) -> String {
report.results.values.contains { if case .copyRequired = $0 { return true }; return false }
? "Some fields need manual copying."
: "Suno fields are ready."
}
private func statusIcon(for report: SunoFieldFillReport) -> String {
report.results.values.contains { if case .copyRequired = $0 { return true }; return false }
? "exclamationmark.triangle.fill"
: "checkmark.circle.fill"
}
private func statusColor(for report: SunoFieldFillReport) -> Color {
report.results.values.contains { if case .copyRequired = $0 { return true }; return false }
? .orange
: .green
}
}
private struct SunoCreateWebView: NSViewRepresentable {
let payload: SunoHandoffPayload
let onReport: (SunoFieldFillReport) -> Void
func makeCoordinator() -> Coordinator {
Coordinator(payload: payload, onReport: onReport)
}
func makeNSView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = .default()
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.load(URLRequest(url: SunoHandoffDestination.createPageURL))
return webView
}
func updateNSView(_ webView: WKWebView, context: Context) {}
final class Coordinator: NSObject, WKNavigationDelegate {
let payload: SunoHandoffPayload
let onReport: (SunoFieldFillReport) -> Void
init(payload: SunoHandoffPayload, onReport: @escaping (SunoFieldFillReport) -> Void) {
self.payload = payload
self.onReport = onReport
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript(fillScript()) { value, _ in
guard let json = value as? String,
let data = json.data(using: .utf8),
let values = try? JSONSerialization.jsonObject(with: data) as? [String: Bool] else {
self.onReport(.copyRequired(for: SunoHandoffField.allCases, reason: "Suno fields could not be inspected."))
return
}
let results: [SunoHandoffField: SunoFieldFillResult] = Dictionary(
uniqueKeysWithValues: SunoHandoffField.allCases.map { field in
(field, values[field.rawValue] == true
? SunoFieldFillResult.filled
: SunoFieldFillResult.copyRequired("The \(field.displayName) field was not found."))
}
)
self.onReport(SunoFieldFillReport(results: results))
}
}
private func fillScript() -> String {
let payloadData = try! JSONEncoder().encode(payload)
let payloadJSON = String(data: payloadData, encoding: .utf8)!
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
return """
(() => {
const payload = JSON.parse('\(payloadJSON)');
const fields = { title: false, lyrics: false, stylePrompt: false };
const custom = [...document.querySelectorAll('button,[role="button"]')].find(e => /custom/i.test(e.innerText));
if (custom) custom.click();
const candidates = [...document.querySelectorAll('input,textarea,[contenteditable="true"]')];
const label = e => [e.getAttribute('aria-label'), e.getAttribute('placeholder'), e.name, e.id].filter(Boolean).join(' ');
const set = (terms, value, key) => {
const element = candidates.find(e => terms.some(term => label(e).toLowerCase().includes(term)));
if (!element || !value) return;
if (element.isContentEditable) element.textContent = value; else element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
fields[key] = true;
};
set(['title'], payload.title, 'title');
set(['lyric'], payload.lyrics, 'lyrics');
set(['style','prompt'], payload.stylePrompt, 'stylePrompt');
return JSON.stringify(fields);
})();
"""
}
}
}
@@ -1,4 +1,3 @@
import AppKit
import MusicAssistantCore
import SwiftUI
@@ -7,8 +6,6 @@ struct FinalReviewView: View {
@State private var newFieldKey = ""
@State private var newFieldValue = ""
@State private var handoffStatus: HandoffStatus?
@State private var sunoPayload: SunoHandoffPayload?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -34,7 +31,7 @@ struct FinalReviewView: View {
Divider()
HStack {
Text("Suno fields")
Text("Final song output")
.font(.headline)
Spacer()
@@ -46,31 +43,11 @@ struct FinalReviewView: View {
}
.disabled(project.sunoOutput != nil)
Button(action: sendToSuno) {
Label("Send to Suno", systemImage: "arrow.up.forward.app")
}
}
if let handoffStatus {
Label(handoffStatus.title, systemImage: handoffStatus.systemImage)
.font(.caption)
.foregroundStyle(handoffStatus.foregroundStyle)
}
if let fieldsToCopy, let output = project.sunoOutput {
HStack(spacing: 8) {
ForEach(fieldsToCopy) { field in
Button("Copy \(field.displayName)") {
copy(field, from: output)
}
.buttonStyle(.bordered)
}
}
}
TextField("Suno lyrics", text: sunoLyricsBinding, axis: .vertical)
TextField("Lyrics", text: sunoLyricsBinding, axis: .vertical)
.lineLimit(4...10)
TextField("Suno style prompt", text: sunoStylePromptBinding, axis: .vertical)
TextField("Style prompt", text: sunoStylePromptBinding, axis: .vertical)
.lineLimit(3...8)
if !additionalFieldKeys.isEmpty {
@@ -110,16 +87,6 @@ struct FinalReviewView: View {
.help("Add field")
}
}
.sheet(isPresented: Binding(
get: { sunoPayload != nil },
set: { if !$0 { sunoPayload = nil } }
)) {
if let sunoPayload {
SunoHandoffSheet(payload: sunoPayload) { report in
handoffStatus = .fieldFill(report)
}
}
}
}
private var additionalFieldKeys: [String] {
@@ -200,95 +167,6 @@ struct FinalReviewView: View {
)
}
private func sendToSuno() {
ensureSunoOutput()
guard let output = project.sunoOutput else { return }
do {
try CompiledSunoOutputValidator().validate(
CompiledSunoOutput(lyricsText: output.lyricsText, stylePrompt: output.stylePrompt)
)
} catch let validationError as LocalizedError {
handoffStatus = .failed(validationError.errorDescription ?? "Unable to prepare the Suno handoff.")
return
} catch {
handoffStatus = .failed("Unable to prepare the Suno handoff.")
return
}
sunoPayload = SunoHandoffPayload(
title: project.title,
lyrics: output.lyricsText,
stylePrompt: output.stylePrompt
)
}
private var fieldsToCopy: [SunoHandoffField]? {
guard case let .fieldFill(report) = handoffStatus else { return nil }
let fields = SunoHandoffField.allCases.filter {
if case .copyRequired = report.results[$0] { return true }
return false
}
return fields.isEmpty ? nil : fields
}
private func copy(_ field: SunoHandoffField, from output: SunoOutput) {
let value: String
switch field {
case .title:
value = project.title
case .lyrics:
value = output.lyricsText
case .stylePrompt:
value = output.stylePrompt
}
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(value, forType: .string)
}
}
private enum HandoffStatus: Equatable {
case fieldFill(SunoFieldFillReport)
case failed(String)
var title: String {
switch self {
case let .fieldFill(report):
let fieldsToCopy = report.results.compactMap { field, result in
if case .copyRequired = result { return field.displayName }
return nil
}
return fieldsToCopy.isEmpty
? "Suno fields filled in your browser."
: "Copy these fields into Suno: \(fieldsToCopy.joined(separator: ", "))."
case let .failed(message):
return message
}
}
var systemImage: String {
switch self {
case let .fieldFill(report):
return report.results.values.contains { if case .copyRequired = $0 { return true }; return false }
? "exclamationmark.triangle.fill"
: "checkmark.circle.fill"
case .failed:
return "exclamationmark.triangle.fill"
}
}
var foregroundStyle: Color {
switch self {
case let .fieldFill(report):
return report.results.values.contains { if case .copyRequired = $0 { return true }; return false }
? .orange
: .green
case .failed:
return .red
}
}
}
#Preview {
@@ -6,6 +6,6 @@ public struct ProductBoundarySummary: Equatable, Sendable {
}
public static let initial = ProductBoundarySummary(
description: "Create and refine song project concepts, then prepare lyrics and style instructions for Suno handoff without starting generation."
description: "Create and refine song project concepts, then prepare final lyrics and style instructions."
)
}
@@ -1,72 +0,0 @@
import Foundation
public protocol SunoHandoffService: Sendable {
func handOff(_ output: CompiledSunoOutput) async throws
}
public enum SunoHandoffDestination {
public static let createPageURL = URL(string: "https://suno.com/create")!
}
public struct SunoHandoffPayload: Codable, Equatable, Sendable {
public let title: String
public let lyrics: String
public let stylePrompt: String
public init(title: String, lyrics: String, stylePrompt: String) {
self.title = title
self.lyrics = lyrics
self.stylePrompt = stylePrompt
}
public func value(for field: SunoHandoffField) -> String {
switch field {
case .title:
return title
case .lyrics:
return lyrics
case .stylePrompt:
return stylePrompt
}
}
}
public enum SunoHandoffField: String, CaseIterable, Equatable, Identifiable, Sendable {
case title
case lyrics
case stylePrompt
public var id: String { rawValue }
public var displayName: String {
switch self {
case .title:
return "Title"
case .lyrics:
return "Lyrics"
case .stylePrompt:
return "Style prompt"
}
}
}
public enum SunoFieldFillResult: Equatable, Sendable {
case filled
case copyRequired(String)
}
public struct SunoFieldFillReport: Equatable, Sendable {
public let results: [SunoHandoffField: SunoFieldFillResult]
public init(results: [SunoHandoffField: SunoFieldFillResult]) {
self.results = results
}
public static func copyRequired(for fields: [SunoHandoffField], reason: String) -> SunoFieldFillReport {
SunoFieldFillReport(results: Dictionary(uniqueKeysWithValues: fields.map { ($0, .copyRequired(reason)) }))
}
}
public protocol SunoFieldFilling: Sendable {
func fill(_ payload: SunoHandoffPayload) async -> SunoFieldFillReport
}
@@ -10,7 +10,7 @@ public enum SunoOutputValidationError: Error, Equatable, LocalizedError, Sendabl
public var errorDescription: String? {
switch self {
case .missingContent:
return "Add lyrics or a style prompt before sending the song to Suno."
return "Add lyrics or a style prompt before finalizing the song output."
}
}
}