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

91
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')
return redirect(url_for('admin_request', rid=rid))
flash('No Version A audio available to extract stems from.', '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))
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: