From ecdae446b06f3ab31c55fe27e72cae7dc52834d0 Mon Sep 17 00:00:00 2001 From: wuhaobin <2307299049@qq.com> Date: Thu, 13 Nov 2025 23:41:12 +0800 Subject: [PATCH] 1 Signed-off-by: wuhaobin <2307299049@qq.com> --- src/daudio/diagnose_audio_separation.py | 314 ++++++++++++++++++ src/daudio/spleeter_monitor.py | 204 ++++++++++++ src/daudio/spleeter_service.py | 264 +++++++++++++++ .../service/AudioSeparationServiceTest.java | 40 +++ src/daudio/start_services.bat | 88 +++++ src/daudio/test_audio.mp3 | Bin 0 -> 40559 bytes src/daudio/test_ffmpeg_enhancement.py | 178 ++++++++++ 7 files changed, 1088 insertions(+) create mode 100644 src/daudio/diagnose_audio_separation.py create mode 100644 src/daudio/spleeter_monitor.py create mode 100644 src/daudio/spleeter_service.py create mode 100644 src/daudio/src/test/java/com/cauc/deaudio/service/AudioSeparationServiceTest.java create mode 100644 src/daudio/start_services.bat create mode 100644 src/daudio/test_audio.mp3 create mode 100644 src/daudio/test_ffmpeg_enhancement.py diff --git a/src/daudio/diagnose_audio_separation.py b/src/daudio/diagnose_audio_separation.py new file mode 100644 index 0000000..6cdd18e --- /dev/null +++ b/src/daudio/diagnose_audio_separation.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +""" +音频分离服务诊断工具 +检查Spleeter和FFmpeg是否正常工作 +""" + +import requests +import json +import os +import subprocess +import sys + +def check_server_status(): + """检查服务器状态""" + print("🔍 检查服务器状态...") + try: + response = requests.get("http://localhost:8081/api/audio/health", timeout=5) + if response.status_code == 200: + result = response.json() + status = result.get('status', 'UNKNOWN') + ffmpeg_status = result.get('ffmpeg', 'UNKNOWN') + spleeter_status = result.get('spleeter', 'UNKNOWN') + + print(f"✅ 服务器正常运行 (状态: {status})") + print(f" FFmpeg: {ffmpeg_status}") + print(f" Spleeter: {spleeter_status}") + return True + else: + print(f"⚠️ 服务器响应异常,状态码: {response.status_code}") + return False + except requests.exceptions.ConnectionError: + print("❌ 无法连接到服务器") + return False + except Exception as e: + print(f"❌ 检查服务器状态时出错: {e}") + return False + +def check_spleeter_availability(): + """检查Spleeter可用性""" + print("\n🔍 检查Spleeter可用性...") + try: + response = requests.get("http://localhost:8081/api/audio/test-spleeter", timeout=30) + if response.status_code == 200: + result = response.json() + available = result.get('available', False) + print(f"Spleeter状态: {'✅ 可用' if available else '❌ 不可用'}") + return available + else: + print(f"⚠️ Spleeter测试请求失败,状态码: {response.status_code}") + return False + except Exception as e: + print(f"❌ 检查Spleeter可用性时出错: {e}") + return False + +def check_ffmpeg_availability(): + """检查FFmpeg可用性""" + print("\n🔍 检查FFmpeg可用性...") + try: + response = requests.get("http://localhost:8081/api/audio/test-ffmpeg", timeout=10) + if response.status_code == 200: + result = response.json() + available = result.get('available', False) + print(f"FFmpeg状态: {'✅ 可用' if available else '❌ 不可用'}") + return available + else: + print(f"⚠️ FFmpeg测试请求失败,状态码: {response.status_code}") + return False + except Exception as e: + print(f"❌ 检查FFmpeg可用性时出错: {e}") + return False + +def check_python_environment(): + """检查Python环境""" + print("\n🔍 检查Python环境...") + try: + response = requests.get("http://localhost:8081/api/audio/python-info", timeout=10) + if response.status_code == 200: + info = response.text + print("✅ Python环境信息:") + print(info) + return True + else: + print(f"⚠️ Python信息请求失败,状态码: {response.status_code}") + return False + except Exception as e: + print(f"❌ 检查Python环境时出错: {e}") + return False + +def test_audio_separation(): + """测试音频分离功能""" + print("\n🧪 测试音频分离功能...") + + # 查找测试音频文件 + test_files = [ + "test_audio.wav", + "test_audio.mp3", + "李荣浩 - 不将就.mp3", + "林俊杰 - 江南.mp3", + "林俊杰 - 江南.wav" + ] + + test_audio_path = None + for file in test_files: + if os.path.exists(file): + test_audio_path = file + break + + if not test_audio_path: + print("❌ 未找到可用的测试音频文件") + print("💡 请确保以下文件之一存在:") + for file in test_files: + print(f" - {file}") + return False + + print(f"📁 使用测试文件: {test_audio_path}") + print(f"📊 文件大小: {os.path.getsize(test_audio_path) / 1024 / 1024:.2f} MB") + + try: + with open(test_audio_path, 'rb') as audio_file: + files = {'audioFile': (os.path.basename(test_audio_path), audio_file, 'audio/mpeg')} + + print("📤 上传音频文件进行分离...") + response = requests.post("http://localhost:8081/api/audio/separate", files=files, timeout=120) + + if response.status_code == 200: + result = response.json() + print("✅ 音频分离请求成功") + + if result.get('success'): + print("🎯 分离结果详情:") + print(f" ✅ 成功: {result['success']}") + print(f" 🎤 人声文件: {result.get('vocalsPath', 'N/A')}") + print(f" 🎵 伴奏文件: {result.get('accompanimentPath', 'N/A')}") + print(f" 💬 消息: {result.get('message', 'N/A')}") + + # 检查文件是否存在 + vocals_path = result.get('vocalsPath') + accompaniment_path = result.get('accompanimentPath') + + if vocals_path and os.path.exists(vocals_path): + vocals_size = os.path.getsize(vocals_path) + print(f" ✅ 人声文件存在,大小: {vocals_size / 1024 / 1024:.2f} MB") + if vocals_size > 1024: # 大于1KB + print(" 🎉 人声文件大小正常") + else: + print(" ⚠️ 人声文件可能过小") + else: + print(" ❌ 人声文件不存在") + + if accompaniment_path and os.path.exists(accompaniment_path): + accomp_size = os.path.getsize(accompaniment_path) + print(f" ✅ 伴奏文件存在,大小: {accomp_size / 1024 / 1024:.2f} MB") + if accomp_size > 1024: # 大于1KB + print(" 🎉 伴奏文件大小正常") + else: + print(" ⚠️ 伴奏文件可能过小") + else: + print(" ❌ 伴奏文件不存在") + + return True + else: + print("❌ 分离失败") + print(f"错误信息: {result.get('message', '未知错误')}") + return False + + else: + print(f"❌ 请求失败,状态码: {response.status_code}") + print(f"响应内容: {response.text}") + return False + + except requests.exceptions.Timeout: + print("❌ 请求超时,音频分离可能耗时过长") + return False + except Exception as e: + print(f"❌ 测试音频分离时出错: {e}") + return False + +def check_direct_spleeter(): + """直接检查Spleeter命令行""" + print("\n🔧 直接检查Spleeter命令行...") + + # 测试Python命令 + python_commands = ["python", "python3", "py"] + working_python = None + + for cmd in python_commands: + try: + result = subprocess.run([cmd, "--version"], capture_output=True, text=True, timeout=10) + if result.returncode == 0: + working_python = cmd + print(f"✅ {cmd} 可用: {result.stdout.strip()}") + break + except: + print(f"❌ {cmd} 不可用") + + if not working_python: + print("❌ 未找到可用的Python命令") + return False + + # 测试Spleeter导入 + try: + result = subprocess.run([working_python, "-c", "import spleeter; print('Spleeter导入成功')"], + capture_output=True, text=True, timeout=30) + if result.returncode == 0: + print("✅ Spleeter导入成功") + return True + else: + print(f"❌ Spleeter导入失败: {result.stderr}") + return False + except Exception as e: + print(f"❌ 测试Spleeter导入时出错: {e}") + return False + +def check_ffmpeg_direct(): + """直接检查FFmpeg命令行""" + print("\n🔧 直接检查FFmpeg命令行...") + + ffmpeg_commands = ["ffmpeg", "ffmpeg.exe"] + working_ffmpeg = None + + for cmd in ffmpeg_commands: + try: + result = subprocess.run([cmd, "-version"], capture_output=True, text=True, timeout=10) + if result.returncode == 0: + working_ffmpeg = cmd + # 提取版本信息 + lines = result.stdout.split('\n') + if lines: + version_line = lines[0] + print(f"✅ {cmd} 可用: {version_line}") + break + except: + print(f"❌ {cmd} 不可用") + + if working_ffmpeg: + return True + else: + print("❌ 未找到可用的FFmpeg命令") + return False + +def main(): + print("=" * 60) + print("🎵 音频分离服务诊断工具") + print("=" * 60) + + # 检查服务器状态 + if not check_server_status(): + print("\n💡 服务器未启动,请运行以下命令:") + print("cd f:\\traeprojects\\DeAudio\\project\\src\\daudio") + print(".\\mvnw spring-boot:run") + return + + # 检查组件可用性 + print("\n" + "-" * 40) + print("🔧 检查依赖组件") + print("-" * 40) + + spleeter_available = check_spleeter_availability() + ffmpeg_available = check_ffmpeg_availability() + python_ok = check_python_environment() + + # 直接命令行检查 + print("\n" + "-" * 40) + print("🔧 直接命令行检查") + print("-" * 40) + + spleeter_direct = check_direct_spleeter() + ffmpeg_direct = check_ffmpeg_direct() + + # 测试音频分离 + print("\n" + "-" * 40) + print("🧪 功能测试") + print("-" * 40) + + separation_ok = test_audio_separation() + + # 诊断总结 + print("\n" + "=" * 60) + print("📊 诊断总结") + print("=" * 60) + + issues = [] + + if not spleeter_available: + issues.append("❌ Spleeter在服务中不可用") + if not ffmpeg_available: + issues.append("❌ FFmpeg在服务中不可用") + if not separation_ok: + issues.append("❌ 音频分离功能异常") + + if not spleeter_direct: + issues.append("❌ 命令行Spleeter不可用") + if not ffmpeg_direct: + issues.append("❌ 命令行FFmpeg不可用") + + if issues: + print("发现以下问题:") + for issue in issues: + print(f" {issue}") + + print("\n💡 解决方案建议:") + if not spleeter_direct: + print(" 1. 安装Spleeter: pip install spleeter") + if not ffmpeg_direct: + print(" 2. 安装FFmpeg并添加到PATH环境变量") + if spleeter_direct and not spleeter_available: + print(" 3. 检查Java服务中的Python路径配置") + else: + print("✅ 所有检查项正常,音频分离服务应该可以正常工作") + + print("\n" + "=" * 60) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/daudio/spleeter_monitor.py b/src/daudio/spleeter_monitor.py new file mode 100644 index 0000000..28af3c2 --- /dev/null +++ b/src/daudio/spleeter_monitor.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +""" +Spleeter服务监控脚本 +用于监控Spleeter服务的健康状态,并在服务异常时自动重启 +""" + +import requests +import time +import subprocess +import os +import sys +import logging +from datetime import datetime + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('spleeter_monitor.log'), + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + +class SpleeterMonitor: + def __init__(self): + self.service_url = "http://localhost:5000/api/health" + self.service_process = None + self.max_retries = 5 + self.retry_delay = 10 # 秒 + self.health_check_interval = 30 # 秒 + self.startup_timeout = 60 # 秒 + + # Spleeter服务配置 + self.spleeter_dir = os.path.dirname(os.path.abspath(__file__)) + self.spleeter_script = "spleeter_service.py" + + def is_service_healthy(self): + """检查Spleeter服务是否健康""" + try: + response = requests.get(self.service_url, timeout=5) + if response.status_code == 200: + data = response.json() + if data.get('status') == 'UP': + return True + return False + except requests.exceptions.RequestException as e: + logger.debug(f"服务健康检查失败: {e}") + return False + + def start_service(self): + """启动Spleeter服务""" + try: + logger.info("正在启动Spleeter服务...") + + # 构建启动命令 + cmd = ["python", self.spleeter_script] + + # 启动服务进程 + self.service_process = subprocess.Popen( + cmd, + cwd=self.spleeter_dir, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True + ) + + logger.info(f"Spleeter服务已启动,PID: {self.service_process.pid}") + + # 等待服务启动完成 + start_time = time.time() + while time.time() - start_time < self.startup_timeout: + if self.is_service_healthy(): + logger.info("Spleeter服务启动成功") + return True + time.sleep(2) + + logger.error("Spleeter服务启动超时") + return False + + except Exception as e: + logger.error(f"启动Spleeter服务失败: {e}") + return False + + def stop_service(self): + """停止Spleeter服务""" + if self.service_process and self.service_process.poll() is None: + try: + logger.info("正在停止Spleeter服务...") + self.service_process.terminate() + + # 等待进程结束 + try: + self.service_process.wait(timeout=10) + except subprocess.TimeoutExpired: + logger.warning("服务正常终止超时,强制终止...") + self.service_process.kill() + self.service_process.wait() + + logger.info("Spleeter服务已停止") + return True + + except Exception as e: + logger.error(f"停止Spleeter服务失败: {e}") + return False + return True + + def restart_service(self): + """重启Spleeter服务""" + logger.info("正在重启Spleeter服务...") + + # 先停止服务 + self.stop_service() + + # 等待一段时间确保服务完全停止 + time.sleep(3) + + # 重新启动服务 + return self.start_service() + + def monitor_service(self): + """监控Spleeter服务""" + consecutive_failures = 0 + + while True: + try: + # 检查服务健康状态 + if self.is_service_healthy(): + consecutive_failures = 0 + logger.debug("服务健康检查通过") + else: + consecutive_failures += 1 + logger.warning(f"服务健康检查失败 (连续失败次数: {consecutive_failures})") + + # 如果连续失败次数达到阈值,重启服务 + if consecutive_failures >= 3: + logger.error("服务连续健康检查失败,尝试重启...") + if self.restart_service(): + consecutive_failures = 0 + logger.info("服务重启成功") + else: + logger.error("服务重启失败") + + # 等待下一次检查 + time.sleep(self.health_check_interval) + + except KeyboardInterrupt: + logger.info("收到中断信号,正在停止监控...") + break + except Exception as e: + logger.error(f"监控过程中发生错误: {e}") + time.sleep(self.health_check_interval) + + def run(self): + """运行监控器""" + logger.info("=== Spleeter服务监控器启动 ===") + logger.info(f"监控目录: {self.spleeter_dir}") + logger.info(f"健康检查间隔: {self.health_check_interval}秒") + + # 检查服务是否已运行 + if self.is_service_healthy(): + logger.info("检测到Spleeter服务已在运行") + else: + logger.info("未检测到Spleeter服务,尝试启动...") + if not self.start_service(): + logger.error("Spleeter服务启动失败,监控器退出") + return + + # 开始监控 + try: + self.monitor_service() + except KeyboardInterrupt: + logger.info("监控器被用户中断") + finally: + # 清理资源 + self.stop_service() + logger.info("=== Spleeter服务监控器停止 ===") + +def main(): + """主函数""" + monitor = SpleeterMonitor() + + # 解析命令行参数 + if len(sys.argv) > 1: + if sys.argv[1] == "start": + monitor.start_service() + elif sys.argv[1] == "stop": + monitor.stop_service() + elif sys.argv[1] == "restart": + monitor.restart_service() + elif sys.argv[1] == "status": + if monitor.is_service_healthy(): + print("Spleeter服务状态: 运行中") + else: + print("Spleeter服务状态: 停止") + else: + print("用法: python spleeter_monitor.py [start|stop|restart|status|monitor]") + else: + # 默认运行监控模式 + monitor.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/daudio/spleeter_service.py b/src/daudio/spleeter_service.py new file mode 100644 index 0000000..68700cf --- /dev/null +++ b/src/daudio/spleeter_service.py @@ -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//', 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/', 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) \ No newline at end of file diff --git a/src/daudio/src/test/java/com/cauc/deaudio/service/AudioSeparationServiceTest.java b/src/daudio/src/test/java/com/cauc/deaudio/service/AudioSeparationServiceTest.java new file mode 100644 index 0000000..2f31d8c --- /dev/null +++ b/src/daudio/src/test/java/com/cauc/deaudio/service/AudioSeparationServiceTest.java @@ -0,0 +1,40 @@ +package com.cauc.deaudio.service; + +import com.cauc.deaudio.model.AudioSeparationResult; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +public class AudioSeparationServiceTest { + + @Test + public void testSeparateVocalsWithModelParameter() { + // 创建一个模拟的音频文件 + MultipartFile mockFile = new MockMultipartFile( + "audio", + "test.mp3", + "audio/mpeg", + "test audio content".getBytes() + ); + + // 创建服务实例 + AudioSeparationService service = new AudioSeparationService(); + + // 测试调用带有model参数的方法 + // 这个测试可能会失败,因为Spleeter不可用,但至少可以验证方法调用不会因为参数不匹配而失败 + try { + AudioSeparationResult result = service.separateVocals(mockFile, "2stems"); + // 如果Spleeter不可用,结果应该是失败的 + assertEquals(false, result.isSuccess()); + } catch (Exception e) { + // 如果有异常,检查是否是参数不匹配的异常 + if (e.getMessage().contains("no suitable method found for separateVocals")) { + throw new AssertionError("方法参数不匹配问题未解决", e); + } + } + } +} \ No newline at end of file diff --git a/src/daudio/start_services.bat b/src/daudio/start_services.bat new file mode 100644 index 0000000..bc467fc --- /dev/null +++ b/src/daudio/start_services.bat @@ -0,0 +1,88 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo DeAudio 服务启动脚本 +echo ======================================== +echo. + +REM 检查Python是否可用 +python --version >nul 2>&1 +if errorlevel 1 ( + echo [错误] 未找到Python,请确保Python已安装并添加到PATH + pause + exit /b 1 +) + +echo [信息] 检测到Python环境 +echo. + +REM 检查Spleeter服务是否已在运行 +echo [信息] 检查Spleeter服务状态... +netstat -ano | findstr ":5000" | findstr "LISTENING" >nul +if errorlevel 1 ( + echo [信息] Spleeter服务未运行,正在启动... + + REM 启动Spleeter服务 + start "Spleeter Service" python spleeter_service.py + + REM 等待服务启动 + echo [信息] 等待Spleeter服务启动... + timeout /t 5 /nobreak >nul + + REM 检查服务是否成功启动 + python -c "import requests; r = requests.get('http://localhost:5000/api/health', timeout=5); print('[成功] Spleeter服务已启动' if r.status_code == 200 else '[警告] Spleeter服务启动异常')" 2>nul + if errorlevel 1 ( + echo [警告] Spleeter服务健康检查失败,但进程已启动 + ) +) else ( + echo [信息] Spleeter服务已在运行 +) + +echo. + +REM 检查Spring Boot应用是否已在运行 +echo [信息] 检查DeAudio应用状态... +netstat -ano | findstr ":8081" | findstr "LISTENING" >nul +if errorlevel 1 ( + echo [信息] DeAudio应用未运行,正在启动... + + REM 切换到Spring Boot应用目录并启动 + cd src\daudio + + REM 检查Maven Wrapper是否存在 + if exist mvnw ( + echo [信息] 使用Maven Wrapper启动Spring Boot应用... + start "DeAudio Application" cmd /k "mvnw spring-boot:run" + ) else ( + echo [错误] 未找到mvnw文件,请确保Maven Wrapper存在 + pause + exit /b 1 + ) + + REM 等待应用启动 + echo [信息] 等待DeAudio应用启动... + timeout /t 10 /nobreak >nul + + REM 检查应用是否成功启动 + curl -s -o nul -w "%%{http_code}" http://localhost:8081/ >nul + if errorlevel 1 ( + echo [警告] DeAudio应用启动检查失败,但进程已启动 + ) else ( + echo [成功] DeAudio应用已启动 + ) + + cd ..\.. +) else ( + echo [信息] DeAudio应用已在运行 +) + +echo. +echo ======================================== +echo [完成] 服务启动完成 +echo. +echo 访问地址: +echo - DeAudio网页界面: http://localhost:8081/audio-processing.html +echo - Spleeter API服务: http://localhost:5000/api/health +echo. +echo 按任意键退出... +pause >nul \ No newline at end of file diff --git a/src/daudio/test_audio.mp3 b/src/daudio/test_audio.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..648359df8a7e7ebe5f803fa2d9f7b1f3dbd2e6e9 GIT binary patch literal 40559 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 J7ync5_#a{Qqvrqs literal 0 HcmV?d00001 diff --git a/src/daudio/test_ffmpeg_enhancement.py b/src/daudio/test_ffmpeg_enhancement.py new file mode 100644 index 0000000..e67395f --- /dev/null +++ b/src/daudio/test_ffmpeg_enhancement.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +测试FFmpeg音频分离结果优化功能 +""" + +import requests +import json +import time +import os + +def test_ffmpeg_enhancement(): + """测试FFmpeg后处理优化功能""" + + # 服务器地址 + base_url = "http://localhost:8081" + + # 测试音频文件路径(使用一个较小的音频文件进行测试) + test_audio_path = "test_audio.mp3" + + # 检查测试文件是否存在 + if not os.path.exists(test_audio_path): + print(f"❌ 测试音频文件不存在: {test_audio_path}") + print("请准备一个测试音频文件或使用现有的音频文件") + return + + print("🎵 开始测试FFmpeg音频分离结果优化功能") + print(f"📁 测试文件: {test_audio_path}") + print(f"📊 文件大小: {os.path.getsize(test_audio_path) / 1024 / 1024:.2f} MB") + + # 上传音频文件进行分离 + print("\n📤 上传音频文件进行分离...") + + try: + with open(test_audio_path, 'rb') as audio_file: + files = {'audioFile': (os.path.basename(test_audio_path), audio_file, 'audio/mpeg')} + + start_time = time.time() + response = requests.post(f"{base_url}/api/audio/separate", files=files) + end_time = time.time() + + if response.status_code == 200: + result = response.json() + print("✅ 音频分离请求成功") + print(f"⏱️ 处理时间: {end_time - start_time:.2f} 秒") + + # 检查分离结果 + if result.get('success'): + print("\n🎯 分离结果详情:") + print(f"✅ 成功: {result['success']}") + print(f"🎤 人声文件: {result.get('vocalsPath', 'N/A')}") + print(f"🎵 伴奏文件: {result.get('accompanimentPath', 'N/A')}") + print(f"💬 消息: {result.get('message', 'N/A')}") + + # 检查是否包含FFmpeg优化信息 + if "FFmpeg" in result.get('message', ''): + print("\n🎉 FFmpeg后处理优化已成功应用!") + else: + print("\n⚠️ 未检测到FFmpeg优化信息,可能使用了标准处理流程") + + # 检查文件是否存在 + vocals_path = result.get('vocalsPath') + accompaniment_path = result.get('accompanimentPath') + + if vocals_path and os.path.exists(vocals_path): + print(f"✅ 人声文件存在,大小: {os.path.getsize(vocals_path) / 1024 / 1024:.2f} MB") + else: + print("❌ 人声文件不存在") + + if accompaniment_path and os.path.exists(accompaniment_path): + print(f"✅ 伴奏文件存在,大小: {os.path.getsize(accompaniment_path) / 1024 / 1024:.2f} MB") + else: + print("❌ 伴奏文件不存在") + + else: + print("❌ 分离失败") + print(f"错误信息: {result.get('message', '未知错误')}") + + else: + print(f"❌ 请求失败,状态码: {response.status_code}") + print(f"响应内容: {response.text}") + + except requests.exceptions.ConnectionError: + print("❌ 无法连接到服务器,请确保Spring Boot应用正在运行") + print("💡 运行命令: cd f:\\traeprojects\\DeAudio\\project\\src\\daudio && .\\mvnw spring-boot:run") + except Exception as e: + print(f"❌ 测试过程中出现错误: {e}") + +def test_server_status(): + """测试服务器状态""" + print("🔍 检查服务器状态...") + + try: + response = requests.get("http://localhost:8081/api/audio/status", timeout=5) + if response.status_code == 200: + print("✅ 服务器正常运行") + return True + else: + print(f"⚠️ 服务器响应异常,状态码: {response.status_code}") + return False + except requests.exceptions.ConnectionError: + print("❌ 服务器未启动或无法连接") + return False + except Exception as e: + print(f"❌ 检查服务器状态时出错: {e}") + return False + +def test_ffmpeg_availability(): + """测试FFmpeg可用性""" + print("🔍 检查FFmpeg可用性...") + + try: + response = requests.get("http://localhost:8081/api/audio/test-ffmpeg", timeout=10) + if response.status_code == 200: + result = response.json() + print(f"FFmpeg状态: {'✅ 可用' if result.get('available') else '❌ 不可用'}") + return result.get('available', False) + else: + print(f"⚠️ FFmpeg测试请求失败,状态码: {response.status_code}") + return False + except Exception as e: + print(f"❌ 测试FFmpeg可用性时出错: {e}") + return False + +def test_spleeter_availability(): + """测试Spleeter可用性""" + print("🔍 检查Spleeter可用性...") + + try: + response = requests.get("http://localhost:8081/api/audio/test-spleeter", timeout=10) + if response.status_code == 200: + result = response.json() + print(f"Spleeter状态: {'✅ 可用' if result.get('available') else '❌ 不可用'}") + return result.get('available', False) + else: + print(f"⚠️ Spleeter测试请求失败,状态码: {response.status_code}") + return False + except Exception as e: + print(f"❌ 测试Spleeter可用性时出错: {e}") + return False + +if __name__ == "__main__": + print("=" * 60) + print("🎵 FFmpeg音频分离结果优化功能测试") + print("=" * 60) + + # 检查服务器状态 + if not test_server_status(): + print("\n💡 请先启动服务器:") + print("cd f:\\traeprojects\\DeAudio\\project\\src\\daudio") + print(".\\mvnw spring-boot:run") + exit(1) + + # 测试依赖组件可用性 + print("\n" + "-" * 40) + print("🔧 检查依赖组件") + print("-" * 40) + + ffmpeg_available = test_ffmpeg_availability() + spleeter_available = test_spleeter_availability() + + if not ffmpeg_available: + print("\n⚠️ FFmpeg不可用,优化功能将无法正常工作") + print("💡 请确保FFmpeg已正确安装并配置在系统PATH中") + + if not spleeter_available: + print("\n⚠️ Spleeter不可用,音频分离功能将无法正常工作") + print("💡 请确保Python环境已正确安装spleeter包") + + # 执行FFmpeg优化测试 + print("\n" + "-" * 40) + print("🧪 执行FFmpeg优化测试") + print("-" * 40) + + test_ffmpeg_enhancement() + + print("\n" + "=" * 60) + print("🎉 测试完成") + print("=" * 60) \ No newline at end of file