|
|
|
|
@ -0,0 +1,264 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
"""
|
|
|
|
|
独立的Spleeter HTTP服务
|
|
|
|
|
为Java应用提供音频分离API接口
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import subprocess
|
|
|
|
|
import threading
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from flask import Flask, request, jsonify, send_file
|
|
|
|
|
from werkzeug.utils import secure_filename
|
|
|
|
|
|
|
|
|
|
# 配置日志
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
|
|
|
|
# 配置
|
|
|
|
|
UPLOAD_FOLDER = 'uploads'
|
|
|
|
|
OUTPUT_FOLDER = 'separated'
|
|
|
|
|
TEMP_FOLDER = 'temp'
|
|
|
|
|
ALLOWED_EXTENSIONS = {'wav', 'mp3', 'm4a', 'flac', 'aac'}
|
|
|
|
|
PORT = 5000
|
|
|
|
|
|
|
|
|
|
# 创建必要的目录
|
|
|
|
|
for folder in [UPLOAD_FOLDER, OUTPUT_FOLDER, TEMP_FOLDER]:
|
|
|
|
|
Path(folder).mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
def allowed_file(filename):
|
|
|
|
|
"""检查文件扩展名是否允许"""
|
|
|
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
|
|
|
|
|
|
def run_spleeter(input_file, output_dir, model='2stems'):
|
|
|
|
|
"""运行Spleeter分离音频"""
|
|
|
|
|
try:
|
|
|
|
|
# 构建Spleeter命令 - 正确的参数顺序
|
|
|
|
|
cmd = [
|
|
|
|
|
sys.executable, '-m', 'spleeter', 'separate',
|
|
|
|
|
'-p', f'spleeter:{model}',
|
|
|
|
|
'-o', output_dir,
|
|
|
|
|
'-c', 'wav',
|
|
|
|
|
input_file
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
logger.info(f"执行Spleeter命令: {' '.join(cmd)}")
|
|
|
|
|
|
|
|
|
|
# 执行命令
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
|
|
|
|
|
|
|
|
if result.returncode == 0:
|
|
|
|
|
logger.info("Spleeter分离成功")
|
|
|
|
|
return True, result.stdout
|
|
|
|
|
else:
|
|
|
|
|
logger.error(f"Spleeter分离失败: {result.stderr}")
|
|
|
|
|
return False, result.stderr
|
|
|
|
|
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
logger.error("Spleeter执行超时")
|
|
|
|
|
return False, "执行超时"
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Spleeter执行异常: {str(e)}")
|
|
|
|
|
return False, str(e)
|
|
|
|
|
|
|
|
|
|
def enhance_audio_with_ffmpeg(input_file, output_file, audio_type='vocals'):
|
|
|
|
|
"""使用FFmpeg优化音频"""
|
|
|
|
|
try:
|
|
|
|
|
if audio_type == 'vocals':
|
|
|
|
|
# 人声优化:高通滤波、压缩、均衡
|
|
|
|
|
cmd = [
|
|
|
|
|
'ffmpeg', '-i', input_file,
|
|
|
|
|
'-af', 'highpass=f=80,acompressor=threshold=0.1:ratio=4:attack=20:release=300,equalizer=f=1000:width_type=h:width=200:gain=3',
|
|
|
|
|
'-y', output_file
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
# 伴奏优化:低通滤波、压缩
|
|
|
|
|
cmd = [
|
|
|
|
|
'ffmpeg', '-i', input_file,
|
|
|
|
|
'-af', 'lowpass=f=8000,acompressor=threshold=0.05:ratio=3:attack=50:release=500',
|
|
|
|
|
'-y', output_file
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
logger.info(f"执行FFmpeg优化: {' '.join(cmd)}")
|
|
|
|
|
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
|
|
|
|
|
|
|
|
if result.returncode == 0:
|
|
|
|
|
logger.info("FFmpeg优化成功")
|
|
|
|
|
return True, "优化成功"
|
|
|
|
|
else:
|
|
|
|
|
logger.error(f"FFmpeg优化失败: {result.stderr}")
|
|
|
|
|
return False, result.stderr
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"FFmpeg优化异常: {str(e)}")
|
|
|
|
|
return False, str(e)
|
|
|
|
|
|
|
|
|
|
@app.route('/api/health', methods=['GET'])
|
|
|
|
|
def health_check():
|
|
|
|
|
"""健康检查接口"""
|
|
|
|
|
try:
|
|
|
|
|
# 检查Python和Spleeter
|
|
|
|
|
result = subprocess.run([sys.executable, '-c', 'import spleeter'],
|
|
|
|
|
capture_output=True, text=True)
|
|
|
|
|
spleeter_ok = result.returncode == 0
|
|
|
|
|
|
|
|
|
|
# 检查FFmpeg
|
|
|
|
|
result = subprocess.run(['ffmpeg', '-version'],
|
|
|
|
|
capture_output=True, text=True)
|
|
|
|
|
ffmpeg_ok = result.returncode == 0
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
'status': 'UP',
|
|
|
|
|
'python': sys.executable,
|
|
|
|
|
'spleeter': 'AVAILABLE' if spleeter_ok else 'NOT_AVAILABLE',
|
|
|
|
|
'ffmpeg': 'AVAILABLE' if ffmpeg_ok else 'NOT_AVAILABLE'
|
|
|
|
|
})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return jsonify({'status': 'DOWN', 'error': str(e)}), 500
|
|
|
|
|
|
|
|
|
|
@app.route('/api/separate', methods=['POST'])
|
|
|
|
|
def separate_audio():
|
|
|
|
|
"""音频分离接口"""
|
|
|
|
|
try:
|
|
|
|
|
# 检查文件
|
|
|
|
|
if 'file' not in request.files:
|
|
|
|
|
return jsonify({'error': '没有上传文件'}), 400
|
|
|
|
|
|
|
|
|
|
file = request.files['file']
|
|
|
|
|
if file.filename == '':
|
|
|
|
|
return jsonify({'error': '没有选择文件'}), 400
|
|
|
|
|
|
|
|
|
|
if not allowed_file(file.filename):
|
|
|
|
|
return jsonify({'error': '不支持的文件格式'}), 400
|
|
|
|
|
|
|
|
|
|
# 获取参数
|
|
|
|
|
model = request.form.get('model', '2stems')
|
|
|
|
|
enhance = request.form.get('enhance', 'true').lower() == 'true'
|
|
|
|
|
|
|
|
|
|
# 保存上传文件
|
|
|
|
|
filename = secure_filename(file.filename)
|
|
|
|
|
input_path = os.path.join(UPLOAD_FOLDER, filename)
|
|
|
|
|
file.save(input_path)
|
|
|
|
|
|
|
|
|
|
logger.info(f"开始分离音频: {filename}, 模型: {model}, 优化: {enhance}")
|
|
|
|
|
|
|
|
|
|
# 创建输出目录
|
|
|
|
|
base_name = os.path.splitext(filename)[0]
|
|
|
|
|
output_dir = os.path.join(OUTPUT_FOLDER, base_name)
|
|
|
|
|
Path(output_dir).mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# 执行Spleeter分离
|
|
|
|
|
success, message = run_spleeter(input_path, output_dir, model)
|
|
|
|
|
|
|
|
|
|
if not success:
|
|
|
|
|
return jsonify({'error': f'Spleeter分离失败: {message}'}), 500
|
|
|
|
|
|
|
|
|
|
# 检查分离结果
|
|
|
|
|
vocals_file = os.path.join(output_dir, f'{base_name}_vocals.wav')
|
|
|
|
|
accompaniment_file = os.path.join(output_dir, f'{base_name}_accompaniment.wav')
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(vocals_file) or not os.path.exists(accompaniment_file):
|
|
|
|
|
# 尝试其他可能的文件名格式
|
|
|
|
|
for file in Path(output_dir).glob('*_vocals.*'):
|
|
|
|
|
vocals_file = str(file)
|
|
|
|
|
for file in Path(output_dir).glob('*_accompaniment.*'):
|
|
|
|
|
accompaniment_file = str(file)
|
|
|
|
|
|
|
|
|
|
# 应用FFmpeg优化
|
|
|
|
|
if enhance:
|
|
|
|
|
enhanced_vocals = os.path.join(output_dir, f'{base_name}_vocals_enhanced.wav')
|
|
|
|
|
enhanced_accompaniment = os.path.join(output_dir, f'{base_name}_accompaniment_enhanced.wav')
|
|
|
|
|
|
|
|
|
|
# 优化人声
|
|
|
|
|
success, msg = enhance_audio_with_ffmpeg(vocals_file, enhanced_vocals, 'vocals')
|
|
|
|
|
if success:
|
|
|
|
|
vocals_file = enhanced_vocals
|
|
|
|
|
|
|
|
|
|
# 优化伴奏
|
|
|
|
|
success, msg = enhance_audio_with_ffmpeg(accompaniment_file, enhanced_accompaniment, 'accompaniment')
|
|
|
|
|
if success:
|
|
|
|
|
accompaniment_file = enhanced_accompaniment
|
|
|
|
|
|
|
|
|
|
# 返回结果
|
|
|
|
|
result = {
|
|
|
|
|
'success': True,
|
|
|
|
|
'task_id': base_name,
|
|
|
|
|
'vocals_file': os.path.basename(vocals_file),
|
|
|
|
|
'accompaniment_file': os.path.basename(accompaniment_file),
|
|
|
|
|
'output_dir': output_dir,
|
|
|
|
|
'message': '音频分离完成'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(f"音频分离完成: {result}")
|
|
|
|
|
return jsonify(result)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"音频分离异常: {str(e)}")
|
|
|
|
|
return jsonify({'error': f'服务器内部错误: {str(e)}'}), 500
|
|
|
|
|
|
|
|
|
|
@app.route('/api/download/<task_id>/<file_type>', methods=['GET'])
|
|
|
|
|
def download_file(task_id, file_type):
|
|
|
|
|
"""下载分离后的文件"""
|
|
|
|
|
try:
|
|
|
|
|
file_type = file_type.lower()
|
|
|
|
|
valid_types = ['vocals', 'accompaniment', 'vocals_enhanced', 'accompaniment_enhanced']
|
|
|
|
|
|
|
|
|
|
if file_type not in valid_types:
|
|
|
|
|
return jsonify({'error': '无效的文件类型'}), 400
|
|
|
|
|
|
|
|
|
|
# 查找文件
|
|
|
|
|
output_dir = os.path.join(OUTPUT_FOLDER, task_id)
|
|
|
|
|
if not os.path.exists(output_dir):
|
|
|
|
|
return jsonify({'error': '任务不存在'}), 404
|
|
|
|
|
|
|
|
|
|
# 查找匹配的文件
|
|
|
|
|
pattern = f"*{file_type}*"
|
|
|
|
|
files = list(Path(output_dir).glob(pattern))
|
|
|
|
|
|
|
|
|
|
if not files:
|
|
|
|
|
return jsonify({'error': '文件不存在'}), 404
|
|
|
|
|
|
|
|
|
|
file_path = str(files[0])
|
|
|
|
|
return send_file(file_path, as_attachment=True)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"文件下载异常: {str(e)}")
|
|
|
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
|
|
|
|
@app.route('/api/cleanup/<task_id>', methods=['DELETE'])
|
|
|
|
|
def cleanup_task(task_id):
|
|
|
|
|
"""清理任务文件"""
|
|
|
|
|
try:
|
|
|
|
|
import shutil
|
|
|
|
|
|
|
|
|
|
output_dir = os.path.join(OUTPUT_FOLDER, task_id)
|
|
|
|
|
if os.path.exists(output_dir):
|
|
|
|
|
shutil.rmtree(output_dir)
|
|
|
|
|
logger.info(f"清理任务文件: {task_id}")
|
|
|
|
|
|
|
|
|
|
return jsonify({'success': True, 'message': '清理完成'})
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"清理任务异常: {str(e)}")
|
|
|
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
logger.info(f"启动Spleeter HTTP服务,端口: {PORT}")
|
|
|
|
|
logger.info(f"上传目录: {UPLOAD_FOLDER}")
|
|
|
|
|
logger.info(f"输出目录: {OUTPUT_FOLDER}")
|
|
|
|
|
|
|
|
|
|
# 检查依赖
|
|
|
|
|
try:
|
|
|
|
|
import spleeter
|
|
|
|
|
logger.info("Spleeter导入成功")
|
|
|
|
|
except ImportError:
|
|
|
|
|
logger.error("Spleeter导入失败,请检查安装")
|
|
|
|
|
|
|
|
|
|
app.run(host='0.0.0.0', port=PORT, debug=False)
|