Replace free-text genre field with structured style dropdowns
The customer request form previously had an open 'Style / genre / mood' text field, which produced inconsistent genre descriptions and confused Suno. Replace it with three dropdowns: - Decade / era (required) — loaded from /mnt/Storage/Decades.txt - Basic style (required) — loaded from /mnt/Storage/Music Genres.txt - Additional style (optional) — same genre list The combined value is stored in the existing style_genre column as a comma-separated string (e.g. '1980's, Pop, Funk'), so no schema change is required. The admin request page exposes 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'. The canonical lists live in /mnt/Storage; bundled copies under lists/ act as a fallback when that mount is unavailable in the container. Bumps version to 0.4.6 and updates README.
This commit is contained in:
parent
d79c58691a
commit
10ee182558
7 changed files with 381 additions and 16 deletions
14
README.md
14
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# Theme Song Booth
|
||||
|
||||
**Version:** `v0.4.5`
|
||||
**Version:** `v0.4.6`
|
||||
|
||||
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, preferred style/genre, vocal gender, and extra requests. Rate limited to 5 submissions per minute per IP. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| Order status | `/status` | Customers enter their email to see all their requests and statuses. |
|
||||
|
|
@ -46,6 +46,16 @@ A Flask web application for running a convention booth where visitors request a
|
|||
| Private player | `/play/<token>` | Secret link emailed to the customer. Streams Version A and B, lets them approve or request revisions, and later download delivered files / stems. |
|
||||
| Kiosk | `/kiosk` | Public full-screen display for a booth tablet. Cycles between a QR code for `/request` and the configured price list. Updates automatically when pricing or booth state changes. |
|
||||
|
||||
### Style selection
|
||||
|
||||
The request form no longer has a free-text genre field. Instead, customers choose:
|
||||
|
||||
1. **Decade / era** — required (e.g. `1980's`).
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Operator / admin pages
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.4.5
|
||||
0.4.6
|
||||
|
|
|
|||
90
app.py
90
app.py
|
|
@ -64,6 +64,60 @@ from models import SCHEMA, get_requests_by_email, log_revision, list_revision_hi
|
|||
from mutagen.mp3 import MP3
|
||||
from mutagen.easyid3 import EasyID3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Genre / decade helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Load external lists once at import time. These are shared by the customer
|
||||
# request form and the admin request editor so the dropdowns stay in sync.
|
||||
# In production the canonical files live under /mnt/Storage. We also bundle
|
||||
# copies in the repo so the container still works if that mount is missing.
|
||||
_GENRES_PATH = Path('/mnt/Storage/Music Genres.txt')
|
||||
_DECADES_PATH = Path('/mnt/Storage/Decades.txt')
|
||||
_FALLBACK_GENRES_PATH = Path(__file__).parent / 'lists' / 'music_genres.txt'
|
||||
_FALLBACK_DECADES_PATH = Path(__file__).parent / 'lists' / 'decades.txt'
|
||||
|
||||
|
||||
def _load_lines(path: Path) -> list[str]:
|
||||
"""Load a text file and return non-empty stripped lines."""
|
||||
if not path.exists():
|
||||
return []
|
||||
lines = path.read_text(encoding='utf-8').splitlines()
|
||||
return [line.strip() for line in lines if line.strip()]
|
||||
|
||||
|
||||
def _load_list(primary: Path, fallback: Path) -> list[str]:
|
||||
"""Load from the primary path, falling back to the bundled copy."""
|
||||
lines = _load_lines(primary)
|
||||
if lines:
|
||||
return lines
|
||||
return _load_lines(fallback)
|
||||
|
||||
|
||||
MUSIC_GENRES = _load_list(_GENRES_PATH, _FALLBACK_GENRES_PATH)
|
||||
DECADES = _load_list(_DECADES_PATH, _FALLBACK_DECADES_PATH)
|
||||
|
||||
|
||||
def parse_style_genre(style_genre: str | None) -> dict:
|
||||
"""
|
||||
Split a stored combined style string into decade, basic, and additional.
|
||||
The stored format is 'Decade, Basic, Additional' (additional may be empty).
|
||||
"""
|
||||
parts = [p.strip() for p in (style_genre or '').split(',') if p.strip()]
|
||||
return {
|
||||
'decade': parts[0] if len(parts) > 0 else '',
|
||||
'basic_style': parts[1] if len(parts) > 1 else '',
|
||||
'additional_style': ', '.join(parts[2:]) if len(parts) > 2 else '',
|
||||
}
|
||||
|
||||
|
||||
def build_style_genre(decade: str, basic_style: str, additional_style: str) -> str:
|
||||
"""Build the combined style_genre string stored in the database."""
|
||||
parts = [p.strip() for p in [decade, basic_style, additional_style] if p.strip()]
|
||||
return ', '.join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# App setup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -472,18 +526,27 @@ def request_form():
|
|||
return render_template('closed.html')
|
||||
|
||||
if request.method == 'POST':
|
||||
decade = request.form.get('decade', '').strip()
|
||||
basic_style = request.form.get('basic_style', '').strip()
|
||||
additional_style = request.form.get('additional_style', '').strip()
|
||||
if not decade:
|
||||
flash('Please select a decade.', 'error')
|
||||
return render_template('request.html', form=request.form, decades=DECADES, genres=MUSIC_GENRES), 400
|
||||
if not basic_style:
|
||||
flash('Please select a basic style.', 'error')
|
||||
return render_template('request.html', form=request.form, decades=DECADES, genres=MUSIC_GENRES), 400
|
||||
form_data = {
|
||||
'name': request.form.get('name', '').strip(),
|
||||
'email': request.form.get('email', '').strip().lower(),
|
||||
'hobbies': request.form.get('hobbies', '').strip(),
|
||||
'notable_facts': request.form.get('notable_facts', '').strip(),
|
||||
'style_genre': request.form.get('style_genre', '').strip(),
|
||||
'style_genre': build_style_genre(decade, basic_style, additional_style),
|
||||
'extra_requests': request.form.get('extra_requests', '').strip(),
|
||||
'vocal_gender': request.form.get('vocal_gender', '').strip(),
|
||||
}
|
||||
if not is_valid_email(form_data['email']):
|
||||
flash('Please enter a valid email address.', 'error')
|
||||
return render_template('request.html', form=form_data), 400
|
||||
return render_template('request.html', form=form_data, decades=DECADES, genres=MUSIC_GENRES), 400
|
||||
rid = create_request(**form_data)
|
||||
|
||||
# Send confirmation email with a summary of what the customer asked for.
|
||||
|
|
@ -514,7 +577,7 @@ def request_form():
|
|||
|
||||
flash('Your request has been submitted! Check your email soon.', 'success')
|
||||
return redirect(url_for('thanks', rid=rid))
|
||||
return render_template('request.html', form=None)
|
||||
return render_template('request.html', form=None, decades=DECADES, genres=MUSIC_GENRES)
|
||||
|
||||
|
||||
@app.route('/api/ping', methods=['GET'])
|
||||
|
|
@ -1010,12 +1073,17 @@ def admin_request(rid):
|
|||
if not is_valid_email(new_email):
|
||||
flash('Please enter a valid email address.', '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(),
|
||||
request.form.get('additional_style', '').strip()
|
||||
)
|
||||
update_request(rid,
|
||||
email=new_email,
|
||||
name=request.form.get('name', '').strip(),
|
||||
hobbies=request.form.get('hobbies', '').strip(),
|
||||
notable_facts=request.form.get('notable_facts', '').strip(),
|
||||
style_genre=request.form.get('style_genre', '').strip(),
|
||||
style_genre=style_genre,
|
||||
vocal_gender=request.form.get('vocal_gender', '').strip(),
|
||||
extra_requests=request.form.get('extra_requests', '').strip()
|
||||
)
|
||||
|
|
@ -1132,7 +1200,19 @@ def admin_request(rid):
|
|||
|
||||
return redirect(url_for('admin_request', rid=rid))
|
||||
|
||||
return render_template('admin/request.html', req=req, statuses=STATUS_LABELS, file_exists=file_exists, basename=basename, extra_files=extra_files, callback_url=build_prompt_callback_url(rid), revision_history=list_revision_history(rid))
|
||||
return render_template(
|
||||
'admin/request.html',
|
||||
req=req,
|
||||
style_parts=parse_style_genre(req.get('style_genre')),
|
||||
decades=DECADES,
|
||||
genres=MUSIC_GENRES,
|
||||
statuses=STATUS_LABELS,
|
||||
file_exists=file_exists,
|
||||
basename=basename,
|
||||
extra_files=extra_files,
|
||||
callback_url=build_prompt_callback_url(rid),
|
||||
revision_history=list_revision_history(rid)
|
||||
)
|
||||
|
||||
|
||||
@app.route('/admin/request/<int:rid>/delete', methods=['POST'])
|
||||
|
|
|
|||
10
lists/decades.txt
Executable file
10
lists/decades.txt
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
Early 20th Centrury
|
||||
1940's
|
||||
1950's
|
||||
1960's
|
||||
1970's
|
||||
1980's
|
||||
1990's
|
||||
2000's
|
||||
2010's
|
||||
Modern
|
||||
190
lists/music_genres.txt
Executable file
190
lists/music_genres.txt
Executable file
|
|
@ -0,0 +1,190 @@
|
|||
A Cappella
|
||||
Abstract
|
||||
Acid
|
||||
Acid Jazz
|
||||
Acid Punk
|
||||
Acoustic
|
||||
Afro-Punk
|
||||
Alternative
|
||||
Alternative Rock
|
||||
Ambient
|
||||
Anime
|
||||
Art Rock
|
||||
Avantgarde
|
||||
Ballad
|
||||
Baroque
|
||||
Bass
|
||||
Beat
|
||||
Bebop
|
||||
Bhangra
|
||||
Big Band
|
||||
Big Beat
|
||||
Black Metal
|
||||
Bluegrass
|
||||
Blues
|
||||
Booty Bass
|
||||
Breakbeat
|
||||
BritPop
|
||||
Cabaret
|
||||
Celtic
|
||||
Chamber Music
|
||||
Chanson
|
||||
Chillout
|
||||
Chorus
|
||||
Christian Gangsta Rap
|
||||
Christian Rap
|
||||
Christian Rock
|
||||
Classic Rock
|
||||
Classical
|
||||
Club
|
||||
Club-House
|
||||
Comedy
|
||||
Contemporary Christian
|
||||
Country
|
||||
Crossover
|
||||
Cult
|
||||
Dance
|
||||
Dance Hall
|
||||
Darkwave
|
||||
Death Metal
|
||||
Disco
|
||||
Downtempo
|
||||
Dream
|
||||
Drum & Bass
|
||||
Drum Solo
|
||||
Dub
|
||||
Dubstep
|
||||
Duet
|
||||
Easy Listening
|
||||
EBM
|
||||
Eclectic
|
||||
Electro
|
||||
Electroclash
|
||||
Electronic
|
||||
Emo
|
||||
Ethnic
|
||||
Euro-House
|
||||
Euro-Techno
|
||||
Eurodance
|
||||
Experimental
|
||||
Fast Fusion
|
||||
Folk
|
||||
Folk-Rock
|
||||
Folklore
|
||||
Freestyle
|
||||
Funk
|
||||
Fusion
|
||||
G-Funk
|
||||
Game
|
||||
Gangsta
|
||||
Garage
|
||||
Garage Rock
|
||||
Global
|
||||
Goa
|
||||
Gospel
|
||||
Gothic
|
||||
Gothic Rock
|
||||
Grunge
|
||||
Hard Rock
|
||||
Hardcore
|
||||
Heavy Metal
|
||||
Hip-Hop
|
||||
House
|
||||
Humour
|
||||
IDM
|
||||
Illbient
|
||||
Indie
|
||||
Indie Rock
|
||||
Industrial
|
||||
Industro-Goth
|
||||
Instrumental
|
||||
Instrumental Pop
|
||||
Instrumental Rock
|
||||
Jam Band
|
||||
Jazz
|
||||
Jazz & Funk
|
||||
JPop
|
||||
Jungle
|
||||
Krautrock
|
||||
Latin
|
||||
Leftfield
|
||||
Lo-Fi
|
||||
Lounge
|
||||
Math Rock
|
||||
Mariachi
|
||||
Meditative
|
||||
Merengue
|
||||
Metal
|
||||
Musical
|
||||
National Folk
|
||||
Native American
|
||||
Neoclassical
|
||||
Neue Deutsche Welle
|
||||
New Age
|
||||
New Romantic
|
||||
New Wave
|
||||
Noise
|
||||
Nu-Breakz
|
||||
Oldies
|
||||
Opera
|
||||
Podcast
|
||||
Polka
|
||||
Polsk Punk
|
||||
Pop
|
||||
Pop-Folk
|
||||
Pop/Funk
|
||||
Porn Groove
|
||||
Post-Punk
|
||||
Post-Rock
|
||||
Power Ballad
|
||||
Pranks
|
||||
Primus
|
||||
Progressive Rock
|
||||
Psybient
|
||||
Psychedelic
|
||||
Psychedelic Rock
|
||||
Psytrance
|
||||
Punk
|
||||
Punk Rock
|
||||
Rap
|
||||
Rave
|
||||
Reggae
|
||||
Retro
|
||||
Revival
|
||||
Rhythm and Blues
|
||||
Rhythmic Soul
|
||||
Rock
|
||||
Rock & Roll
|
||||
Salsa
|
||||
Samba
|
||||
Satire
|
||||
Shoegaze
|
||||
Showtunes
|
||||
Ska
|
||||
Slow Jam
|
||||
Slow Rock
|
||||
Sonata
|
||||
Soul
|
||||
Sound Clip
|
||||
Soundtrack
|
||||
Southern Rock
|
||||
Space
|
||||
Space Rock
|
||||
Speech
|
||||
Swing
|
||||
Symphonic Rock
|
||||
Symphony
|
||||
Synthpop
|
||||
Tango
|
||||
Techno
|
||||
Techno-Industrial
|
||||
Terror
|
||||
Thrash Metal
|
||||
Top 40
|
||||
Trailer
|
||||
Trance
|
||||
Tribal
|
||||
Trip-Hop
|
||||
Trop Rock
|
||||
Vocal
|
||||
World Music
|
||||
|
|
@ -182,8 +182,31 @@
|
|||
<label for="customer_notable_facts">Notable things</label>
|
||||
<textarea id="customer_notable_facts" name="notable_facts" rows="3">{{ req.notable_facts or '' }}</textarea>
|
||||
|
||||
<label for="customer_style_genre">Style / genre / mood</label>
|
||||
<input type="text" id="customer_style_genre" name="style_genre" value="{{ req.style_genre or '' }}">
|
||||
<label for="customer_style_genre_decade">Decade / era <span style="color:#f87171">*</span></label>
|
||||
<select id="customer_style_genre_decade" name="decade" required>
|
||||
<option value="" {% if not style_parts.decade %}selected{% endif %}>— Select decade —</option>
|
||||
{% for d in decades %}
|
||||
<option value="{{ d }}" {% if style_parts.decade == d %}selected{% endif %}>{{ d }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="customer_style_genre_basic">Basic style <span style="color:#f87171">*</span></label>
|
||||
<select id="customer_style_genre_basic" name="basic_style" required>
|
||||
<option value="" {% if not style_parts.basic_style %}selected{% endif %}>— Select style —</option>
|
||||
{% for g in genres %}
|
||||
<option value="{{ g }}" {% if style_parts.basic_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="customer_style_genre_additional">Additional style (optional)</label>
|
||||
<select id="customer_style_genre_additional" name="additional_style">
|
||||
<option value="" {% if not style_parts.additional_style %}selected{% endif %}>— None —</option>
|
||||
{% for g in genres %}
|
||||
<option value="{{ g }}" {% if style_parts.additional_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<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>
|
||||
<input type="text" id="customer_vocal_gender" name="vocal_gender" value="{{ req.vocal_gender or '' }}">
|
||||
|
|
@ -407,17 +430,23 @@
|
|||
<script>
|
||||
function copyPromptForHermes() {
|
||||
const callbackUrl = {{ callback_url | tojson }};
|
||||
const styleGenre = {{ req.style_genre | tojson }};
|
||||
const styleParts = styleGenre ? styleGenre.split(', ') : [];
|
||||
const decade = styleParts[0] || '';
|
||||
const basic = styleParts[1] || '';
|
||||
const additional = styleParts.slice(2).join(', ') || '';
|
||||
const styleSentence = [decade && `${decade}-era`, basic, additional && `with ${additional} influences`].filter(Boolean).join(' ');
|
||||
const data = {
|
||||
request_id: {{ req.id | tojson }},
|
||||
email: {{ req.email | tojson }},
|
||||
name: {{ req.name | tojson }},
|
||||
hobbies: {{ req.hobbies | tojson }},
|
||||
notable_facts: {{ req.notable_facts | tojson }},
|
||||
style_genre: {{ req.style_genre | 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: ${data.style_genre || '-'}\\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}\\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."));
|
||||
}
|
||||
|
||||
|
|
@ -435,17 +464,23 @@
|
|||
const currentLyrics = {{ req.suno_lyrics | tojson }};
|
||||
const revisionNote = {{ req.revision_note | tojson }};
|
||||
const revisionCount = {{ req.revision_count | tojson }};
|
||||
const styleGenre = {{ req.style_genre | tojson }};
|
||||
const styleParts = styleGenre ? styleGenre.split(', ') : [];
|
||||
const decade = styleParts[0] || '';
|
||||
const basic = styleParts[1] || '';
|
||||
const additional = styleParts.slice(2).join(', ') || '';
|
||||
const styleSentence = [decade && `${decade}-era`, basic, additional && `with ${additional} influences`].filter(Boolean).join(' ');
|
||||
const data = {
|
||||
request_id: {{ req.id | tojson }},
|
||||
email: {{ req.email | tojson }},
|
||||
name: {{ req.name | tojson }},
|
||||
hobbies: {{ req.hobbies | tojson }},
|
||||
notable_facts: {{ req.notable_facts | tojson }},
|
||||
style_genre: {{ req.style_genre | 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: ${data.style_genre || '-'}\\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}\\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."));
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -146,8 +146,48 @@
|
|||
<label for="notable_facts">Notable things about you</label>
|
||||
<textarea id="notable_facts" name="notable_facts" placeholder="Anything fun, weird, or heroic we should mention">{{ form.notable_facts if form else '' }}</textarea>
|
||||
|
||||
<label for="style_genre">Style / genre / mood</label>
|
||||
<input type="text" id="style_genre" name="style_genre" value="{{ form.style_genre if form else '' }}" placeholder="e.g. 80s power ballad, cinematic orchestral, lo-fi synthwave">
|
||||
<label for="decade">Decade / era <span style="color:#f87171">*</span></label>
|
||||
<select id="decade" name="decade" required>
|
||||
<option value="" {% if not form or not form.decade %}selected{% endif %}>— Select decade —</option>
|
||||
{% for d in decades %}
|
||||
<option value="{{ d }}" {% if form and form.decade == d %}selected{% endif %}>{{ d }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="basic_style">Basic style <span style="color:#f87171">*</span></label>
|
||||
<select id="basic_style" name="basic_style" required>
|
||||
<option value="" {% if not form or not form.basic_style %}selected{% endif %}>— Select style —</option>
|
||||
{% for g in genres %}
|
||||
<option value="{{ g }}" {% if form and form.basic_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="additional_style">Additional style (optional)</label>
|
||||
<select id="additional_style" name="additional_style">
|
||||
<option value="" {% if not form or not form.additional_style %}selected{% endif %}>— None —</option>
|
||||
{% for g in genres %}
|
||||
<option value="{{ g }}" {% if form and form.additional_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<p style="font-size:.85rem;color:#9ca3af;margin-top:.3rem">Selected style: <span id="style-preview">{% if form %}{{ form.decade or '' }}{% if form.decade and form.basic_style %}, {% endif %}{{ form.basic_style or '' }}{% if form.basic_style and form.additional_style %}, {% endif %}{{ form.additional_style or '' }}{% else %}—{% endif %}</span></p>
|
||||
|
||||
<script>
|
||||
// Live preview of the combined style string as the customer changes dropdowns.
|
||||
(function(){
|
||||
const decade = document.getElementById('decade');
|
||||
const basic = document.getElementById('basic_style');
|
||||
const additional = document.getElementById('additional_style');
|
||||
const preview = document.getElementById('style-preview');
|
||||
function updatePreview(){
|
||||
const parts = [decade.value, basic.value, additional.value].filter(Boolean);
|
||||
preview.textContent = parts.length ? parts.join(', ') : '—';
|
||||
}
|
||||
decade.addEventListener('change', updatePreview);
|
||||
basic.addEventListener('change', updatePreview);
|
||||
additional.addEventListener('change', updatePreview);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<label for="vocal_gender">Preferred singer voice / gender</label>
|
||||
<select id="vocal_gender" name="vocal_gender">
|
||||
|
|
|
|||
Reference in a new issue