Fix MusicGPT integration: v6/v6-pro models, per-conversion webhooks, store conversion IDs

This commit is contained in:
Troll (Hermes Agent) 2026-08-10 22:53:26 +00:00
parent f185232907
commit 5443c464bd
5 changed files with 81 additions and 43 deletions

43
app.py
View file

@ -297,30 +297,43 @@ def musicgpt_webhook():
return jsonify({'ok': False, 'reason': 'missing_task_id'}), 400
conversion_id = data.get('conversion_id')
if not conversion_id and 'conversion_id_1' in data:
conversion_id = data.get('conversion_id_1')
# Music AI generation webhook
if 'Music' in conversion_type or 'Music AI' in conversion_type:
req = None
if 'Music' in conversion_type or 'Music AI' in conversion_type or not conversion_type:
db = get_db()
row = db.execute('SELECT * FROM requests WHERE musicgpt_task_id = ?', (task_id,)).fetchone()
if row:
req = dict(row)
if not req:
if not row:
return jsonify({'ok': False, 'reason': 'request_not_found'}), 404
req = dict(row)
# Determine which version this webhook belongs to.
version = None
if req.get('musicgpt_conversion_id_1') and conversion_id == req['musicgpt_conversion_id_1']:
version = 'A'
elif req.get('musicgpt_conversion_id_2') and conversion_id == req['musicgpt_conversion_id_2']:
version = 'B'
else:
# Fallback: if only one conversion_id stored, or single-webhook payload.
if not req.get('musicgpt_conversion_id_1'):
version = 'A'
elif not req.get('musicgpt_conversion_id_2'):
version = 'B'
new_status = status.upper()
update_fields = {'musicgpt_status': new_status}
if data.get('is_flagged'):
update_fields['musicgpt_error'] = data.get('reason') or 'Flagged by MusicGPT'
if new_status in ('COMPLETED', 'FINISHED'):
download_musicgpt_outputs(req, data)
download_musicgpt_outputs(req, data, version=version)
# Re-fetch to get updated paths.
row = db.execute('SELECT * FROM requests WHERE id = ?', (req['id'],)).fetchone()
req = dict(row)
if req.get('song_a_path') and req.get('song_b_path'):
update_fields['status'] = 'songs_uploaded'
try:
update_fields['musicgpt_cost'] = float(data.get('conversion_cost') or 0)
except (ValueError, TypeError):
pass
elif new_status in ('FAILED', 'ERROR'):
update_fields['musicgpt_error'] = data.get('reason') or data.get('error') or 'MusicGPT reported failure'
update_request(req['id'], **update_fields)
@ -328,13 +341,11 @@ def musicgpt_webhook():
# Extraction / stems webhook
if 'Extraction' in conversion_type:
req = None
db = get_db()
row = db.execute('SELECT * FROM requests WHERE stems_task_id = ?', (task_id,)).fetchone()
if row:
req = dict(row)
if not req:
if not row:
return jsonify({'ok': False, 'reason': 'request_not_found'}), 404
req = dict(row)
new_status = status.upper()
update_fields = {'stems_status': new_status}
@ -902,7 +913,8 @@ def admin_request(rid):
rid, title, style, lyrics, gender=gender, model=model
)
if error:
update_request(rid, musicgpt_status='ERROR', musicgpt_error=error)
update_request(rid, musicgpt_status='ERROR', musicgpt_error=error,
musicgpt_conversion_id_1=conv1, musicgpt_conversion_id_2=conv2)
flash(f'Failed to queue MusicGPT generation: {error}', 'error')
else:
update_request(rid,
@ -910,6 +922,8 @@ def admin_request(rid):
suno_style=style,
suno_lyrics=lyrics,
musicgpt_task_id=task_id,
musicgpt_conversion_id_1=conv1,
musicgpt_conversion_id_2=conv2,
musicgpt_status='IN_QUEUE',
musicgpt_error=None,
musicgpt_cost=estimate,
@ -1262,7 +1276,8 @@ def admin_settings():
'suno_lyrics', 'song_a_path', 'song_b_path', 'customer_approved',
'approval_notified_at', 'preview_sent_at', 'delivery_sent_at',
'square_payment_ref', 'admin_alert_email', 'player_token', 'revision_note', 'revision_count', 'operator_notes', 'stems_link', 'stems_interest',
'musicgpt_task_id', 'musicgpt_status', 'musicgpt_cost', 'musicgpt_error',
'musicgpt_task_id', 'musicgpt_conversion_id_1', 'musicgpt_conversion_id_2',
'musicgpt_status', 'musicgpt_cost', 'musicgpt_error',
'album_cover_url', 'song_a_wav_path', 'song_b_wav_path', 'deliver_wav',
'stems_task_id', 'stems_status', 'stems_cost', 'stems_url', 'stems_error'
}

View file

