diff --git a/app.py b/app.py index 13141d4..28ef28a 100644 --- a/app.py +++ b/app.py @@ -714,6 +714,73 @@ def stream_audio(token, version): return response +@app.route('/api/audio-source//.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//.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: diff --git a/templates/admin/request.html b/templates/admin/request.html index 040b2d8..e24430b 100644 --- a/templates/admin/request.html +++ b/templates/admin/request.html @@ -573,8 +573,35 @@
- - +

Select source MP3 for stems extraction:

+ {% 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) %} +
+ + +
+
+ + +

Vocal + instrumental extraction: ~$0.018–$0.084 USD per song on Pro plan.

@@ -590,12 +617,14 @@
{{ req.stems_error }}
{% endif %} {% if req.stems_url %} -

Download stems →

+ {% endif %} {% set stems_in_progress = req.stems_status in ('IN_QUEUE', 'IN_PROGRESS') %}
- +