Revert "feat: send song requests to Hermes via webhook"

This reverts commit d7ed56f34e.
This commit is contained in:
Troll (Hermes Agent) 2026-08-24 22:57:05 +00:00
parent d7ed56f34e
commit 6919ac816e
6 changed files with 15 additions and 213 deletions

View file

@ -1,6 +1,6 @@
# Theme Song Booth # Theme Song Booth
**Version:** `v0.7.0` **Version:** `v0.6.3`
A Flask web application for running a convention booth where visitors request a custom AI-generated theme song. Operators manage the queue from an admin dashboard, generate Suno prompts, upload MP3 previews, collect payment, and deliver final songs by email. A Flask web application for running a convention booth where visitors request a custom AI-generated theme song. Operators manage the queue from an admin dashboard, generate Suno prompts, upload MP3 previews, collect payment, and deliver final songs by email.
@ -25,13 +25,12 @@ A Flask web application for running a convention booth where visitors request a
## What the booth does ## What the booth does
1. A visitor fills out a short form at `/request`. 1. A visitor fills out a short form at `/request`.
2. The operator reviews the request in the admin dashboard and clicks **Send to Hermes** to push the customer data to a Hermes webhook. 2. The operator reviews the request in the admin dashboard and generates a Suno Custom Mode prompt.
3. Hermes generates a Suno Custom Mode prompt and can POST it directly back to the `/api/prompt` callback endpoint, or reply in chat with the prompt text. 3. The operator (or an AI assistant via the `/api/prompt` callback) saves the prompt to the request.
4. The operator (or the Hermes callback) saves the prompt to the request. 4. The operator creates two song versions in Suno, downloads them, and uploads **Version A** and **Version B** to the request page.
5. The operator creates two song versions in Suno, downloads them, and uploads **Version A** and **Version B** to the request page. 5. The operator sends a preview email; the customer visits their private player page, listens to both versions, and either approves one/both or requests changes.
6. The operator sends a preview email; the customer visits their private player page, listens to both versions, and either approves one/both or requests changes. 6. After the customer approves and pays, the operator records the payment reference and delivers the selected MP3(s) by email.
7. After the customer approves and pays, the operator records the payment reference and delivers the selected MP3(s) by email. 7. Optional stems / extras can be delivered via a share link that appears on the player page after delivery.
8. Optional stems / extras can be delivered via a share link that appears on the player page after delivery.
--- ---
@ -75,10 +74,10 @@ The selected pronouns are stored in the `pronouns` column and included in confir
|------|------|---------| |------|------|---------|
| Login | `/admin/login` | Simple session-based login. Password comes from the `ADMIN_PASSWORD` environment variable. | | Login | `/admin/login` | Simple session-based login. Password comes from the `ADMIN_PASSWORD` environment variable. |
| Dashboard | `/admin` | Main queue. Filter by status and auto-refresh at a configurable interval. | | Dashboard | `/admin` | Main queue. Filter by status and auto-refresh at a configurable interval. |
| Request detail | `/admin/request/<id>` | Full control of one request: edit customer info (including pronouns and structured style), save prompt, copy Hermes callback, **Send to Hermes** webhook, view revision history, upload MP3s, send preview, record payment, deliver files, add operator notes, and cancel. | | Request detail | `/admin/request/<id>` | Full control of one request: edit customer info (including pronouns and structured style), save prompt, copy Hermes callback, view revision history, upload MP3s, send preview, record payment, deliver files, add operator notes, and cancel. |
| Pricing | `/admin/pricing` | Configure fixed prices (one song, both songs, WAV per song, STEMs per song) and up to 5 custom items. | | Pricing | `/admin/pricing` | Configure fixed prices (one song, both songs, WAV per song, STEMs per song) and up to 5 custom items. |
| Sales | `/admin/sales` | Report of all delivered requests with customer details and Square payment references. | | Sales | `/admin/sales` | Report of all delivered requests with customer details and Square payment references. |
| Settings | `/admin/settings` | Database health, backups, SMTP config, MP3 metadata defaults, revision limit, auto-refresh interval, kiosk mode, booth open/closed switch, Hermes API key, Hermes webhook configuration, and system reset. | | Settings | `/admin/settings` | Database health, backups, SMTP config, MP3 metadata defaults, revision limit, auto-refresh interval, kiosk mode, booth open/closed switch, Hermes API key management, and system reset. |
| Reset | `/admin/reset` | Clears all requests and uploaded files. Requires admin password confirmation. | | Reset | `/admin/reset` | Clears all requests and uploaded files. Requires admin password confirmation. |
--- ---
@ -111,14 +110,9 @@ The `/admin/settings` page is split into functional sections:
### Hermes API key ### Hermes API key
- Displays whether a key is configured. - Displays whether a key is configured.
- **Regenerate API Key** creates a new random key stored in runtime settings.
- The key is used by the `/api/prompt/<id>` callback and by the `/api/key-test` diagnostic endpoint. - The key is used by the `/api/prompt/<id>` callback and by the `/api/key-test` diagnostic endpoint.
- Copy this key into your Hermes webhook subscription and AI assistant config. - Copy this key into your Hermes skill or AI assistant config.
### Hermes webhook
- Configure the public webhook URL and HMAC secret so the booth can push song request data to Hermes.
- The operator clicks **Send to Hermes** on a request to trigger prompt generation.
- The payload includes the callback URL so Hermes can POST the generated prompt back to `/api/prompt/<id>`.
- **Send Test Request** verifies the URL, secret, and signature.
### Customer revision limit ### Customer revision limit
- Maximum number of times a customer can click **Request Changes** on the player page. - Maximum number of times a customer can click **Request Changes** on the player page.
@ -180,8 +174,9 @@ python init_db.py
``` ```
7. Point your reverse proxy at the host port you chose (default `127.0.0.1:8000`). 7. Point your reverse proxy at the host port you chose (default `127.0.0.1:8000`).
8. Visit `/admin/settings` and configure the **Hermes webhook** URL and secret. 8. Visit `/admin/settings` and click **Regenerate API Key**.
9. Print or display a QR code pointing to `https://your-domain/request`. 9. Copy the key to your Hermes skill / AI assistant.
10. Print or display a QR code pointing to `https://your-domain/request`.
### Updating the deployment ### Updating the deployment

View file

@ -1 +1 @@
0.7.0 0.6.3

69
app.py
View file

@ -57,7 +57,6 @@ from helpers import (
get_max_revisions, get_callback_expiry_hours, get_max_revisions, get_callback_expiry_hours,
get_hermes_api_key, set_hermes_api_key, generate_hermes_api_key, mask_api_key, get_hermes_api_key, set_hermes_api_key, generate_hermes_api_key, mask_api_key,
get_ntfy_config, send_ntfy, get_ntfy_config, send_ntfy,
get_hermes_webhook_config, set_hermes_webhook_config, send_hermes_webhook,
sign_prompt_callback, verify_prompt_callback, build_prompt_callback_url, sign_prompt_callback, verify_prompt_callback, build_prompt_callback_url,
send_email, build_signature_images, send_email, build_signature_images,
get_booth_open, get_booth_open,
@ -906,37 +905,6 @@ def admin_request(rid):
) )
@app.route('/admin/request/<int:rid>/send-to-hermes', methods=['POST'])
def send_to_hermes(rid):
"""
Operator action: push the current request data to the configured Hermes webhook.
Hermes will generate a Suno prompt and either POST it back to the callback URL
or reply in chat.
"""
redir = require_admin()
if redir:
return redir
req = get_request_by_id(rid)
if not req:
abort(404)
if req['status'] not in ('pending', 'revisions_requested'):
flash('Request must be pending or awaiting revision to send to Hermes.', 'error')
return redirect(url_for('admin_request', rid=rid))
cfg = get_hermes_webhook_config()
if not cfg['url'] or not cfg['secret']:
flash('Hermes webhook is not configured. Set URL and secret in /admin/settings.', 'error')
return redirect(url_for('admin_request', rid=rid))
callback_url = build_prompt_callback_url(rid)
ok, msg = send_hermes_webhook(rid, req, callback_url=callback_url)
if ok:
flash(f'Sent to Hermes. {msg}', 'success')
else:
flash(f'Failed to send to Hermes: {msg}', 'error')
return redirect(url_for('admin_request', rid=rid))
@app.route('/admin/request/<int:rid>/delete', methods=['POST']) @app.route('/admin/request/<int:rid>/delete', methods=['POST'])
def admin_delete_request(rid): def admin_delete_request(rid):
"""Delete a single request and remove its uploaded MP3 files.""" """Delete a single request and remove its uploaded MP3 files."""
@ -1279,42 +1247,6 @@ def admin_settings():
flash('ntfy notification settings saved.', 'success') flash('ntfy notification settings saved.', 'success')
return redirect(url_for('admin_settings')) return redirect(url_for('admin_settings'))
elif action == 'save_hermes_webhook':
# Update Hermes webhook URL and secret from the settings form.
webhook_url = request.form.get('hermes_webhook_url', '').strip().rstrip('/')
webhook_secret = request.form.get('hermes_webhook_secret', '').strip()
set_hermes_webhook_config(webhook_url, webhook_secret)
flash('Hermes webhook settings saved.', 'success')
return redirect(url_for('admin_settings'))
elif action == 'send_test_hermes_webhook':
# Send a test request to the configured Hermes webhook.
cfg = get_hermes_webhook_config()
if not cfg['url'] or not cfg['secret']:
flash('Configure Hermes webhook URL and secret first.', 'error')
return redirect(url_for('admin_settings'))
ok, msg = send_hermes_webhook(0, {
'email': 'test@example.com',
'name': 'Test Customer',
'pronouns': 'They/Them/Their',
'hobbies': 'testing',
'notable_facts': 'none',
'style_genre': "1980's, Pop, Funk",
'vocal_gender': 'female',
'extra_requests': 'Test webhook',
'stems_interest': 0,
'revision_count': 0,
'revision_note': '',
'suno_title': '',
'suno_style': '',
'suno_lyrics': '',
})
if ok:
flash(f'Test Hermes webhook sent. {msg}', 'success')
else:
flash(f'Test Hermes webhook failed: {msg}', 'error')
return redirect(url_for('admin_settings'))
elif action == 'send_test_ntfy': elif action == 'send_test_ntfy':
# Send a test push notification to the configured ntfy topic. # Send a test push notification to the configured ntfy topic.
ntfy = get_ntfy_config() ntfy = get_ntfy_config()
@ -1357,7 +1289,6 @@ def admin_settings():
version=current_app.config['VERSION'], version=current_app.config['VERSION'],
current_callback_expiry_hours=get_callback_expiry_hours(), current_callback_expiry_hours=get_callback_expiry_hours(),
ntfy=runtime_settings, ntfy=runtime_settings,
hermes_webhook=get_hermes_webhook_config(),
) )

View file

@ -374,96 +374,6 @@ def get_ntfy_config():
} }
def get_hermes_webhook_config():
"""Return the Hermes webhook URL and HMAC secret from runtime settings."""
cfg = load_booth_settings()
return {
'url': cfg.get('hermes_webhook_url', '').strip(),
'secret': decrypt_value(cfg.get('hermes_webhook_secret', '')) or '',
}
def set_hermes_webhook_config(url, secret):
"""Persist Hermes webhook URL and secret (encrypted) to runtime settings."""
cfg = load_booth_settings()
cfg['hermes_webhook_url'] = url.strip().rstrip('/')
if secret:
cfg['hermes_webhook_secret'] = encrypt_value(secret)
save_booth_settings(cfg)
def send_hermes_webhook(rid, req, callback_url=None):
"""
POST a song request payload to the configured Hermes webhook.
Returns (success: bool, message: str). On success, Hermes receives the
customer data and can generate a Suno prompt. If callback_url is provided,
Hermes can POST the generated prompt directly back to /api/prompt/<rid>.
"""
cfg = get_hermes_webhook_config()
url = cfg.get('url', '')
secret = cfg.get('secret', '')
if not url or not secret:
return False, 'Hermes webhook is not configured in /admin/settings'
style_genre = req.get('style_genre') or ''
style_parts = [p.strip() for p in style_genre.split(',') if p.strip()]
style_sentence = ''
if style_parts:
parts = []
if style_parts[0]:
parts.append(f"{style_parts[0]}-era")
if len(style_parts) > 1 and style_parts[1]:
parts.append(style_parts[1])
if len(style_parts) > 2:
parts.append(f"with {', '.join(style_parts[2:])} influences")
style_sentence = ' '.join(parts)
def _bool(value):
if isinstance(value, bool):
return value
return bool(int(value or 0))
payload = {
'event_type': 'song_request',
'request_id': rid,
'email': req.get('email', ''),
'name': req.get('name', ''),
'pronouns': req.get('pronouns', ''),
'hobbies': req.get('hobbies', ''),
'notable_facts': req.get('notable_facts', ''),
'style_genre': style_sentence or style_genre,
'vocal_gender': req.get('vocal_gender', ''),
'extra_requests': req.get('extra_requests', ''),
'stems_interest': _bool(req.get('stems_interest')),
'is_revision': bool(req.get('revision_count', 0)) and bool(req.get('revision_note')),
'revision_count': int(req.get('revision_count') or 0),
'revision_note': req.get('revision_note', ''),
'previous_title': req.get('suno_title', ''),
'previous_style': req.get('suno_style', ''),
'previous_lyrics': req.get('suno_lyrics', ''),
'callback_url': callback_url or '',
}
body = json.dumps(payload, separators=(',', ':')).encode('utf-8')
timestamp = str(int(time.time()))
sig_data = f"{timestamp}.{body.decode('utf-8')}"
signature = hmac.new(secret.encode('utf-8'), sig_data.encode('utf-8'), hashlib.sha256).hexdigest()
headers = {
'Content-Type': 'application/json',
'X-Webhook-Signature-V2': signature,
'X-Webhook-Timestamp': timestamp,
}
try:
resp = requests.post(url, data=body, headers=headers, timeout=15)
if resp.status_code in (200, 202):
return True, f"Sent to Hermes (HTTP {resp.status_code})"
return False, f"Hermes webhook returned HTTP {resp.status_code}: {resp.text[:200]}"
except Exception as e:
return False, f"Failed to reach Hermes webhook: {e}"
def send_ntfy(message, title='Theme Song Booth', priority='default', tags='bell'): def send_ntfy(message, title='Theme Song Booth', priority='default', tags='bell'):
"""Send a push notification to the configured ntfy topic, if configured.""" """Send a push notification to the configured ntfy topic, if configured."""
ntfy = get_ntfy_config() ntfy = get_ntfy_config()

