|
|
import datetime |
|
|
|
|
|
import gradio as gr |
|
|
import json |
|
|
import requests |
|
|
import time |
|
|
|
|
|
|
|
|
def create_task(prompt: str, style: str, title: str, api_key: str): |
|
|
headers = { |
|
|
"Content-Type": "application/json", |
|
|
"Authorization": f"Bearer {api_key}" |
|
|
} |
|
|
url = "https://api.kie.ai/api/v1/generate" |
|
|
payload = { |
|
|
"prompt": prompt, |
|
|
"style": style, |
|
|
"title": title, |
|
|
"customMode": False, |
|
|
"instrumental": True, |
|
|
"model": "V3_5", |
|
|
"callBackUrl": "https://your-app.com/callback" |
|
|
} |
|
|
try: |
|
|
response = requests.post(url, headers=headers, data=json.dumps(payload), timeout=60) |
|
|
result = response.json() |
|
|
|
|
|
if response.ok and result.get('code') == 200: |
|
|
print(f"任务ID: {result['data']['taskId']}") |
|
|
return True, result['data']['taskId'] |
|
|
else: |
|
|
print(f"请求失败: {result.get('msg', '未知错误')}") |
|
|
return False, result.get('msg', "Internal Server Error") |
|
|
except Exception: |
|
|
pass |
|
|
return False, "Internal Server Error" |
|
|
|
|
|
|
|
|
def check_task_status(task_id, api_key): |
|
|
url = f"https://api.kie.ai/api/v1/generate/record-info?taskId={task_id}" |
|
|
headers = {"Authorization": f"Bearer {api_key}"} |
|
|
for _ in range(3): |
|
|
try: |
|
|
response = requests.get(url, headers=headers) |
|
|
result = response.json() |
|
|
|
|
|
if response.ok and result.get('code') == 200: |
|
|
task_data = result['data'] |
|
|
status = task_data['status'] |
|
|
|
|
|
response_data = task_data['response'] |
|
|
|
|
|
if status == 'SUCCESS': |
|
|
return "SUCCESS", response_data["sunoData"] |
|
|
elif status == 'FIRST_SUCCESS': |
|
|
return "FIRST_SUCCESS", response_data["sunoData"] |
|
|
elif status == 'TEXT_SUCCESS': |
|
|
return "TEXT_SUCCESS", task_data |
|
|
elif status == 'PENDING': |
|
|
return "PENDING", None |
|
|
else: |
|
|
return "FAILED", response_data.get('errorMessage', 'Internal Server Error') |
|
|
else: |
|
|
return "FAILED", result.get('msg', 'Internal Server Error') |
|
|
except Exception: |
|
|
pass |
|
|
time.sleep(5) |
|
|
return "FAILED", 'Internal Server Error' |
|
|
|
|
|
|
|
|
def format_duration(seconds): |
|
|
"""格式化时长显示""" |
|
|
minutes = int(seconds // 60) |
|
|
seconds = int(seconds % 60) |
|
|
return f"{minutes:02d}:{seconds:02d}" |
|
|
|
|
|
|
|
|
def create_music_card(music_data, index): |
|
|
"""创建音乐卡片的HTML""" |
|
|
if music_data is None: |
|
|
return f""" |
|
|
<div class="music-card empty-card"> |
|
|
<div class="card-content"> |
|
|
<div class="empty-icon">🎵</div> |
|
|
<div class="empty-text">Waiting for music #{index} to generate...</div> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
duration = format_duration(music_data.get('duration', 0)) |
|
|
|
|
|
|
|
|
create_time = music_data.get('createTime', '') |
|
|
if create_time: |
|
|
|
|
|
if create_time > 1e12: |
|
|
create_time = create_time / 1000 |
|
|
|
|
|
|
|
|
dt = datetime.datetime.fromtimestamp(create_time) |
|
|
|
|
|
formatted_time = dt.strftime("%Y-%m-%d %H:%M:%S") |
|
|
else: |
|
|
formatted_time = "" |
|
|
|
|
|
print(formatted_time) |
|
|
return f""" |
|
|
<div class="music-card"> |
|
|
<div class="card-header"> |
|
|
<img src="{music_data.get('imageUrl', '')}" alt="Album cover" class="album-cover"> |
|
|
<div class="track-number">#{index}</div> |
|
|
</div> |
|
|
<div class="card-content"> |
|
|
<h3 class="music-title">{music_data.get('title', 'Untitled')}</h3> |
|
|
<div class="music-tags"> |
|
|
<span class="tag-icon">🏷️</span> |
|
|
<span class="tag-text">{music_data.get('tags', 'No tags')}</span> |
|
|
</div> |
|
|
<div class="music-duration"> |
|
|
<span class="duration-icon">⏱️</span> |
|
|
<span class="duration-text">{duration}</span> |
|
|
</div> |
|
|
<audio controls class="audio-player"> |
|
|
<source src="{music_data.get('audioUrl', '')}" type="audio/mpeg"> |
|
|
Your browser does not support audio playback |
|
|
</audio> |
|
|
<div class="create-time">Generated: {formatted_time}</div> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
def process_music_generation(prompt, style, title, api_key, progress=gr.Progress()): |
|
|
yield ( |
|
|
create_music_card(None, 1), |
|
|
create_music_card(None, 2), |
|
|
f"🚀 Creating music generation task..." |
|
|
) |
|
|
"""处理音乐生成的主函数""" |
|
|
if not api_key or not api_key.strip(): |
|
|
yield (gr.update(), |
|
|
gr.update(), |
|
|
f"❌ Please enter API Key") |
|
|
return "❌ Please enter API Key" |
|
|
|
|
|
if not prompt or not prompt.strip(): |
|
|
yield (gr.update(), |
|
|
gr.update(), |
|
|
f"❌ Please enter music description") |
|
|
return "❌ Please enter music description" |
|
|
|
|
|
try: |
|
|
|
|
|
success, task_id = create_task(prompt.strip(), style.strip(), title.strip(), api_key.strip()) |
|
|
|
|
|
if not success: |
|
|
yield (gr.update(), |
|
|
gr.update(), |
|
|
f"❌ Failed to create task: {task_id}") |
|
|
return f"❌ Failed to create task" |
|
|
|
|
|
yield (gr.update(), |
|
|
gr.update(), |
|
|
f"📋 Starting music generation") |
|
|
|
|
|
|
|
|
start_time = time.time() |
|
|
first_music_shown = False |
|
|
music_data_1 = None |
|
|
music_data_2 = None |
|
|
last_api_check = 0 |
|
|
api_check_interval = 8 |
|
|
|
|
|
while time.time() - start_time < 600: |
|
|
elapsed = int(time.time() - start_time) |
|
|
progress_value = min(0.2 + (elapsed / 600) * 0.7, 0.9) |
|
|
|
|
|
|
|
|
if time.time() - last_api_check >= api_check_interval: |
|
|
status, data = check_task_status(task_id, api_key.strip()) |
|
|
last_api_check = time.time() |
|
|
|
|
|
if status == 'FIRST_SUCCESS' and not first_music_shown: |
|
|
|
|
|
progress(progress_value, desc="🎵 First music generated!") |
|
|
if data and len(data) > 0: |
|
|
music_data_1 = data[0] |
|
|
first_music_shown = True |
|
|
|
|
|
yield ( |
|
|
create_music_card(music_data_1, 1), |
|
|
create_music_card(None, 2), |
|
|
f"✅ First music generated, generating second..." |
|
|
) |
|
|
continue |
|
|
|
|
|
elif status == 'SUCCESS': |
|
|
|
|
|
progress(0.95, desc="🎉 All music generated!") |
|
|
if data and len(data) > 0: |
|
|
music_data_1 = data[0] if len(data) > 0 else music_data_1 |
|
|
music_data_2 = data[1] if len(data) > 1 else None |
|
|
|
|
|
yield ( |
|
|
create_music_card(music_data_1, 1), |
|
|
create_music_card(music_data_2, 2) if music_data_2 else create_music_card(None, 2), |
|
|
f"✅ Successfully generated {len(data)} music tracks!" |
|
|
) |
|
|
break |
|
|
|
|
|
elif status == 'FAILED': |
|
|
yield ( |
|
|
gr.update(), |
|
|
gr.update(), |
|
|
f"❌ Generation failed: {data}" |
|
|
) |
|
|
break |
|
|
|
|
|
if not first_music_shown: |
|
|
yield ( |
|
|
gr.update(), |
|
|
gr.update(), |
|
|
f"⏳ Generating music... (waited {elapsed}s)" |
|
|
) |
|
|
elif first_music_shown: |
|
|
|
|
|
yield ( |
|
|
gr.update(), |
|
|
gr.update(), |
|
|
f"✅ First music generated, generating second... (waited {elapsed}s)" |
|
|
) |
|
|
|
|
|
time.sleep(1) |
|
|
|
|
|
|
|
|
if time.time() - start_time >= 600: |
|
|
yield ( |
|
|
gr.update(), |
|
|
gr.update(), |
|
|
"⚠️ Generation timeout, please try again later" |
|
|
) |
|
|
|
|
|
except Exception as e: |
|
|
yield ( |
|
|
gr.update(), |
|
|
gr.update(), |
|
|
f"❌ Error occurred: {str(e)}" |
|
|
) |
|
|
return "❌ Error occurred: {str(e)}" |
|
|
|
|
|
|
|
|
|
|
|
css = """ |
|
|
.gradio-container { |
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
|
|
min-height: 100vh; |
|
|
} |
|
|
.header-container { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
padding: 2.5rem; |
|
|
border-radius: 24px; |
|
|
margin-bottom: 2.5rem; |
|
|
box-shadow: 0 20px 60px rgba(102, 126, 234, 0.25); |
|
|
} |
|
|
.logo-text { |
|
|
font-size: 3.5rem; |
|
|
font-weight: 900; |
|
|
color: white; |
|
|
text-align: center; |
|
|
margin: 0; |
|
|
letter-spacing: -2px; |
|
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
.subtitle { |
|
|
color: rgba(255, 255, 255, 0.9); |
|
|
text-align: center; |
|
|
font-size: 1.2rem; |
|
|
margin-top: 0.5rem; |
|
|
} |
|
|
.main-content { |
|
|
background: rgba(255, 255, 255, 0.95); |
|
|
backdrop-filter: blur(20px); |
|
|
border-radius: 24px; |
|
|
padding: 2.5rem; |
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08); |
|
|
} |
|
|
.mode-indicator { |
|
|
background: rgba(255, 255, 255, 0.3); |
|
|
backdrop-filter: blur(10px); |
|
|
border-radius: 12px; |
|
|
padding: 0.5rem 1rem; |
|
|
margin-top: 1rem; |
|
|
text-align: center; |
|
|
font-weight: 600; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.info-box { |
|
|
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); |
|
|
border-radius: 12px; |
|
|
padding: 1.5rem; |
|
|
margin-bottom: 1.5rem; |
|
|
border-left: 4px solid #2196f3; |
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
.info-box strong { |
|
|
color: #1976d2; |
|
|
font-size: 1.1rem; |
|
|
} |
|
|
|
|
|
.gr-button-primary { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
|
|
border: none !important; |
|
|
color: white !important; |
|
|
font-weight: 700 !important; |
|
|
font-size: 1.1rem !important; |
|
|
padding: 1.2rem 2rem !important; |
|
|
border-radius: 14px !important; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 1px; |
|
|
width: 100%; |
|
|
margin-top: 1rem !important; |
|
|
transition: all 0.3s ease !important; |
|
|
} |
|
|
|
|
|
.gr-button-primary:hover { |
|
|
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%) !important; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15) !important; |
|
|
} |
|
|
|
|
|
/* 音乐卡片样式 */ |
|
|
.music-cards-container { |
|
|
display: flex; |
|
|
gap: 1.5rem; |
|
|
margin-top: 1rem; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.music-card { |
|
|
background: white; |
|
|
border-radius: 16px; |
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); |
|
|
overflow: hidden; |
|
|
transition: all 0.3s ease; |
|
|
flex: 1; |
|
|
min-width: 300px; |
|
|
animation: slideIn 0.5s ease-out; |
|
|
} |
|
|
|
|
|
@keyframes slideIn { |
|
|
from { |
|
|
opacity: 0; |
|
|
transform: translateY(20px); |
|
|
} |
|
|
to { |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
} |
|
|
} |
|
|
|
|
|
.music-card:hover { |
|
|
transform: translateY(-5px); |
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15); |
|
|
} |
|
|
|
|
|
.empty-card { |
|
|
border: 2px dashed #e0e0e0; |
|
|
background: #fafafa; |
|
|
min-height: 400px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.empty-icon { |
|
|
font-size: 3rem; |
|
|
margin-bottom: 1rem; |
|
|
opacity: 0.3; |
|
|
} |
|
|
|
|
|
.empty-text { |
|
|
color: #9e9e9e; |
|
|
font-size: 1rem; |
|
|
} |
|
|
|
|
|
.card-header { |
|
|
position: relative; |
|
|
height: 200px; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.album-cover { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
object-fit: cover; |
|
|
} |
|
|
|
|
|
.track-number { |
|
|
position: absolute; |
|
|
top: 10px; |
|
|
right: 10px; |
|
|
background: rgba(0, 0, 0, 0.7); |
|
|
color: white; |
|
|
padding: 5px 12px; |
|
|
border-radius: 20px; |
|
|
font-weight: bold; |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
|
|
|
.card-content { |
|
|
padding: 1.5rem; |
|
|
} |
|
|
|
|
|
.music-title { |
|
|
font-size: 1.4rem; |
|
|
font-weight: 700; |
|
|
color: #2c3e50; |
|
|
margin: 0 0 1rem 0; |
|
|
} |
|
|
|
|
|
.music-tags { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
margin-bottom: 0.8rem; |
|
|
color: #7f8c8d; |
|
|
} |
|
|
|
|
|
.tag-icon, .duration-icon { |
|
|
margin-right: 0.5rem; |
|
|
font-size: 1rem; |
|
|
} |
|
|
|
|
|
.tag-text { |
|
|
font-size: 0.9rem; |
|
|
font-style: italic; |
|
|
} |
|
|
|
|
|
.music-duration { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
margin-bottom: 1rem; |
|
|
color: #7f8c8d; |
|
|
} |
|
|
|
|
|
.duration-text { |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.audio-player { |
|
|
width: 100%; |
|
|
margin: 1rem 0; |
|
|
border-radius: 8px; |
|
|
} |
|
|
|
|
|
.create-time { |
|
|
font-size: 0.8rem; |
|
|
color: #95a5a6; |
|
|
text-align: right; |
|
|
margin-top: 1rem; |
|
|
} |
|
|
|
|
|
/* 输入框样式 */ |
|
|
.gr-input, .gr-textarea { |
|
|
background: #ffffff !important; |
|
|
border: 2px solid #e1e8ed !important; |
|
|
border-radius: 10px !important; |
|
|
color: #374151 !important; |
|
|
font-size: 1rem !important; |
|
|
padding: 0.75rem 1rem !important; |
|
|
transition: all 0.3s ease !important; |
|
|
} |
|
|
|
|
|
.gr-input:focus, .gr-textarea:focus { |
|
|
border-color: #667eea !important; |
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important; |
|
|
} |
|
|
|
|
|
/* 下拉框样式 */ |
|
|
.gr-dropdown { |
|
|
background: #ffffff !important; |
|
|
border: 2px solid #e1e8ed !important; |
|
|
border-radius: 10px !important; |
|
|
} |
|
|
|
|
|
label { |
|
|
color: #4a5568 !important; |
|
|
font-weight: 600 !important; |
|
|
font-size: 0.9rem !important; |
|
|
margin-bottom: 0.5rem !important; |
|
|
} |
|
|
|
|
|
/* 示例样式 */ |
|
|
.examples-holder { |
|
|
margin-top: 2rem; |
|
|
padding: 1.5rem; |
|
|
background: #f8f9fa; |
|
|
border-radius: 12px; |
|
|
} |
|
|
|
|
|
/* 隐藏页脚 */ |
|
|
footer { |
|
|
display: none !important; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
with gr.Blocks(css=css, theme=gr.themes.Base()) as demo: |
|
|
with gr.Column(elem_classes="header-container"): |
|
|
gr.HTML(""" |
|
|
<h1 class="logo-text">🎵 Suno AI Music API Free Online Test</h1> |
|
|
<p class="subtitle">Supported by the latest Suno v4.5 and Suno v4.5+ models</p> |
|
|
<div class="mode-indicator"> |
|
|
💡 Test Suno AI Music API for free on Hugging Face Spaces. Generate music, edit vocals, and create tracks with the latest Suno v4.5 and Suno v4.5+ models. |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Column(elem_classes="main-content"): |
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="info-box"> |
|
|
<strong>Supported Features of Suno API:</strong><br> |
|
|
• <b>Generate Music:</b> Create original songs with or without lyrics.<br> |
|
|
• <b>Extend Music:</b> Seamlessly extend your tracks to any length.<br> |
|
|
• <b>Generate Lyrics:</b> Create unique lyrics from text prompts.<br> |
|
|
• <b>Add Vocals/Instrumentals:</b> Add vocals or instrumental accompaniments to your tracks.<br> |
|
|
• <b>Separate Vocals:</b> Isolate vocals or instruments for editing.<br> |
|
|
• <b>Convert to WAV:</b> Export music in high-quality WAV format.<br> |
|
|
<strong>Usage Instructions: </strong><br> |
|
|
• Get your Suno API Key <a href="https://kie.ai/playground/suno" target="_blank" style="color: #1976d2;">👉 here 👈</a><br> |
|
|
• Select your model: Choose from Suno v4.5+, Suno v4.5, Suno v4, or Suno v3.5.<br> |
|
|
• Choose Custom Mode (optional), specify if you want to generate instrumental music only, and enter your music style description or lyrics.<br> |
|
|
• Click Generate and wait ~1–2 minutes for processing.<br> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
|
|
|
with gr.Column(scale=1): |
|
|
|
|
|
with gr.Group(): |
|
|
gr.Markdown("### 🔑 API Configuration") |
|
|
api_key = gr.Textbox( |
|
|
label="API Key", |
|
|
placeholder="Please enter your API Key", |
|
|
type="password" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Group(): |
|
|
gr.Markdown("### 🎵 Music Information") |
|
|
title = gr.Textbox( |
|
|
label="Music Title", |
|
|
placeholder="Give your music a name", |
|
|
value="" |
|
|
) |
|
|
|
|
|
style = gr.Textbox( |
|
|
label="Music Style", |
|
|
placeholder="Enter your desired music style", |
|
|
interactive=True |
|
|
) |
|
|
|
|
|
prompt = gr.Textbox( |
|
|
label="Music Description", |
|
|
placeholder="Describe the music you want, e.g.: An energetic electronic dance track with futuristic synthesizer sounds...", |
|
|
lines=3, |
|
|
) |
|
|
|
|
|
generate_btn = gr.Button( |
|
|
"🎼 Generate Music", |
|
|
variant="primary", |
|
|
size="lg" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Column(scale=2): |
|
|
gr.Markdown("### 🎵 Generation Results") |
|
|
|
|
|
|
|
|
with gr.Row(elem_classes="music-cards-container"): |
|
|
music_display_1 = gr.HTML( |
|
|
value=create_music_card(None, 1), |
|
|
elem_id="music-1" |
|
|
) |
|
|
music_display_2 = gr.HTML( |
|
|
value=create_music_card(None, 2), |
|
|
elem_id="music-2" |
|
|
) |
|
|
|
|
|
|
|
|
status_text = gr.Textbox( |
|
|
label="Generation Status", |
|
|
interactive=False, |
|
|
value="Ready! Please enter music description and click generate button...", |
|
|
lines=2 |
|
|
) |
|
|
|
|
|
|
|
|
gr.Examples( |
|
|
examples=[ |
|
|
["Summer Beach", "Pop", "A light and cheerful summer beach music with fresh guitar and upbeat rhythm"], |
|
|
["Burning Heart", "Rock", "Passionate rock music with powerful electric guitar and drums"], |
|
|
["Neon Night", "Electronic", "Futuristic electronic music with cyberpunk style"], |
|
|
["Afternoon Time", "Jazz", "Gentle jazz piano piece, perfect for café ambiance"], |
|
|
["Heroic Epic", "Classical", "Epic orchestral music, grand and majestic, perfect for film soundtrack"], |
|
|
], |
|
|
inputs=[title, style, prompt], |
|
|
label="🎵 Music Creative Examples" |
|
|
) |
|
|
|
|
|
|
|
|
generate_btn.click( |
|
|
fn=process_music_generation, |
|
|
inputs=[prompt, style, title, api_key], |
|
|
outputs=[music_display_1, music_display_2, status_text] |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch( |
|
|
share=True, |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860 |
|
|
) |
|
|
|