File size: 10,887 Bytes
5621d80
 
7155a3e
2eaa647
 
 
d3cdef9
5621d80
2eaa647
e1c2b50
2eaa647
 
 
d3cdef9
2eaa647
 
597c3bb
2eaa647
 
5621d80
2eaa647
 
 
 
 
 
 
5621d80
 
 
 
 
 
2eaa647
5621d80
 
2eaa647
 
5621d80
2eaa647
5621d80
 
 
 
2eaa647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5621d80
2eaa647
 
5621d80
d3cdef9
2eaa647
5621d80
2eaa647
 
5621d80
2eaa647
 
 
 
5621d80
2eaa647
 
5621d80
2eaa647
5621d80
 
 
 
 
 
2eaa647
5621d80
 
2eaa647
5621d80
 
 
2eaa647
 
5621d80
2eaa647
 
 
5621d80
2eaa647
5621d80
2eaa647
5621d80
2eaa647
 
5621d80
2eaa647
 
 
5621d80
2eaa647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5621d80
2eaa647
 
 
 
 
 
 
5621d80
2eaa647
e1c2b50
2eaa647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5621d80
 
2eaa647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5621d80
2eaa647
 
 
 
 
 
 
5621d80
2eaa647
 
 
 
 
 
 
 
 
 
5621d80
2eaa647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5621d80
 
2eaa647
 
 
 
5621d80
 
2eaa647
 
 
 
e1c2b50
2eaa647
597c3bb
2eaa647
 
 
 
 
 
 
 
 
 
 
 
 
 
5621d80
 
 
2eaa647
5621d80
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import gradio as gr
import requests
import os
import json
import uuid
from datetime import datetime

# Load Suno API key from environment variable
SUNO_KEY = os.environ.get("SUNO_KEY", os.environ.get("SUNOKEY", ""))

# In production, use your actual public URL
# For Spaces, get from environment or use ngrok for local testing
SPACE_URL = os.environ.get("SPACE_URL", "https://your-username.hf.space")

# Store tasks with timestamps
tasks_db = {}

def generate_lyrics(prompt, callBackUrl=""):
    """Submit lyrics generation task with callback URL"""
    if not SUNO_KEY:
        return "❌ Error: SUNO_KEY environment variable not set"
    
    # Generate unique task ID
    task_id = str(uuid.uuid4())[:8]
    
    # Use provided callback URL or default to your Space URL
    callback_url = callBackUrl or f"{SPACE_URL}/callback"
    
    url = "https://api.sunoapi.org/api/v1/lyrics"
    headers = {
        "Authorization": f"Bearer {SUNO_KEY}",
        "Content-Type": "application/json"
    }
    
    payload = {
        "prompt": prompt,
        "callBackUrl": callback_url,
        "customTaskId": task_id  # Optional: track your own ID
    }
    
    try:
        resp = requests.post(url, headers=headers, json=payload, timeout=30)
        data = resp.json()
        
        if resp.status_code == 200 and data.get("code") == 200:
            api_task_id = data["data"]["taskId"]
            
            # Store task info
            tasks_db[task_id] = {
                "api_task_id": api_task_id,
                "prompt": prompt,
                "status": "submitted",
                "submitted_at": datetime.now().isoformat(),
                "callback_received": False,
                "lyrics": None
            }
            
            return f"""βœ… Task Submitted!
Your Task ID: {task_id}
API Task ID: {api_task_id}
Callback URL: {callback_url}

πŸ“ Prompt: {prompt}

⏳ Processing... The results will be sent to the callback URL.
You can also poll manually using your Task ID above."""
        
        else:
            error_msg = data.get("msg", "Unknown error")
            return f"❌ API Error: {error_msg} (Code: {data.get('code')})"
            
    except Exception as e:
        return f"❌ Error: {str(e)}"

def poll_task(task_id):
    """Manual polling as fallback"""
    if not SUNO_KEY:
        return "❌ Error: SUNO_KEY not configured"
    
    if task_id not in tasks_db:
        return "❌ Task ID not found. Please submit a task first."
    
    task_info = tasks_db[task_id]
    api_task_id = task_info["api_task_id"]
    
    url = f"https://api.sunoapi.org/api/v1/lyrics/details?taskId={api_task_id}"
    headers = {"Authorization": f"Bearer {SUNO_KEY}"}
    
    try:
        resp = requests.get(url, headers=headers, timeout=30)
        data = resp.json()
        
        if resp.status_code == 200 and data.get("code") == 200:
            task_data = data["data"]
            status = task_data.get("status", "unknown")
            tasks_db[task_id]["status"] = status
            
            if status == "completed" and "data" in task_data:
                lyrics_data = task_data["data"]
                tasks_db[task_id]["lyrics"] = lyrics_data
                tasks_db[task_id]["callback_received"] = True
                
                return format_lyrics(lyrics_data, task_id)
            elif status == "failed":
                return f"❌ Task failed: {task_data.get('error', 'Unknown error')}"
            else:
                return f"⏳ Status: {status}\nLast checked: {datetime.now().strftime('%H:%M:%S')}"
        else:
            return f"❌ Polling error: {data.get('msg', 'Unknown error')}"
            
    except Exception as e:
        return f"❌ Error: {str(e)}"

