diff --git a/app.py b/app.py index d092fc2..cc394aa 100644 --- a/app.py +++ b/app.py @@ -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' } diff --git a/config.py b/config.py index 002fa0a..aba40bb 100644 --- a/config.py +++ b/config.py @@ -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): diff --git a/helpers.py b/helpers.py index d086c8e..fc29e96 100644 --- a/helpers.py +++ b/helpers.py @@ -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 diff --git a/models.py b/models.py index ce51c03..826da29 100644 --- a/models.py +++ b/models.py @@ -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', diff --git a/templates/admin/pricing.html b/templates/admin/pricing.html index 426da93..7ae75a8 100644 --- a/templates/admin/pricing.html +++ b/templates/admin/pricing.html @@ -91,7 +91,7 @@

${{ "%.4f"|format(cost_totals.stems_total) }}

-

Per-song cost approx. $0.035–$0.06 USD on Pro plan depending on model.

+

Per-song cost approx. $0.035–$0.045 USD on the current MusicGPT plan (v6 / v6-pro).