Replace stems URL input with radio buttons + add download button

- Radio buttons to select Version A or B MP3 as stems source
- New /api/audio-source endpoint serves single MP3 for Extraction API
- Backend reads radio selection, constructs public URL automatically
- Download Stems button appears when stems are ready
- Generate button disabled when no MP3s are available
This commit is contained in:
Troll (Hermes Agent) 2026-08-11 19:14:25 +00:00
parent 25edd635c3
commit ae9c061225
2 changed files with 114 additions and 14 deletions

89
app.py
View file

@ -714,6 +714,73 @@ def stream_audio(token, version):
return response
@app.route('/api/audio-source/<token>/<version>.mp3')
def audio_source(token, version):
"""
Serve a single MP3 file for stems extraction.
Unlike /api/stream (which requires both A and B to exist for the customer
player), this endpoint serves whichever version is requested, as long as
that file exists. This URL is given to the MusicGPT Extraction API so it
can download the source audio for stem separation.
"""
req = get_request_by_token(token)
if not req:
abort(404)
if version not in ('a', 'b'):
abort(404)
path = req.get('song_a_path') if version == 'a' else req.get('song_b_path')
if not path or not Path(path).exists():
abort(404)
file_path = Path(path)
file_size = file_path.stat().st_size
range_header = request.headers.get('Range', '')
start = 0
end = file_size - 1
status_code = 200
if range_header and range_header.startswith('bytes='):
try:
range_value = range_header[len('bytes='):].strip()
if '-' in range_value:
parts = range_value.split('-')
if parts[0]:
start = int(parts[0])
if parts[1]:
end = min(int(parts[1]), file_size - 1)
if start >= file_size or start > end:
abort(416)
status_code = 206
except ValueError:
start = 0
end = file_size - 1
status_code = 200
def generate():
with open(file_path, 'rb') as f:
f.seek(start)
remaining = end - start + 1
chunk_size = 64 * 1024
while remaining > 0:
to_read = min(chunk_size, remaining)
data = f.read(to_read)
if not data:
break
yield data
remaining -= len(data)
response = current_app.response_class(generate(), mimetype='audio/mpeg')
response.status_code = status_code
response.headers['Accept-Ranges'] = 'bytes'
response.headers['Content-Disposition'] = 'inline'
response.headers['Content-Length'] = str(end - start + 1)
if status_code == 206:
response.headers['Content-Range'] = f'bytes {start}-{end}/{file_size}'
return response
@app.route('/audio/<token>/<version>.mp3')
def audio(token, version):
"""
@ -1053,7 +1120,7 @@ def admin_request(rid):
return redirect(url_for('admin_request', rid=rid))
elif action == 'generate_stems':
# Queue a stem extraction job for Version A MP3.
# Queue a stem extraction job for the selected MP3 (Version A or B).
api_key = get_musicgpt_api_key()
if not api_key:
flash('MusicGPT API key is not configured.', 'error')
@ -1061,16 +1128,20 @@ def admin_request(rid):
if req.get('stems_status') in ('IN_QUEUE', 'IN_PROGRESS'):
flash('A stems extraction is already in progress.', 'error')
return redirect(url_for('admin_request', rid=rid))
audio_url = request.form.get('stems_source_url', '').strip()
if not audio_url:
# Fall back to using the locally stored Version A MP3 path; upload not supported by extraction API.
a_path = req.get('song_a_path')
if a_path:
# Extraction API supports audio_url; we need a public URL. Local path won't work.
flash('Please provide a public audio URL for stems extraction.', 'error')
# Operator selects which version (A or B) to extract stems from.
selected = request.form.get('stems_source', '').strip()
if selected == 'a':
song_path = req.get('song_a_path')
elif selected == 'b':
song_path = req.get('song_b_path')
else:
flash('Please select Version A or Version B for stems extraction.', 'error')
return redirect(url_for('admin_request', rid=rid))
flash('No Version A audio available to extract stems from.', 'error')
if not song_path or not Path(song_path).exists():
flash(f'Version {selected.upper()} MP3 is not available. Generate or upload it first.', 'error')
return redirect(url_for('admin_request', rid=rid))
# Build a public URL the MusicGPT Extraction API can fetch.
audio_url = f"{current_app.config['PUBLIC_BASE_URL'].rstrip('/')}/api/audio-source/{req['player_token']}/{selected}.mp3"
stems = request.form.getlist('stems') or ['vocals', 'instrumental']
task_id, conv_id, estimate, error = musicgpt_queue_stems(rid, audio_url, stems=stems)
if error:

View file

@ -573,8 +573,35 @@
<!-- Stems -->
<form method="POST">
<input type="hidden" name="action" value="generate_stems">
<label for="stems_source_url">Stems Source Audio URL</label>
<input type="url" id="stems_source_url" name="stems_source_url" placeholder="https://.../song.mp3" value="{{ req.song_a_path or '' }}">
<p style="margin:0 0 .5rem 0;font-weight:600">Select source MP3 for stems extraction:</p>
{% set a_ready = req.song_a_path and file_exists(req.song_a_path) %}
{% set b_ready = req.song_b_path and file_exists(req.song_b_path) %}
<div class="song-row">
<input type="radio" id="stems_a" name="stems_source" value="a"
{% if not a_ready %}disabled{% endif %}
{% if a_ready and not b_ready %}checked{% endif %}>
<label for="stems_a" {% if not a_ready %}style="opacity:.6"{% endif %}>
Version A MP3:
{% if a_ready %}
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_a_path) }}
{% else %}
<span class="status-badge missing">❌ Not ready</span>
{% endif %}
</label>
</div>
<div class="song-row">
<input type="radio" id="stems_b" name="stems_source" value="b"
{% if not b_ready %}disabled{% endif %}
{% if b_ready %}checked{% endif %}>
<label for="stems_b" {% if not b_ready %}style="opacity:.6"{% endif %}>
Version B MP3:
{% if b_ready %}
<span class="status-badge ok">✅ Ready</span> {{ basename(req.song_b_path) }}
{% else %}
<span class="status-badge missing">❌ Not ready</span>
{% endif %}
</label>
</div>
<p class="copy-hint">Vocal + instrumental extraction: ~$0.018$0.084 USD per song on Pro plan.</p>
<label for="stems_status">Stems Status:</label>
@ -590,12 +617,14 @@
<div class="flash error">{{ req.stems_error }}</div>
{% endif %}
{% if req.stems_url %}
<p><a href="{{ req.stems_url }}" target="_blank">Download stems →</a></p>
<div class="actions">
<a href="{{ req.stems_url }}" target="_blank" rel="noopener noreferrer" class="button-link" style="display:inline-flex;align-items:center;gap:.4rem;padding:.5rem 1rem;background:#4b5563;border-radius:.375rem;color:#fff;text-decoration:none;font-size:.875rem">⬇ Download Stems</a>
</div>
{% endif %}
{% set stems_in_progress = req.stems_status in ('IN_QUEUE', 'IN_PROGRESS') %}
<div class="actions">
<button type="submit" {% if stems_in_progress %}disabled{% endif %}>Generate Stems</button>
<button type="submit" {% if stems_in_progress or not (a_ready or b_ready) %}disabled{% endif %}>Generate Stems</button>
</div>
</form>
</div>