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:
parent
25edd635c3
commit
ae9c061225
2 changed files with 114 additions and 14 deletions
91
app.py
91
app.py
|
|
@ -714,6 +714,73 @@ def stream_audio(token, version):
|
||||||
return response
|
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')
|
@app.route('/audio/<token>/<version>.mp3')
|
||||||
def audio(token, version):
|
def audio(token, version):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1053,7 +1120,7 @@ def admin_request(rid):
|
||||||
return redirect(url_for('admin_request', rid=rid))
|
return redirect(url_for('admin_request', rid=rid))
|
||||||
|
|
||||||
elif action == 'generate_stems':
|
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()
|
api_key = get_musicgpt_api_key()
|
||||||
if not api_key:
|
if not api_key:
|
||||||
flash('MusicGPT API key is not configured.', 'error')
|
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'):
|
if req.get('stems_status') in ('IN_QUEUE', 'IN_PROGRESS'):
|
||||||
flash('A stems extraction is already in progress.', 'error')
|
flash('A stems extraction is already in progress.', 'error')
|
||||||
return redirect(url_for('admin_request', rid=rid))
|
return redirect(url_for('admin_request', rid=rid))
|
||||||
audio_url = request.form.get('stems_source_url', '').strip()
|
# Operator selects which version (A or B) to extract stems from.
|
||||||
if not audio_url:
|
selected = request.form.get('stems_source', '').strip()
|
||||||
# Fall back to using the locally stored Version A MP3 path; upload not supported by extraction API.
|
if selected == 'a':
|
||||||
a_path = req.get('song_a_path')
|
song_path = req.get('song_a_path')
|
||||||
if a_path:
|
elif selected == 'b':
|
||||||
# Extraction API supports audio_url; we need a public URL. Local path won't work.
|
song_path = req.get('song_b_path')
|
||||||
flash('Please provide a public audio URL for stems extraction.', 'error')
|
else:
|
||||||
return redirect(url_for('admin_request', rid=rid))
|
flash('Please select Version A or Version B for stems extraction.', 'error')
|
||||||
flash('No Version A audio available to extract stems from.', 'error')
|
|
||||||
return redirect(url_for('admin_request', rid=rid))
|
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']
|
stems = request.form.getlist('stems') or ['vocals', 'instrumental']
|
||||||
task_id, conv_id, estimate, error = musicgpt_queue_stems(rid, audio_url, stems=stems)
|
task_id, conv_id, estimate, error = musicgpt_queue_stems(rid, audio_url, stems=stems)
|
||||||
if error:
|
if error:
|
||||||
|
|
|
||||||
|
|
@ -573,8 +573,35 @@
|
||||||
<!-- Stems -->
|
<!-- Stems -->
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="action" value="generate_stems">
|
<input type="hidden" name="action" value="generate_stems">
|
||||||
<label for="stems_source_url">Stems Source Audio URL</label>
|
<p style="margin:0 0 .5rem 0;font-weight:600">Select source MP3 for stems extraction:</p>
|
||||||
<input type="url" id="stems_source_url" name="stems_source_url" placeholder="https://.../song.mp3" value="{{ req.song_a_path or '' }}">
|
{% 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>
|
<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>
|
<label for="stems_status">Stems Status:</label>
|
||||||
|
|
@ -590,12 +617,14 @@
|
||||||
<div class="flash error">{{ req.stems_error }}</div>
|
<div class="flash error">{{ req.stems_error }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if req.stems_url %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% set stems_in_progress = req.stems_status in ('IN_QUEUE', 'IN_PROGRESS') %}
|
{% set stems_in_progress = req.stems_status in ('IN_QUEUE', 'IN_PROGRESS') %}
|
||||||
<div class="actions">
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue