Add required pronouns dropdown and fix admin vocal gender selector
- Added a required Pronouns dropdown to /request and /admin/request: He/Him/His, She/Her/Hers, They/Them/Their. - Moved Email to the top of the customer request form; Pronouns sit just below Name. - Added column to the requests schema with automatic migration. - Stored pronouns are included in confirmation emails and Hermes prompt copy. - Fixed /admin/request so Preferred singer voice / gender is a dropdown matching the customer form instead of a free-text input. - Updated README to describe pronouns and the structured style selectors. Bumps version to 0.4.7.
This commit is contained in:
parent
10ee182558
commit
3f7bbf4b12
6 changed files with 65 additions and 17 deletions
19
README.md
19
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Theme Song Booth
|
# 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.
|
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 |
|
| 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. |
|
| 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/<id>` | Confirmation page shown after a request is submitted. |
|
| Thanks | `/thanks/<id>` | Confirmation page shown after a request is submitted. |
|
||||||
| Order status | `/status` | Customers enter their email to see all their requests and statuses. |
|
| 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`).
|
2. **Basic style** — required (e.g. `Pop`).
|
||||||
3. **Additional style** — optional (e.g. `Funk`).
|
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. |
|
| 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, 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/<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 management, 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. |
|
||||||
|
|
@ -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. |
|
| `init_db.py` | Standalone script to create or migrate the database. |
|
||||||
| `templates/` | Jinja2 templates for customer pages, admin pages, and kiosk display. |
|
| `templates/` | Jinja2 templates for customer pages, admin pages, and kiosk display. |
|
||||||
| `static/` | Banner images, closed banner, email logo, and kiosk QR code. |
|
| `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. |
|
| `Dockerfile` | Production container image definition. |
|
||||||
| `docker-compose.yml` | Portainer stack definition. |
|
| `docker-compose.yml` | Portainer stack definition. |
|
||||||
| `requirements.txt` | Python dependencies. |
|
| `requirements.txt` | Python dependencies. |
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.4.6
|
0.4.7
|
||||||
|
|
|
||||||
11
app.py
11
app.py
|
|
@ -529,6 +529,10 @@ def request_form():
|
||||||
decade = request.form.get('decade', '').strip()
|
decade = request.form.get('decade', '').strip()
|
||||||
basic_style = request.form.get('basic_style', '').strip()
|
basic_style = request.form.get('basic_style', '').strip()
|
||||||
additional_style = request.form.get('additional_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:
|
if not decade:
|
||||||
flash('Please select a decade.', 'error')
|
flash('Please select a decade.', 'error')
|
||||||
return render_template('request.html', form=request.form, decades=DECADES, genres=MUSIC_GENRES), 400
|
return render_template('request.html', form=request.form, decades=DECADES, genres=MUSIC_GENRES), 400
|
||||||
|
|
@ -538,6 +542,7 @@ def request_form():
|
||||||
form_data = {
|
form_data = {
|
||||||
'name': request.form.get('name', '').strip(),
|
'name': request.form.get('name', '').strip(),
|
||||||
'email': request.form.get('email', '').strip().lower(),
|
'email': request.form.get('email', '').strip().lower(),
|
||||||
|
'pronouns': pronouns,
|
||||||
'hobbies': request.form.get('hobbies', '').strip(),
|
'hobbies': request.form.get('hobbies', '').strip(),
|
||||||
'notable_facts': request.form.get('notable_facts', '').strip(),
|
'notable_facts': request.form.get('notable_facts', '').strip(),
|
||||||
'style_genre': build_style_genre(decade, basic_style, additional_style),
|
'style_genre': build_style_genre(decade, basic_style, additional_style),
|
||||||
|
|
@ -561,6 +566,7 @@ def request_form():
|
||||||
"Here's what we have on file:",
|
"Here's what we have on file:",
|
||||||
f"Name: {req['name']}",
|
f"Name: {req['name']}",
|
||||||
f"Email: {req['email']}",
|
f"Email: {req['email']}",
|
||||||
|
f"Pronouns: {req['pronouns'] or '-'}",
|
||||||
f"Style / genre: {req['style_genre'] or '-'}",
|
f"Style / genre: {req['style_genre'] or '-'}",
|
||||||
f"Preferred singer voice / gender: {req['vocal_gender'] or 'No preference'}",
|
f"Preferred singer voice / gender: {req['vocal_gender'] or 'No preference'}",
|
||||||
f"Hobbies: {req['hobbies'] or '-'}",
|
f"Hobbies: {req['hobbies'] or '-'}",
|
||||||
|
|
@ -1070,9 +1076,13 @@ def admin_request(rid):
|
||||||
|
|
||||||
elif action == 'update_customer_info':
|
elif action == 'update_customer_info':
|
||||||
new_email = request.form.get('email', '').strip().lower()
|
new_email = request.form.get('email', '').strip().lower()
|
||||||
|
pronouns = request.form.get('pronouns', '').strip()
|
||||||
if not is_valid_email(new_email):
|
if not is_valid_email(new_email):
|
||||||
flash('Please enter a valid email address.', 'error')
|
flash('Please enter a valid email address.', 'error')
|
||||||
return redirect(url_for('admin_request', rid=rid))
|
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(
|
style_genre = build_style_genre(
|
||||||
request.form.get('decade', '').strip(),
|
request.form.get('decade', '').strip(),
|
||||||
request.form.get('basic_style', '').strip(),
|
request.form.get('basic_style', '').strip(),
|
||||||
|
|
@ -1081,6 +1091,7 @@ def admin_request(rid):
|
||||||
update_request(rid,
|
update_request(rid,
|
||||||
email=new_email,
|
email=new_email,
|
||||||
name=request.form.get('name', '').strip(),
|
name=request.form.get('name', '').strip(),
|
||||||
|
pronouns=pronouns,
|
||||||
hobbies=request.form.get('hobbies', '').strip(),
|
hobbies=request.form.get('hobbies', '').strip(),
|
||||||
notable_facts=request.form.get('notable_facts', '').strip(),
|
notable_facts=request.form.get('notable_facts', '').strip(),
|
||||||
style_genre=style_genre,
|
style_genre=style_genre,
|
||||||
|
|
|
||||||
11
models.py
11
models.py
|
|
@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS requests (
|
||||||
hobbies TEXT,
|
hobbies TEXT,
|
||||||
notable_facts TEXT,
|
notable_facts TEXT,
|
||||||
style_genre TEXT,
|
style_genre TEXT,
|
||||||
|
pronouns TEXT,
|
||||||
extra_requests TEXT,
|
extra_requests TEXT,
|
||||||
status TEXT DEFAULT 'pending',
|
status TEXT DEFAULT 'pending',
|
||||||
suno_title TEXT,
|
suno_title TEXT,
|
||||||
|
|
@ -101,7 +102,7 @@ def init_db():
|
||||||
expected_columns = {
|
expected_columns = {
|
||||||
'requests': [
|
'requests': [
|
||||||
'id', 'created_at', 'name', 'email', 'hobbies', 'notable_facts',
|
'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',
|
'suno_lyrics', 'song_a_path', 'song_b_path', 'vocal_gender',
|
||||||
'customer_approved', 'approval_notified_at', 'preview_sent_at',
|
'customer_approved', 'approval_notified_at', 'preview_sent_at',
|
||||||
'delivery_sent_at', 'square_payment_ref', 'admin_alert_email',
|
'delivery_sent_at', 'square_payment_ref', 'admin_alert_email',
|
||||||
|
|
@ -134,7 +135,7 @@ def now_utc():
|
||||||
return datetime.now(timezone.utc).isoformat()
|
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.
|
Insert a new customer request.
|
||||||
Returns the auto-generated request id.
|
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()
|
db = get_db()
|
||||||
cur = db.execute(
|
cur = db.execute(
|
||||||
"""INSERT INTO requests
|
"""INSERT INTO requests
|
||||||
(name, email, hobbies, notable_facts, style_genre, extra_requests, vocal_gender, player_token)
|
(name, email, hobbies, notable_facts, style_genre, pronouns, extra_requests, vocal_gender, player_token)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
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, new_token())
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
return cur.lastrowid
|
return cur.lastrowid
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,14 @@
|
||||||
<label for="customer_name">Name / persona</label>
|
<label for="customer_name">Name / persona</label>
|
||||||
<input type="text" id="customer_name" name="name" value="{{ req.name }}" required>
|
<input type="text" id="customer_name" name="name" value="{{ req.name }}" required>
|
||||||
|
|
||||||
|
<label for="customer_pronouns">Pronouns <span style="color:#f87171">*</span></label>
|
||||||
|
<select id="customer_pronouns" name="pronouns" required>
|
||||||
|
<option value="" {% if not req.pronouns %}selected{% endif %}>— Select pronouns —</option>
|
||||||
|
<option value="He/Him/His" {% if req.pronouns == 'He/Him/His' %}selected{% endif %}>He/Him/His</option>
|
||||||
|
<option value="She/Her/Hers" {% if req.pronouns == 'She/Her/Hers' %}selected{% endif %}>She/Her/Hers</option>
|
||||||
|
<option value="They/Them/Their" {% if req.pronouns == 'They/Them/Their' %}selected{% endif %}>They/Them/Their</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<label for="customer_hobbies">Hobbies & interests</label>
|
<label for="customer_hobbies">Hobbies & interests</label>
|
||||||
<textarea id="customer_hobbies" name="hobbies" rows="3">{{ req.hobbies or '' }}</textarea>
|
<textarea id="customer_hobbies" name="hobbies" rows="3">{{ req.hobbies or '' }}</textarea>
|
||||||
|
|
||||||
|
|
@ -209,7 +217,14 @@
|
||||||
<p style="font-size:.85rem;color:#9ca3af;margin-top:.3rem">Stored style: {{ req.style_genre or '—' }}</p>
|
<p style="font-size:.85rem;color:#9ca3af;margin-top:.3rem">Stored style: {{ req.style_genre or '—' }}</p>
|
||||||
|
|
||||||
<label for="customer_vocal_gender">Preferred singer voice / gender</label>
|
<label for="customer_vocal_gender">Preferred singer voice / gender</label>
|
||||||
<input type="text" id="customer_vocal_gender" name="vocal_gender" value="{{ req.vocal_gender or '' }}">
|
<select id="customer_vocal_gender" name="vocal_gender">
|
||||||
|
<option value="" {% if not req.vocal_gender %}selected{% endif %}>No preference</option>
|
||||||
|
<option value="female" {% if req.vocal_gender == 'female' %}selected{% endif %}>Female</option>
|
||||||
|
<option value="male" {% if req.vocal_gender == 'male' %}selected{% endif %}>Male</option>
|
||||||
|
<option value="non-binary" {% if req.vocal_gender == 'non-binary' %}selected{% endif %}>Non-binary / Gender-neutral</option>
|
||||||
|
<option value="androgynous" {% if req.vocal_gender == 'androgynous' %}selected{% endif %}>Androgynous</option>
|
||||||
|
<option value="other" {% if req.vocal_gender == 'other' %}selected{% endif %}>Other (mention in Extra Requests)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<label for="customer_extra_requests">Anything else</label>
|
<label for="customer_extra_requests">Anything else</label>
|
||||||
<textarea id="customer_extra_requests" name="extra_requests" rows="3">{{ req.extra_requests or '' }}</textarea>
|
<textarea id="customer_extra_requests" name="extra_requests" rows="3">{{ req.extra_requests or '' }}</textarea>
|
||||||
|
|
@ -440,13 +455,14 @@
|
||||||
request_id: {{ req.id | tojson }},
|
request_id: {{ req.id | tojson }},
|
||||||
email: {{ req.email | tojson }},
|
email: {{ req.email | tojson }},
|
||||||
name: {{ req.name | tojson }},
|
name: {{ req.name | tojson }},
|
||||||
|
pronouns: {{ req.pronouns | tojson }},
|
||||||
hobbies: {{ req.hobbies | tojson }},
|
hobbies: {{ req.hobbies | tojson }},
|
||||||
notable_facts: {{ req.notable_facts | tojson }},
|
notable_facts: {{ req.notable_facts | tojson }},
|
||||||
style_genre: styleGenre,
|
style_genre: styleGenre,
|
||||||
vocal_gender: {{ req.vocal_gender | tojson }},
|
vocal_gender: {{ req.vocal_gender | tojson }},
|
||||||
extra_requests: {{ req.extra_requests | 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."));
|
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 }},
|
request_id: {{ req.id | tojson }},
|
||||||
email: {{ req.email | tojson }},
|
email: {{ req.email | tojson }},
|
||||||
name: {{ req.name | tojson }},
|
name: {{ req.name | tojson }},
|
||||||
|
pronouns: {{ req.pronouns | tojson }},
|
||||||
hobbies: {{ req.hobbies | tojson }},
|
hobbies: {{ req.hobbies | tojson }},
|
||||||
notable_facts: {{ req.notable_facts | tojson }},
|
notable_facts: {{ req.notable_facts | tojson }},
|
||||||
style_genre: styleGenre,
|
style_genre: styleGenre,
|
||||||
vocal_gender: {{ req.vocal_gender | tojson }},
|
vocal_gender: {{ req.vocal_gender | tojson }},
|
||||||
extra_requests: {{ req.extra_requests | 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."));
|
navigator.clipboard.writeText(text).then(() => alert("Revision prompt copied! Paste into Hermes. Hermes will POST the revised prompt back to the Callback URL."));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -134,13 +134,21 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<label for="name">Name / persona you want in the song</label>
|
|
||||||
<input type="text" id="name" name="name" value="{{ form.name if form else '' }}" required>
|
|
||||||
|
|
||||||
<label for="email">Email address</label>
|
<label for="email">Email address</label>
|
||||||
<input type="email" id="email" name="email" value="{{ form.email if form else '' }}" required>
|
<input type="email" id="email" name="email" value="{{ form.email if form else '' }}" required>
|
||||||
|
|
||||||
<label for="hobbies">Hobbies & interests</label>
|
<label for="name">Name / persona you want in the song</label>
|
||||||
|
<input type="text" id="name" name="name" value="{{ form.name if form else '' }}" required>
|
||||||
|
|
||||||
|
<label for="pronouns">Pronouns <span style="color:#f87171">*</span></label>
|
||||||
|
<select id="pronouns" name="pronouns" required>
|
||||||
|
<option value="" {% if not form or not form.pronouns %}selected{% endif %}>— Select pronouns —</option>
|
||||||
|
<option value="He/Him/His" {% if form and form.pronouns == 'He/Him/His' %}selected{% endif %}>He/Him/His</option>
|
||||||
|
<option value="She/Her/Hers" {% if form and form.pronouns == 'She/Her/Hers' %}selected{% endif %}>She/Her/Hers</option>
|
||||||
|
<option value="They/Them/Their" {% if form and form.pronouns == 'They/Them/Their' %}selected{% endif %}>They/Them/Their</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="hobbies">Hobbies & interests</label>
|
||||||
<textarea id="hobbies" name="hobbies" placeholder="e.g. rock climbing, retro gaming, sourdough baking">{{ form.hobbies if form else '' }}</textarea>
|
<textarea id="hobbies" name="hobbies" placeholder="e.g. rock climbing, retro gaming, sourdough baking">{{ form.hobbies if form else '' }}</textarea>
|
||||||
|
|
||||||
<label for="notable_facts">Notable things about you</label>
|
<label for="notable_facts">Notable things about you</label>
|
||||||
|
|
|
||||||
Reference in a new issue