Integrate MusicGPT API generation into booth-musicgpt v0.7.0
This commit is contained in:
parent
99855b1212
commit
2ca148be9b
12 changed files with 721 additions and 44 deletions
224
helpers.py
224
helpers.py
|
|
@ -33,6 +33,8 @@ from mutagen.easyid3 import EasyID3
|
|||
|
||||
import requests
|
||||
|
||||
from models import update_request
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Genre / decade helpers
|
||||
|
|
@ -334,6 +336,29 @@ def get_callback_expiry_hours():
|
|||
return max(1, val)
|
||||
|
||||
|
||||
def get_musicgpt_api_key():
|
||||
"""Return the MusicGPT API key from env only."""
|
||||
return current_app.config.get('MUSICGPT_API_KEY', '')
|
||||
|
||||
|
||||
def get_musicgpt_default_model():
|
||||
"""Return the configured default MusicGPT model."""
|
||||
default = current_app.config.get('MUSICGPT_DEFAULT_MODEL', 'v7-pro')
|
||||
models = get_musicgpt_models()
|
||||
return default if default in models else models[-1]
|
||||
|
||||
|
||||
def get_musicgpt_models():
|
||||
"""Return the list of supported MusicGPT models."""
|
||||
return list(current_app.config.get('MUSICGPT_MODELS', ['v6', 'v6-pro', 'v7', 'v7-pro']))
|
||||
|
||||
|
||||
def build_musicgpt_webhook_url():
|
||||
"""Build the public webhook URL for MusicGPT async callbacks."""
|
||||
base = current_app.config.get('PUBLIC_BASE_URL', '').rstrip('/')
|
||||
return f"{base}/api/musicgpt/webhook"
|
||||
|
||||
|
||||
def get_hermes_api_key():
|
||||
"""Return the effective Hermes API key."""
|
||||
env_key = current_app.config.get('HERMES_API_KEY', '')
|
||||
|
|
@ -500,3 +525,202 @@ def get_booth_open():
|
|||
"""Return True if the booth is currently marked as open in runtime settings."""
|
||||
cfg = load_booth_settings()
|
||||
return cfg.get('booth_open', True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MusicGPT API client
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MUSICGPT_API_BASE = "https://api.musicgpt.com/api/public"
|
||||
|
||||
|
||||
def _musicgpt_headers():
|
||||
"""Return authorization headers for MusicGPT API calls."""
|
||||
return {
|
||||
"Authorization": get_musicgpt_api_key(),
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
|
||||
def musicgpt_generate_request(rid, title, music_style, lyrics, gender=None, model=None):
|
||||
"""
|
||||
Queue a Music AI v2 generation request.
|
||||
Returns (task_id, conversion_id_1, conversion_id_2, credit_estimate, error_message).
|
||||
"""
|
||||
url = f"{MUSICGPT_API_BASE}/v2/MusicAI"
|
||||
if model is None:
|
||||
model = get_musicgpt_default_model()
|
||||
payload = {
|
||||
"title": title,
|
||||
"music_style": music_style,
|
||||
"lyrics": lyrics,
|
||||
"make_instrumental": False,
|
||||
"model": model,
|
||||
"webhook_url": build_musicgpt_webhook_url(),
|
||||
}
|
||||
if gender and gender.lower() in ("male", "female", "neutral"):
|
||||
payload["gender"] = gender.lower()
|
||||
try:
|
||||
resp = requests.post(url, json=payload, headers=_musicgpt_headers(), timeout=30)
|
||||
data = resp.json()
|
||||
if resp.status_code == 200 and data.get("success"):
|
||||
return (
|
||||
data.get("task_id"),
|
||||
data.get("conversion_id_1"),
|
||||
data.get("conversion_id_2"),
|
||||
data.get("credit_estimate"),
|
||||
None,
|
||||
)
|
||||
return None, None, None, None, data.get("error") or f"HTTP {resp.status_code}"
|
||||
except Exception as e:
|
||||
return None, None, None, None, str(e)
|
||||
|
||||
|
||||
def musicgpt_poll_status(task_id):
|
||||
"""
|
||||
Poll the MusicGPT API for a generation task status.
|
||||
Returns a dict with keys: status, message, conversion, or error.
|
||||
"""
|
||||
url = f"{MUSICGPT_API_BASE}/v1/byId"
|
||||
params = {"conversionType": "MUSIC_AI", "task_id": task_id}
|
||||
try:
|
||||
resp = requests.get(url, headers={"Authorization": get_musicgpt_api_key()}, params=params, timeout=20)
|
||||
data = resp.json()
|
||||
if resp.status_code == 200:
|
||||
return data
|
||||
return {"error": data.get("error") or f"HTTP {resp.status_code}"}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def musicgpt_queue_stems(rid, audio_url, stems=None):
|
||||
"""
|
||||
Queue a stem extraction job for a generated audio URL.
|
||||
Returns (task_id, conversion_id, credit_estimate, error_message).
|
||||
"""
|
||||
url = f"{MUSICGPT_API_BASE}/v2/Extraction"
|
||||
if stems is None:
|
||||
stems = ["vocals", "instrumental"]
|
||||
payload = {
|
||||
"audio_url": audio_url,
|
||||
"stems": json.dumps(stems),
|
||||
"webhook_url": build_musicgpt_webhook_url(),
|
||||
}
|
||||
try:
|
||||
resp = requests.post(url, data=payload, headers={"Authorization": get_musicgpt_api_key()}, timeout=30)
|
||||
data = resp.json()
|
||||
if resp.status_code == 200 and data.get("success"):
|
||||
return data.get("task_id"), data.get("conversion_id"), data.get("credit_estimate"), None
|
||||
return None, None, None, data.get("error") or f"HTTP {resp.status_code}"
|
||||
except Exception as e:
|
||||
return None, None, None, str(e)
|
||||
|
||||
|
||||
def _download_file(url, dest):
|
||||
"""Download a file from url to dest. Returns True on success."""
|
||||
try:
|
||||
with requests.get(url, stream=True, timeout=120) as r:
|
||||
r.raise_for_status()
|
||||
with open(dest, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def download_musicgpt_outputs(req, data):
|
||||
"""
|
||||
Download the MP3 and WAV outputs from a completed MusicGPT webhook/poll payload.
|
||||
Updates the request row with local paths and returns a dict of saved paths.
|
||||
`data` is the conversion dict from the API (may contain conversion_path_1/2 keys).
|
||||
"""
|
||||
request_id = req["id"]
|
||||
upload_dir = upload_path(request_id)
|
||||
song_title = req.get("suno_title") or req.get("title")
|
||||
saved = {}
|
||||
|
||||
def _save(url_key, wav_key, field_mp3, field_wav, version):
|
||||
mp3_url = data.get(url_key)
|
||||
wav_url = data.get(wav_key)
|
||||
if mp3_url:
|
||||
ext = Path(mp3_url).suffix or ".mp3"
|
||||
dest = upload_dir / f"{version}{ext}"
|
||||
if _download_file(mp3_url, dest):
|
||||
apply_mp3_tags(str(dest), song_title)
|
||||
saved[field_mp3] = str(dest)
|
||||
if wav_url:
|
||||
dest_wav = upload_dir / f"{version}.wav"
|
||||
if _download_file(wav_url, dest_wav):
|
||||
saved[field_wav] = str(dest_wav)
|
||||
|
||||
_save("conversion_path_1", "conversion_path_wav_1", "song_a_path", "song_a_wav_path", "A")
|
||||
_save("conversion_path_2", "conversion_path_wav_2", "song_b_path", "song_b_wav_path", "B")
|
||||
|
||||
# Album cover
|
||||
cover_url = data.get("album_cover_path")
|
||||
if cover_url:
|
||||
saved["album_cover_url"] = cover_url
|
||||
|
||||
# Cost
|
||||
cost = data.get("conversion_cost")
|
||||
if cost is None:
|
||||
# Webhook sample sometimes has conversion_cost string; poll has it nested.
|
||||
cost = data.get("conversion_cost")
|
||||
try:
|
||||
saved["musicgpt_cost"] = float(cost) if cost is not None else None
|
||||
except (ValueError, TypeError):
|
||||
saved["musicgpt_cost"] = None
|
||||
|
||||
if saved:
|
||||
update_request(request_id, **saved)
|
||||
return saved
|
||||
|
||||
|
||||
def format_musicgpt_cost(cost):
|
||||
"""Return a human-readable cost string in USD."""
|
||||
if cost is None:
|
||||
return "—"
|
||||
return f"${float(cost):.4f} USD"
|
||||
|
||||
|
||||
def download_album_cover(rid, cover_url):
|
||||
"""Download a MusicGPT album cover to a temp file and return its path."""
|
||||
if not cover_url:
|
||||
return None
|
||||
try:
|
||||
import tempfile
|
||||
resp = requests.get(cover_url, timeout=30)
|
||||
if resp.status_code == 200:
|
||||
ext = Path(cover_url).suffix or '.jpg'
|
||||
tmp = Path(tempfile.gettempdir()) / f"cover_{rid}{ext}"
|
||||
tmp.write_bytes(resp.content)
|
||||
return str(tmp)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def get_musicgpt_cost_totals():
|
||||
"""Return aggregate MusicGPT cost metrics across all requests."""
|
||||
from models import get_db
|
||||
db = get_db()
|
||||
rows = db.execute(
|
||||
"""
|
||||
SELECT
|
||||
COUNT(*) AS song_count,
|
||||
COALESCE(SUM(musicgpt_cost), 0) AS total_cost,
|
||||
COALESCE(SUM(
|
||||
CASE WHEN song_a_path IS NOT NULL AND song_b_path IS NOT NULL THEN 1 ELSE 0
|
||||
END), 0) AS completed_pairs,
|
||||
COALESCE(SUM(stems_cost), 0) AS stems_total
|
||||
FROM requests
|
||||
WHERE musicgpt_cost IS NOT NULL
|
||||
"""
|
||||
).fetchone()
|
||||
return {
|
||||
"song_count": rows["song_count"] or 0,
|
||||
"completed_pairs": rows["completed_pairs"] or 0,
|
||||
"total_cost": rows["total_cost"] or 0,
|
||||
"stems_total": rows["stems_total"] or 0,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue