diff --git a/README.md b/README.md index 9541a2e..0d45394 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Theme Song Booth -**Version:** `v0.4.6` +**Version:** `v0.4.7` 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. @@ -38,7 +38,7 @@ A Flask web application for running a convention booth where visitors request a | Page | Path | Purpose | |------|------|---------| -| Request form | `/request` | Visitors enter name, email, hobbies, notable facts, and extra requests. The music style is picked from three dropdowns: **Decade** (required), **Basic style** (required), and **Additional style** (optional). Decades and genres are loaded from `/mnt/Storage/Decades.txt` and `/mnt/Storage/Music Genres.txt`. Rate limited to 5 submissions per minute per IP. | +| Request form | `/request` | Visitors enter email, name, pronouns, hobbies, notable facts, and extra requests. The music style is picked from three dropdowns: **Decade** (required), **Basic style** (required), and **Additional style** (optional). Decades and genres are loaded from `/mnt/Storage/Decades.txt` and `/mnt/Storage/Music Genres.txt`. Rate limited to 5 submissions per minute per IP. | | Closed page | `/request` (when booth is closed) | Shows a friendly closed banner instead of the form when the operator marks the booth closed. | | Thanks | `/thanks/` | Confirmation page shown after a request is submitted. | | Order status | `/status` | Customers enter their email to see all their requests and statuses. | @@ -54,7 +54,17 @@ The request form no longer has a free-text genre field. Instead, customers choos 2. **Basic style** — required (e.g. `Pop`). 3. **Additional style** — optional (e.g. `Funk`). -These are stored together in the `style_genre` column as a comma-separated string (e.g. `1980's, Pop, Funk`) so no database schema change is needed. The admin request page shows the same dropdowns and a live preview of the combined value. The copy-to-Hermes prompt formats this as a clean sentence like "1980's-era Pop with Funk influences" for better Suno results. +These are stored together in the `style_genre` column as a comma-separated string (e.g. `1980's, Pop, Funk`) so no schema change is required. The admin request page shows the same dropdowns for corrections, and the copy-to-Hermes prompt formats the style as a clean sentence like "1980's-era Pop with Funk influences" for better Suno results. + +### Pronouns + +A required **Pronouns** dropdown is shown just below the name field, with options: + +- He/Him/His +- She/Her/Hers +- They/Them/Their + +The selected pronouns are stored in the `pronouns` column and included in confirmation emails and Hermes prompt copy. --- @@ -64,7 +74,7 @@ These are stored together in the `style_genre` column as a comma-separated strin |------|------|---------| | 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, save prompt, copy Hermes callback, 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 management, and system reset. | @@ -214,6 +224,7 @@ Persistent volumes keep the database and uploads safe across redeploys. | `init_db.py` | Standalone script to create or migrate the database. | | `templates/` | Jinja2 templates for customer pages, admin pages, and kiosk display. | | `static/` | Banner images, closed banner, email logo, and kiosk QR code. | +| `lists/` | Bundled copies of `decades.txt` and `music_genres.txt` used as fallback for the style dropdowns. | | `Dockerfile` | Production container image definition. | | `docker-compose.yml` | Portainer stack definition. | | `requirements.txt` | Python dependencies. | diff --git a/VERSION b/VERSION index ef52a64..f905682 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.6 +0.4.7 diff --git a/app.py b/app.py index 2a9b99e..04f184b 100644 --- a/app.py +++ b/app.py @@ -529,6 +529,10 @@ def request_form(): decade = request.form.get('decade', '').strip() basic_style = request.form.get('basic_style', '').strip() additional_style = request.form.get('additional_style', '').strip() + pronouns = request.form.get('pronouns', '').strip() + if not pronouns: + flash('Please select your pronouns.', 'error') + return render_template('request.html', form=request.form, decades=DECADES, genres=MUSIC_GENRES), 400 if not decade: flash('Please select a decade.', 'error') return render_template('request.html', form=request.form, decades=DECADES, genres=MUSIC_GENRES), 400 @@ -538,6 +542,7 @@ def request_form(): form_data = { 'name': request.form.get('name', '').strip(), 'email': request.form.get('email', '').strip().lower(), + 'pronouns': pronouns, 'hobbies': request.form.get('hobbies', '').strip(), 'notable_facts': request.form.get('notable_facts', '').strip(), 'style_genre': build_style_genre(decade, basic_style, additional_style), @@ -561,6 +566,7 @@ def request_form(): "Here's what we have on file:", f"Name: {req['name']}", f"Email: {req['email']}", + f"Pronouns: {req['pronouns'] or '-'}", f"Style / genre: {req['style_genre'] or '-'}", f"Preferred singer voice / gender: {req['vocal_gender'] or 'No preference'}", f"Hobbies: {req['hobbies'] or '-'}", @@ -1070,9 +1076,13 @@ def admin_request(rid): elif action == 'update_customer_info': new_email = request.form.get('email', '').strip().lower() + pronouns = request.form.get('pronouns', '').strip() if not is_valid_email(new_email): flash('Please enter a valid email address.', 'error') return redirect(url_for('admin_request', rid=rid)) + if not pronouns: + flash('Pronouns are required.', 'error') + return redirect(url_for('admin_request', rid=rid)) style_genre = build_style_genre( request.form.get('decade', '').strip(), request.form.get('basic_style', '').strip(), @@ -1081,6 +1091,7 @@ def admin_request(rid): update_request(rid, email=new_email, name=request.form.get('name', '').strip(), + pronouns=pronouns, hobbies=request.form.get('hobbies', '').strip(), notable_facts=request.form.get('notable_facts', '').strip(), style_genre=style_genre, diff --git a/models.py b/models.py index e8d6da6..f56f30d 100644 --- a/models.py +++ b/models.py @@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS requests ( hobbies TEXT, notable_facts TEXT, style_genre TEXT, + pronouns TEXT, extra_requests TEXT, status TEXT DEFAULT 'pending', suno_title TEXT, @@ -101,7 +102,7 @@ def init_db(): expected_columns = { 'requests': [ 'id', 'created_at', 'name', 'email', 'hobbies', 'notable_facts', - 'style_genre', 'extra_requests', 'status', 'suno_title', 'suno_style', + 'style_genre', 'pronouns', 'extra_requests', 'status', 'suno_title', 'suno_style', 'suno_lyrics', 'song_a_path', 'song_b_path', 'vocal_gender', 'customer_approved', 'approval_notified_at', 'preview_sent_at', 'delivery_sent_at', 'square_payment_ref', 'admin_alert_email', @@ -134,7 +135,7 @@ def now_utc(): return datetime.now(timezone.utc).isoformat() -def create_request(name, email, hobbies, notable_facts, style_genre, extra_requests, vocal_gender=None): +def create_request(name, email, hobbies, notable_facts, style_genre, extra_requests, vocal_gender=None, pronouns=None): """ Insert a new customer request. Returns the auto-generated request id. @@ -142,9 +143,9 @@ def create_request(name, email, hobbies, notable_facts, style_genre, extra_reque db = get_db() cur = db.execute( """INSERT INTO requests - (name, email, hobbies, notable_facts, style_genre, extra_requests, vocal_gender, player_token) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", - (name, email, hobbies, notable_facts, style_genre, extra_requests, vocal_gender, new_token()) + (name, email, hobbies, notable_facts, style_genre, pronouns, extra_requests, vocal_gender, player_token) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (name, email, hobbies, notable_facts, style_genre, pronouns, extra_requests, vocal_gender, new_token()) ) db.commit() return cur.lastrowid diff --git a/templates/admin/request.html b/templates/admin/request.html index fe61dd6..3529d84 100644 --- a/templates/admin/request.html +++ b/templates/admin/request.html @@ -176,6 +176,14 @@ + + + @@ -209,7 +217,14 @@

Stored style: {{ req.style_genre or '—' }}

- + @@ -440,13 +455,14 @@ request_id: {{ req.id | tojson }}, email: {{ req.email | tojson }}, name: {{ req.name | tojson }}, + pronouns: {{ req.pronouns | tojson }}, hobbies: {{ req.hobbies | tojson }}, notable_facts: {{ req.notable_facts | tojson }}, style_genre: styleGenre, vocal_gender: {{ req.vocal_gender | tojson }}, extra_requests: {{ req.extra_requests | 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}\\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 || '-'}\\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 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 || '-'}\\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.")); } @@ -474,13 +490,14 @@ request_id: {{ req.id | tojson }}, email: {{ req.email | tojson }}, name: {{ req.name | tojson }}, + pronouns: {{ req.pronouns | tojson }}, hobbies: {{ req.hobbies | tojson }}, notable_facts: {{ req.notable_facts | tojson }}, style_genre: styleGenre, vocal_gender: {{ req.vocal_gender | tojson }}, extra_requests: {{ req.extra_requests | 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}\\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 || '-'}\\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 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 || '-'}\\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.")); } diff --git a/templates/request.html b/templates/request.html index 192f147..befc0ab 100644 --- a/templates/request.html +++ b/templates/request.html @@ -134,13 +134,21 @@ {% endwith %}
- - - - + + + + + + +