diff --git a/README.md b/README.md index db2b28d..147bbf8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. @@ -25,13 +25,12 @@ A Flask web application for running a convention booth where visitors request a ## What the booth does 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. -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. -4. The operator (or the Hermes callback) saves the prompt to the request. -5. The operator creates two song versions in Suno, downloads them, and uploads **Version A** and **Version B** to the request page. -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. -7. After the customer approves and pays, the operator records the payment reference and delivers the selected MP3(s) by email. -8. Optional stems / extras can be delivered via a share link that appears on the player page after delivery. +2. The operator reviews the request in the admin dashboard and generates a Suno Custom Mode prompt. +3. The operator (or an AI assistant via the `/api/prompt` 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 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. 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. | | Dashboard | `/admin` | Main queue. Filter by status and auto-refresh at a configurable interval. | -| Request detail | `/admin/request/` | 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/` | 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. | | 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. | --- @@ -111,14 +110,9 @@ The `/admin/settings` page is split into functional sections: ### Hermes API key - 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/` callback and by the `/api/key-test` diagnostic endpoint. -- Copy this key into your Hermes webhook subscription and 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/`. -- **Send Test Request** verifies the URL, secret, and signature. +- Copy this key into your Hermes skill or AI assistant config. ### Customer revision limit - 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`). -8. Visit `/admin/settings` and configure the **Hermes webhook** URL and secret. -9. Print or display a QR code pointing to `https://your-domain/request`. +8. Visit `/admin/settings` and click **Regenerate API Key**. +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 diff --git a/VERSION b/VERSION index faef31a..844f6a9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.0 +0.6.3 diff --git a/app.py b/app.py index 7662a2a..8b32217 100644 --- a/app.py +++ b/app.py @@ -57,7 +57,6 @@ from helpers import ( get_max_revisions, get_callback_expiry_hours, get_hermes_api_key, set_hermes_api_key, generate_hermes_api_key, mask_api_key, 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, send_email, build_signature_images, get_booth_open, @@ -906,37 +905,6 @@ def admin_request(rid): ) -@app.route('/admin/request//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//delete', methods=['POST']) def admin_delete_request(rid): """Delete a single request and remove its uploaded MP3 files.""" @@ -1279,42 +1247,6 @@ def admin_settings(): flash('ntfy notification settings saved.', 'success') 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': # Send a test push notification to the configured ntfy topic. ntfy = get_ntfy_config() @@ -1357,7 +1289,6 @@ def admin_settings(): version=current_app.config['VERSION'], current_callback_expiry_hours=get_callback_expiry_hours(), ntfy=runtime_settings, - hermes_webhook=get_hermes_webhook_config(), ) diff --git a/helpers.py b/helpers.py index ca1ad89..d0804eb 100644 --- a/helpers.py +++ b/helpers.py @@ -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/. - """ - 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'): """Send a push notification to the configured ntfy topic, if configured.""" ntfy = get_ntfy_config() diff --git a/templates/admin/request.html b/templates/admin/request.html index f64533b..9c3cc79 100644 --- a/templates/admin/request.html +++ b/templates/admin/request.html @@ -326,9 +326,6 @@

1. Generate & Save Suno Prompt

-
- -

Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save.

diff --git a/templates/admin/settings.html b/templates/admin/settings.html index 2e0b1d7..7f94a79 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -339,37 +339,6 @@
- -
-

Hermes Webhook

-

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.

-
- - - - - - - - - - -
Webhook URL
HMAC Secret - -

From the Hermes webhook subscription. Leave blank to keep the existing stored secret.

-
- -
- -
- -

Send Test Request

-
- - -
-
-

Email (SMTP) Settings