Integrate MusicGPT API generation into booth-musicgpt v0.7.0

This commit is contained in:
Troll (Hermes Agent) 2026-08-10 22:01:08 +00:00
parent 99855b1212
commit 2ca148be9b
12 changed files with 721 additions and 44 deletions

View file

@ -134,6 +134,9 @@
<a href="{{ url_for('admin_dashboard', status='prompt_ready') }}" class="{% if current_status == 'prompt_ready' %}active{% endif %}">Needs Upload</a>
<a href="{{ url_for('admin_dashboard', status='awaiting_payment') }}" class="{% if current_status == 'awaiting_payment' %}active{% endif %}">Awaiting Payment</a>
<a href="{{ url_for('admin_dashboard', status='delivered') }}" class="{% if current_status == 'delivered' %}active{% endif %}">Delivered</a>
<form method="POST" action="{{ url_for('admin_musicgpt_refresh') }}" style="display:inline;margin-left:auto">
<button type="submit" class="secondary" style="margin:0;padding:.35rem .7rem;font-weight:600;background:#4b5563">Refresh MusicGPT Status</button>
</form>
<span class="refresh-hint">
{% if refresh_seconds and refresh_seconds > 0 %}
Auto-refresh every {{ refresh_seconds }}s

View file

@ -71,6 +71,29 @@
{% endfor %}
{% endwith %}
<div class="section" style="background:#1f2937;padding:1rem;border-radius:.5rem;margin-bottom:1rem">
<h2 style="margin-top:0">MusicGPT Costs (USD)</h2>
<div class="price-row">
<div>
<p class="hint" style="margin:0">Songs generated</p>
<p style="font-size:1.25rem;font-weight:700;margin:.25rem 0">{{ cost_totals.song_count }}</p>
</div>
<div>
<p class="hint" style="margin:0">Completed A/B pairs</p>
<p style="font-size:1.25rem;font-weight:700;margin:.25rem 0">{{ cost_totals.completed_pairs }}</p>
</div>
<div>
<p class="hint" style="margin:0">Music cost total</p>
<p style="font-size:1.25rem;font-weight:700;margin:.25rem 0">${{ "%.4f"|format(cost_totals.total_cost) }}</p>
</div>
<div>
<p class="hint" style="margin:0">Stems cost total</p>
<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>
</div>
<form method="POST">
<h2>Standard Items</h2>
<p class="hint">Leave blank to hide a price. Enter numbers only or include currency symbols as you prefer.</p>

View file