def format_lyrics(lyrics_data, task_id):
    """Format lyrics for display"""
    output = [f"🎡 **Task {task_id} - Generated Lyrics**", ""]
    
    for i, item in enumerate(lyrics_data, 1):
        title = item.get('title', f'Variant {i}')
        text = item.get('text', 'No lyrics generated')
        
        output.append(f"**Variant {i}: {title}**")
        output.append("```")
        output.append(text)
        output.append("```")
        output.append("---")
    
    output.append(f"\nβœ… Generated at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    return "\n".join(output)

def list_tasks():
    """Show all submitted tasks"""
    if not tasks_db:
        return "No tasks submitted yet."
    
    output = ["πŸ“‹ **Submitted Tasks:**", ""]
    for task_id, info in tasks_db.items():
        status_icon = "βœ…" if info["callback_received"] else "⏳"
        output.append(f"{status_icon} **{task_id}** - {info['status']}")
        output.append(f"   Prompt: {info['prompt'][:50]}...")
        output.append(f"   Submitted: {info['submitted_at']}")
        output.append("")
    
    return "\n".join(output)

# WEBHOOK ENDPOINT (for receiving callbacks)
def webhook_callback(request: gr.Request):
    """Handle incoming webhook from Suno API"""
    try:
        # Try to get JSON data
        data = request.json()
        
        if not data:
            # Try form data
            data = dict(request.form)
        
        print(f"πŸ“₯ Received webhook: {json.dumps(data, indent=2)}")
        
        # Extract task info from webhook
        # Suno API format might vary - adjust based on actual response
        if "data" in data and "taskId" in data["data"]:
            api_task_id = data["data"]["taskId"]
            status = data["data"].get("status", "unknown")
            
            # Find our task by API task ID
            for task_id, task_info in tasks_db.items():
                if task_info["api_task_id"] == api_task_id:
                    task_info["status"] = status
                    task_info["callback_received"] = True
                    
                    if status == "completed" and "data" in data["data"]:
                        task_info["lyrics"] = data["data"]["data"]
                        print(f"βœ… Lyrics received for task {task_id}")
                    
                    return {"status": "success", "message": f"Updated task {task_id}"}
        
        return {"status": "error", "message": "Task not found"}
        
    except Exception as e:
        print(f"❌ Webhook error: {e}")
        return {"status": "error", "message": str(e)}

# Gradio Interface
with gr.Blocks(theme=gr.themes.Soft(), title="Suno Lyrics Generator") as app:
    gr.Markdown("# 🎡 Suno AI Lyrics Generator")
    gr.Markdown("Generate song lyrics with webhook support")
    
    with gr.Tabs():
        with gr.TabItem("🎀 Generate"):
            with gr.Row():
                with gr.Column():
                    prompt = gr.Textbox(
                        label="Lyrics Prompt",
                        placeholder="A romantic ballad about stargazing...",
                        lines=3
                    )
                    
                    # Optional: Custom callback URL
                    callback_url = gr.Textbox(
                        label="Callback URL (Optional)",
                        value=f"{SPACE_URL}/callback",
                        info="Where Suno should send results. Leave as default for Spaces."
                    )
                    
                    submit_btn = gr.Button("✨ Generate Lyrics", variant="primary")
                    
                    gr.Markdown("### ℹ️ Instructions:")
                    gr.Markdown("""
                    1. Enter your lyrics prompt
                    2. Click Generate
                    3. Save your Task ID
                    4. Check status in the "Poll Tasks" tab
                    5. Results will arrive via webhook automatically
                    """)
                
                with gr.Column():
                    output = gr.Textbox(
                        label="Submission Result",
                        lines=10,
                        interactive=False
                    )
            
            submit_btn.click(
                generate_lyrics,
                inputs=[prompt, callback_url],
                outputs=output
            )
        
        with gr.TabItem("πŸ”„ Poll Tasks"):
            with gr.Row():
                with gr.Column():
                    task_id_input = gr.Textbox(
                        label="Your Task ID",
                        placeholder="Enter the Task ID from generation step"
                    )
                    poll_btn = gr.Button("πŸ” Check Status", variant="primary")
                    
                    gr.Markdown("---")
                    refresh_btn = gr.Button("πŸ“‹ List All Tasks")
                    tasks_list = gr.Textbox(label="All Tasks", lines=10)
                
                with gr.Column():
                    poll_result = gr.Textbox(
                        label="Task Status",
                        lines=15,
                        interactive=False
                    )
            
            poll_btn.click(
                poll_task,
                inputs=task_id_input,
                outputs=poll_result
            )
            
            refresh_btn.click(
                list_tasks,
                inputs=None,
                outputs=tasks_list
            )
        
        with gr.TabItem("βš™οΈ Webhook Info"):
            gr.Markdown("### 🌐 Webhook Configuration")
            gr.Markdown(f"""
            **Your Webhook URL:** `{SPACE_URL}/callback`
            
            **For Local Development:**
            1. Use [ngrok](https://ngrok.com/): `ngrok http 7860`
            2. Update callback URL: `https://your-ngrok-url.ngrok.io/callback`
            
            **For Hugging Face Spaces:**
            - The URL above should work automatically
            - Make sure your Space is public or has network access
            """)
            
            webhook_status = gr.Textbox(
                label="Last Webhook Status",
                value="No webhooks received yet",
                lines=5
            )
    
    # Register webhook endpoint
    # Note: In production, you'd set up proper route handling
    # For Gradio, we can simulate with a POST endpoint
    app.post("/callback")(webhook_callback)

# Launch configuration
if __name__ == "__main__":
    # For local testing with webhooks
    import socket
    
    # Get local IP for ngrok compatibility
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        local_ip = s.getsockname()[0]
        s.close()
        print(f"🌐 Local IP: {local_ip}")
        print(f"🌐 Webhook URL: http://{local_ip}:7860/callback")
    except:
        pass
    
    app.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False,
        show_error=True
    )