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>

View file

@ -62,13 +62,13 @@
<p>We create a custom, AI-generated theme song just for you based on your personality, hobbies, and style preferences.</p>
<p><strong>How long does it take to get my song?</strong></p>
<p>Most songs are ready to preview within a few minutes to an hour, depending on how busy the booth is!</p>
<p>Most songs are ready to preview within a few minutes, depending on how busy the booth is!</p>
<p><strong>How much does it cost?</strong></p>
<p>Pricing is displayed at the booth; you pay after listening to and choosing the version you want.</p>
<p><strong>What do I actually receive?</strong></p>
<p>You get an MP3 file of the version you selected, delivered straight to your email. WAV format is available for a slight additional charge.</p>
<p>You get MP3 files of the version(s) you selected, delivered straight to your email. WAV format and STEMS are available for an additional charge.</p>
<h2>How it works</h2>
<p><strong>What info do I need to give you?</strong></p>
@ -137,7 +137,7 @@
<p>To the email address you gave us, so double-check it for typos before submitting.</p>
<p><strong>What file format is it?</strong></p>
<p>The delivered song is a standard MP3 file that plays on virtually any device. WAV files can be provided upon request. We can also get you STEMS. If you know, you know, otherwise these aren't for everybody.</p>
<p>The delivered song is a standard MP3 file that plays on virtually any device. WAV files and STEMS can be provided upon request.</p>
<p><strong>Can you send it to someone else's email?</strong></p>
<p>At the booth we can update the email address if needed, but the original request needs a valid email to start.</p>

View file

@ -128,6 +128,13 @@
</div>
{% endif %}
<!-- Album cover -->
{% if req.album_cover_url %}
<div style="text-align:center;margin:1rem 0">
<img src="{{ req.album_cover_url }}" alt="Album cover" style="max-width:320px;border-radius:.5rem">
</div>
{% endif %}
<!-- Version A player -->
<div class="player">
<h3>Version A</h3>