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
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'])
|
||||
|
|
|
|||
Reference in a new issue