@ -164,6 +164,11 @@
<h1>Request #{{ req.id }} — {{ req.name }}</h1>
<div class="status-row">
<p style="margin:0"><strong>Status:</strong> <span class="status-badge">{{ statuses[req.status] }}</span></p>
{% if req.musicgpt_status %}
<p style="margin:0"><strong>MusicGPT:</strong> <span class="status-badge {% if req.musicgpt_status == 'COMPLETED' %}ok{% elif req.musicgpt_status in ('FAILED','ERROR','CANCELLED') %}missing{% endif %}">{{ req.musicgpt_status }}</span>
{% if req.musicgpt_cost is not none %}({{ format_musicgpt_cost(req.musicgpt_cost) }}){% endif %}
</p>
{% endif %}
{% if req.status != 'cancelled' %}
<form method="POST" onsubmit="return confirm('Are you sure you want to cancel request #{{ req.id }}?')" style="margin:0">
<input type="hidden" name="action" value="cancel_request">
@ -172,6 +177,12 @@
{% endif %}
</div>
{% if req.album_cover_url %}
<div class="section" style="text-align:center">
<img src="{{ req.album_cover_url }}" alt="Album cover" style="max-width:320px;border-radius:.5rem">
</div>
{% endif %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="flash {{ category }}">{{ message }}</div>
@ -322,11 +333,15 @@
<!-- RIGHT COLUMN -->
<div class="right">
<!-- Generate Suno Prompt -->
<!-- Generate Music Prompt -->
<div class="section">
<h2>1. Generate &amp; Save Suno Prompt</h2>
<h2>1. Generate & Save Music Prompt</h2>
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
<p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save.</p>
<p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save or generate.</p>
{% if req.musicgpt_error %}
<div class="flash error">Error: {{ req.musicgpt_error }}</div>
{% endif %}
<form method="POST">
<input type="hidden" name="action" value="save_prompt">
@ -343,32 +358,67 @@
<button type="submit">Save Prompt</button>
</div>
</form>
<hr style="border-color:#374151;margin:1rem 0">
<form method="POST">
<input type="hidden" name="action" value="generate_musicgpt">
<input type="hidden" name="suno_title" value="{{ req.suno_title or '' }}">
<input type="hidden" name="suno_style" value="{{ req.suno_style or '' }}">
<input type="hidden" name="suno_lyrics" value="{{ req.suno_lyrics or '' }}">
<label for="musicgpt_model">MusicGPT Model</label>
<select id="musicgpt_model" name="musicgpt_model">
{% for m in musicgpt_models %}
<option value="{{ m }}" {% if m == musicgpt_default_model %}selected{% endif %}>{{ m }}</option>
{% endfor %}
</select>
<label for="deliver_wav_gen" style="display:flex;align-items:center;gap:.5rem;margin-top:1rem;cursor:pointer;">
<input type="checkbox" id="deliver_wav_gen" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %} style="width:auto;margin:0">
Include WAV files with delivery
</label>
{% set in_progress = req.musicgpt_status in ('IN_QUEUE', 'IN_PROGRESS') %}
<div class="actions">
<button type="submit" {% if in_progress %}disabled{% endif %}>Generate A/B with MusicGPT</button>
{% if in_progress %}
<button type="submit" name="action" value="cancel_musicgpt" class="danger">Cancel Generation</button>
{% endif %}
</div>
</form>
</div>
<!-- Upload Songs -->
<div class="section">
<h2>2. Upload Songs</h2>
<h2>2. Songs</h2>
<form method="POST">
<input type="hidden" name="action" value="delete_songs">
<div class="song-row">
<input type="checkbox" id="delete_a" name="delete_song" value="a"
{% if not (req.song_a_path and file_exists(req.song_a_path)) %}disabled{% endif %}>
<label for="delete_a">Version A:
<label for="delete_a">Version A MP3:
{% if req.song_a_path and file_exists(req.song_a_path) %}
<span class="status-badge ok">✅ Uploaded</span> {{ basename(req.song_a_path) }}
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_a_path) }}
{% if req.song_a_wav_path and file_exists(req.song_a_wav_path) %}
<br><span class="status-badge ok">WAV</span> {{ basename(req.song_a_wav_path) }}
{% endif %}
{% else %}
<span class="status-badge missing">❌ Not uploaded</span>
<span class="status-badge missing">❌ Not ready</span>
{% endif %}
</label>
</div>
<div class="song-row">
<input type="checkbox" id="delete_b" name="delete_song" value="b"
{% if not (req.song_b_path and file_exists(req.song_b_path)) %}disabled{% endif %}>
<label for="delete_b">Version B:
<label for="delete_b">Version B MP3:
{% if req.song_b_path and file_exists(req.song_b_path) %}
<span class="status-badge ok">✅ Uploaded</span> {{ basename(req.song_b_path) }}
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_b_path) }}
{% if req.song_b_wav_path and file_exists(req.song_b_wav_path) %}
<br><span class="status-badge ok">WAV</span> {{ basename(req.song_b_wav_path) }}
{% endif %}
{% else %}
<span class="status-badge missing">❌ Not uploaded</span>
<span class="status-badge missing">❌ Not ready</span>
{% endif %}
</label>
</div>
@ -381,14 +431,45 @@
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload_songs">
<label for="song_a">Version A MP3</label>
<label for="song_a">Version A MP3 (override)</label>
<input type="file" id="song_a" name="song_a" accept="audio/mpeg,.mp3">
<label for="song_b">Version B MP3</label>
<label for="song_b">Version B MP3 (override)</label>
<input type="file" id="song_b" name="song_b" accept="audio/mpeg,.mp3">
<div class="actions">
<button type="submit">Upload Songs</button>
</div>
</form>
<hr style="border-color:#374151;margin:1rem 0">
<!-- Stems -->
<form method="POST">
<input type="hidden" name="action" value="generate_stems">
<label for="stems_source_url">Stems Source Audio URL</label>
<input type="url" id="stems_source_url" name="stems_source_url" placeholder="https://.../song.mp3" value="{{ req.song_a_path or '' }}">
<p class="copy-hint">Vocal + instrumental extraction: ~$0.018$0.084 USD per song on Pro plan.</p>
<label for="stems_status">Stems Status:</label>
<p>
{% if req.stems_status %}
<span class="status-badge {% if req.stems_status == 'COMPLETED' %}ok{% elif req.stems_status in ('FAILED','ERROR') %}missing{% endif %}">{{ req.stems_status }}</span>
{% if req.stems_cost is not none %}({{ format_musicgpt_cost(req.stems_cost) }}){% endif %}
{% else %}
<span class="status-badge missing">Not requested</span>
{% endif %}
</p>
{% if req.stems_error %}
<div class="flash error">{{ req.stems_error }}</div>
{% endif %}
{% if req.stems_url %}
<p><a href="{{ req.stems_url }}" target="_blank">Download stems →</a></p>
{% endif %}
{% set stems_in_progress = req.stems_status in ('IN_QUEUE', 'IN_PROGRESS') %}
<div class="actions">
<button type="submit" {% if stems_in_progress %}disabled{% endif %}>Generate Stems</button>
</div>
</form>
</div>
<!-- Notify Customer -->
@ -449,6 +530,20 @@
<hr style="border-color:#374151;margin:1rem 0">
<p style="color:#93c5fd;font-weight:600">Delivery options:</p>
<form method="POST" style="margin-bottom:.5rem">
<input type="hidden" name="action" value="save_delivery_options">
<label for="deliver_wav" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
<input type="checkbox" id="deliver_wav" name="deliver_wav" value="1" {% if req.deliver_wav %}checked{% endif %} style="width:auto;margin:0">
Include WAV files in delivery email
</label>
<div class="actions">
<button type="submit" class="secondary">Save Delivery Options</button>
</div>
</form>
<hr style="border-color:#374151;margin:1rem 0">
<p style="color:#93c5fd;font-weight:600">Select files to deliver:</p>
{% set all_files = [] %}
{% if req.song_a_path and file_exists(req.song_a_path) %}
@ -457,6 +552,14 @@
{% if req.song_b_path and file_exists(req.song_b_path) %}
{% set _ = all_files.append(req.song_b_path) %}
{% endif %}
{% if req.deliver_wav %}
{% if req.song_a_wav_path and file_exists(req.song_a_wav_path) %}
{% set _ = all_files.append(req.song_a_wav_path) %}
{% endif %}
{% if req.song_b_wav_path and file_exists(req.song_b_wav_path) %}
{% set _ = all_files.append(req.song_b_wav_path) %}
{% endif %}
{% endif %}
{% set files_to_show = all_files + extra_files %}
{% for fpath in files_to_show %}
@ -505,7 +608,7 @@
extra_requests: {{ req.extra_requests | tojson }},
stems_interest: {{ (req.stems_interest or 0) | tojson }}
};
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nGenerate a Suno Custom Mode prompt for this customer and POST it back to the Callback URL as JSON.\\n\\nCustomer data:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nExpected JSON response format:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nGenerate an AI music prompt for this customer and POST it back to the Callback URL as JSON.\\n\\nCustomer data:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nExpected JSON response format:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
navigator.clipboard.writeText(text).then(() => alert("Copied! Paste into Hermes. Hermes will POST the generated prompt back to the Callback URL."));
}
@ -541,7 +644,7 @@
extra_requests: {{ req.extra_requests | tojson }},
stems_interest: {{ (req.stems_interest or 0) | tojson }}
};
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nThis is REVISION #${revisionCount || 1} for this customer.\\n\\nGenerate a NEW Suno Custom Mode prompt that addresses the following revision request, while keeping the same overall theme/persona and matching the customer's original brief as closely as possible.\\n\\nOriginal brief:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nPreviously generated prompt (do not copy verbatim; adapt and improve):\\nTitle: ${currentTitle || '-'}\\nStyle: ${currentStyle || '-'}\\nLyrics:\\n${currentLyrics || '-'}\\n\\nRevision request from customer:\\n${revisionNote || '-'}\\n\\nPOST the new prompt back to the Callback URL as JSON with this shape:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nThis is REVISION #${revisionCount || 1} for this customer.\\n\\nGenerate a NEW AI music prompt that addresses the following revision request, while keeping the same overall theme/persona and matching the customer's original brief as closely as possible.\\n\\nOriginal brief:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nPreviously generated prompt (do not copy verbatim; adapt and improve):\\nTitle: ${currentTitle || '-'}\\nStyle: ${currentStyle || '-'}\\nLyrics:\\n${currentLyrics || '-'}\\n\\nRevision request from customer:\\n${revisionNote || '-'}\\n\\nPOST the new prompt back to the Callback URL as JSON with this shape:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
navigator.clipboard.writeText(text).then(() => alert("Revision prompt copied! Paste into Hermes. Hermes will POST the revised prompt back to the Callback URL."));
}
</script>

View file

@ -339,6 +339,28 @@
</form>
</div>
<!-- MusicGPT integration -->
<div class="section">
<h2>MusicGPT Integration</h2>
<p class="copy-hint">API key is set via the <code>MUSICGPT_API_KEY</code> environment variable in Docker.</p>
<p><strong>API key configured:</strong> {{ '✅ Yes' if musicgpt_api_key_set else '❌ No' }}</p>
<p><strong>Webhook URL:</strong> <span class="path">{{ musicgpt_webhook_url }}</span></p>
<p class="copy-hint">Register this URL in your MusicGPT account webhook settings so completed songs POST back here.</p>
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
<h3>Album Cover Delivery</h3>
<p class="copy-hint">When enabled, the generated album cover is attached to the delivery email. It is always shown on this admin page.</p>
<form method="POST">
<input type="hidden" name="action" value="save_album_cover">
<label for="deliver_album_cover" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
<input type="checkbox" id="deliver_album_cover" name="deliver_album_cover" value="1" {% if deliver_album_cover %}checked{% endif %} style="width:auto;margin:0">
Attach album cover to delivery email
</label>
<button type="submit">Save Album Cover Setting</button>
</form>
</div>
<!-- Email / SMTP configuration -->
<div class="section">
<h2>Email (SMTP) Settings</h2>