View file

@ -326,9 +326,6 @@
<div class="section"> <div class="section">
<h2>1. Generate &amp; Save Suno Prompt</h2> <h2>1. Generate &amp; Save Suno Prompt</h2>
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button> <button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
<form method="POST" action="{{ url_for('send_to_hermes', rid=req.id) }}" style="display:inline">
<button type="submit" onclick="return confirm('Send this request to Hermes to generate a Suno prompt?')">🚀 Send to Hermes</button>
</form>
<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.</p>
<form method="POST"> <form method="POST">

View file

@ -339,37 +339,6 @@
</form> </form>
</div> </div>
<!-- Hermes webhook integration -->
<div class="section">
<h2>Hermes Webhook</h2>
<p class="copy-hint">Push song request data to Hermes so it can generate Suno prompts automatically. The URL must include the route name (e.g. https://ntfy.hallsworth.ca/webhooks/trollgorithm-song-info). The secret is encrypted before storage.</p>
<form method="POST">
<input type="hidden" name="action" value="save_hermes_webhook">
<table class="meta-table">
<tr>
<td>Webhook URL</td>
<td><input type="text" id="hermes_webhook_url" name="hermes_webhook_url" value="{{ hermes_webhook.get('url', '') }}" placeholder="https://ntfy.hallsworth.ca/webhooks/trollgorithm-song-info"></td>
</tr>
<tr>
<td>HMAC Secret</td>
<td>
<input type="password" id="hermes_webhook_secret" name="hermes_webhook_secret" placeholder="{% if hermes_webhook.get('secret') %}Stored encrypted — type to replace{% else %}Enter secret{% endif %}">
<p class="copy-hint">From the Hermes webhook subscription. Leave blank to keep the existing stored secret.</p>
</td>
</tr>
</table>
<button type="submit">Save Hermes Webhook</button>
</form>
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
<h3>Send Test Request</h3>
<form method="POST">
<input type="hidden" name="action" value="send_test_hermes_webhook">
<button type="submit">Send Test to Hermes</button>
</form>
</div>
<!-- Email / SMTP configuration --> <!-- Email / SMTP configuration -->
<div class="section"> <div class="section">
<h2>Email (SMTP) Settings</h2> <h2>Email (SMTP) Settings</h2>