@ -102,8 +102,8 @@ class Config:
MUSICGPT_API_KEY = os.environ.get('MUSICGPT_API_KEY', '')
# Available MusicGPT generation models.
MUSICGPT_MODELS = ['v6', 'v6-pro', 'v7', 'v7-pro']
MUSICGPT_DEFAULT_MODEL = os.environ.get('MUSICGPT_DEFAULT_MODEL', 'v7-pro')
MUSICGPT_MODELS = ['v6', 'v6-pro']
MUSICGPT_DEFAULT_MODEL = os.environ.get('MUSICGPT_DEFAULT_MODEL', 'v6-pro')
@classmethod
def musicgpt_webhook_base_url(cls):

View file

@ -343,14 +343,14 @@ def 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')
default = current_app.config.get('MUSICGPT_DEFAULT_MODEL', 'v6-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']))
return list(current_app.config.get('MUSICGPT_MODELS', ['v6', 'v6-pro']))
def build_musicgpt_webhook_url():
@ -629,49 +629,70 @@ def _download_file(url, dest):
return False
def download_musicgpt_outputs(req, data):
def download_musicgpt_outputs(req, data, version=None):
"""
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).
`data` is the conversion dict from the API. For per-conversion webhooks,
pass `version='A' or 'B'`. For combined payloads, version is auto-detected
from 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)
# Map version label to field names.
def _fields(v):
return ("song_a_path", "song_a_wav_path") if v == "A" else ("song_b_path", "song_b_wav_path")
# Determine which version(s) are present in the payload.
versions = []
if version in ("A", "B"):
versions = [version]
elif data.get("conversion_path_1") or data.get("conversion_path_wav_1"):
if data.get("conversion_path_2") or data.get("conversion_path_wav_2"):
versions = ["A", "B"]
else:
versions = ["A"]
elif data.get("conversion_path_2") or data.get("conversion_path_wav_2"):
versions = ["B"]
elif data.get("conversion_path") or data.get("conversion_path_wav"):
# Per-conversion webhook without explicit version: use the requested version or A.
versions = [version if version in ("A", "B") else "A"]
for v in versions:
field_mp3, field_wav = _fields(v)
url_key = "conversion_path_1" if v == "A" else "conversion_path_2"
wav_key = "conversion_path_wav_1" if v == "A" else "conversion_path_wav_2"
# Also support per-conversion webhook keys without _1/_2 suffix.
if not (data.get(url_key) or data.get(wav_key)):
if v == "A" or version == "A":
mp3_url = data.get("conversion_path")
wav_url = data.get("conversion_path_wav")
else:
mp3_url = data.get("conversion_path")
wav_url = data.get("conversion_path_wav")
else:
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}"
dest = upload_dir / f"{v}{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"
dest_wav = upload_dir / f"{v}.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

View file

@ -55,6 +55,8 @@ CREATE TABLE IF NOT EXISTS requests (
stems_link TEXT,
stems_interest INTEGER DEFAULT 0,
musicgpt_task_id TEXT,
musicgpt_conversion_id_1 TEXT,
musicgpt_conversion_id_2 TEXT,
musicgpt_status TEXT,
musicgpt_cost REAL,
musicgpt_error TEXT,
@ -121,10 +123,10 @@ def init_db():
'customer_approved', 'approval_notified_at', 'preview_sent_at',
'delivery_sent_at', 'square_payment_ref', 'admin_alert_email',
'player_token', 'revision_count', 'revision_note', 'operator_notes',
'stems_link', 'stems_interest', 'musicgpt_task_id', 'musicgpt_status',
'musicgpt_cost', 'musicgpt_error', 'album_cover_url', 'song_a_wav_path',
'song_b_wav_path', 'deliver_wav', 'stems_task_id', 'stems_status',
'stems_cost', 'stems_url', 'stems_error'
'stems_link', 'stems_interest', 'musicgpt_task_id', 'musicgpt_conversion_id_1',
'musicgpt_conversion_id_2', 'musicgpt_status', 'musicgpt_cost', 'musicgpt_error',
'album_cover_url', 'song_a_wav_path', 'song_b_wav_path', 'deliver_wav',
'stems_task_id', 'stems_status', 'stems_cost', 'stems_url', 'stems_error'
],
'revision_history': [
'id', 'created_at', 'request_id', 'revision_count', 'note',

View file

@ -91,7 +91,7 @@
<p style="font-size:1.25rem;font-weight:700;margin:.25rem 0">${{ "%.4f"|format(cost_totals.stems_total) }}</p>
</div>
</div>
<p class="hint" style="margin-top:.5rem">Per-song cost approx. $0.035$0.06 USD on Pro plan depending on model.</p>
<p class="hint" style="margin-top:.5rem">Per-song cost approx. $0.035$0.045 USD on the current MusicGPT plan (v6 / v6-pro).</p>
</div>
<form method="POST">