分离实现

Signed-off-by: wuhaobin <2307299049@qq.com>
main
wuhaobin 6 months ago
parent 18ee039b96
commit 5f11e5cb52

@ -109,13 +109,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- ✅ 新增:配置属性绑定 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>

@ -63,18 +63,26 @@ public class AudioSeparationController {
logger.info("Start processing uploaded file: {}, size: {} bytes, model: {}",
file.getOriginalFilename(), file.getSize(), model);
// <EFBFBD><EFBFBD><EFBFBD>÷<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
AudioSeparationResult result = audioSeparationService.separateVocals(file);
// 调用分离服务(根据模型参数)
AudioSeparationResult result = audioSeparationService.separateVocals(file, model);
if (result.isSuccess()) {
response.put("success", true);
response.put("message", result.getMessage());
response.put("data", Map.of(
"vocalsPath", result.getVocalsPath(),
"accompanimentPath", result.getAccompanimentPath(),
"vocalsDownloadUrl", "/api/audio/download/vocals?path=" + result.getVocalsPath(),
"accompanimentDownloadUrl", "/api/audio/download/accompaniment?path=" + result.getAccompanimentPath()
));
// 构建响应数据,包含原始文件名
Map<String, Object> data = new HashMap<>();
data.put("vocalsPath", result.getVocalsPath());
data.put("accompanimentPath", result.getAccompanimentPath());
data.put("vocalsDownloadUrl", "/api/audio/download/vocals?path=" + java.net.URLEncoder.encode(result.getVocalsPath(), "UTF-8"));
data.put("accompanimentDownloadUrl", "/api/audio/download/accompaniment?path=" + java.net.URLEncoder.encode(result.getAccompanimentPath(), "UTF-8"));
// 添加原始文件名到响应中
if (result.getOriginalFileName() != null) {
data.put("originalFileName", result.getOriginalFileName());
}
response.put("data", data);
return ResponseEntity.ok(response);
} else {
response.put("success", false);
@ -96,39 +104,77 @@ public class AudioSeparationController {
@GetMapping("/download/{type}")
public ResponseEntity<Resource> downloadAudio(
@PathVariable String type,
@RequestParam String path) {
@RequestParam String path,
@RequestParam(required = false) String originalFileName) {
try {
File file = new File(path);
// 解码路径参数
String decodedPath = java.net.URLDecoder.decode(path, "UTF-8");
File file = new File(decodedPath);
if (!file.exists()) {
logger.error("File not found: {}", decodedPath);
return ResponseEntity.notFound().build();
}
// determine file name and content type
String filename = file.getName();
String contentType = Files.probeContentType(file.toPath());
if (contentType == null) {
contentType = "audio/wav";
// 获取文件名 - 优先使用原始文件名,如果没有则使用生成的文件名
String filename;
if (originalFileName != null && !originalFileName.trim().isEmpty()) {
// 使用原始文件名,但根据文件类型添加后缀
String fileExtension = getFileExtension(file.getName());
String baseName = getBaseName(originalFileName);
filename = baseName + "_" + type + fileExtension;
} else {
// 使用生成的文件名
filename = file.getName();
}
// 处理中文文件名编码 - 使用更兼容的方式
String encodedFilename = java.net.URLEncoder.encode(filename, "UTF-8").replace("+", "%20");
// 构建Content-Disposition头兼容各种浏览器
String contentDisposition = "attachment; filename=\"" + encodedFilename + "\"";
InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + filename + "\"")
.contentType(MediaType.parseMediaType(contentType))
.contentLength(file.length())
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length()))
.header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION)
.body(resource);
} catch (FileNotFoundException e) {
logger.error("File not found: {}", path);
logger.error("File not found: {}", e.getMessage());
return ResponseEntity.notFound().build();
} catch (Exception e) {
logger.error("Error serving file: {}", e.getMessage());
logger.error("Error downloading file: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
*
*/
private String getFileExtension(String filename) {
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex > 0) {
return filename.substring(lastDotIndex);
}
return ".wav"; // 默认扩展名
}
/**
*
*/
private String getBaseName(String filename) {
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex > 0) {
return filename.substring(0, lastDotIndex);
}
return filename;
}
/**
* Debug endpoint: test whether backend can invoke Spleeter (python)
*/
@ -148,6 +194,25 @@ public class AudioSeparationController {
}
}
/**
* Debug endpoint: test whether backend can invoke FFmpeg
*/
@GetMapping("/test-ffmpeg")
public ResponseEntity<Map<String, Object>> testFFmpeg() {
Map<String, Object> resp = new HashMap<>();
try {
boolean ok = audioSeparationService.testFFmpeg();
resp.put("success", ok);
resp.put("message", ok ? "FFmpeg is available" : "FFmpeg is NOT available");
return ResponseEntity.ok(resp);
} catch (Exception e) {
logger.error("Error testing FFmpeg: {}", e.getMessage(), e);
resp.put("success", false);
resp.put("message", "Error testing FFmpeg: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(resp);
}
}
/**
* Get progress for a separation task (placeholder)
*/
@ -171,23 +236,12 @@ public class AudioSeparationController {
response.put("service", "Audio Separation Service");
response.put("timestamp", System.currentTimeMillis());
// Check external dependencies
try {
// Check FFmpeg availability
Process process = Runtime.getRuntime().exec("ffmpeg -version");
int ffmpegStatus = process.waitFor();
response.put("ffmpeg", ffmpegStatus == 0 ? "AVAILABLE" : "UNAVAILABLE");
// Check Spleeter (python) availability
Process pythonProcess = Runtime.getRuntime().exec("py -3.8 -c \"import spleeter\"");
int pythonStatus = pythonProcess.waitFor();
response.put("spleeter", pythonStatus == 0 ? "AVAILABLE" : "UNAVAILABLE");
} catch (Exception e) {
response.put("ffmpeg", "UNAVAILABLE");
response.put("spleeter", "UNAVAILABLE");
response.put("error", e.getMessage());
}
// Check external dependencies using service methods
boolean ffmpegAvailable = audioSeparationService.testFFmpeg();
boolean spleeterAvailable = audioSeparationService.testSpleeter();
response.put("ffmpeg", ffmpegAvailable ? "AVAILABLE" : "UNAVAILABLE");
response.put("spleeter", spleeterAvailable ? "AVAILABLE" : "UNAVAILABLE");
return ResponseEntity.ok(response);
}
@ -203,7 +257,12 @@ public class AudioSeparationController {
"separate", "POST /api/audio/separate",
"download", "GET /api/audio/download/{type}",
"health", "GET /api/audio/health",
"test", "GET /api/audio/test"
"test", "GET /api/audio/test",
"test-spleeter", "GET /api/audio/test-spleeter",
"test-ffmpeg", "GET /api/audio/test-ffmpeg",
"cut", "POST /api/audio/cut",
"merge", "POST /api/audio/merge",
"system-info", "GET /api/audio/system-info"
));
return ResponseEntity.ok(response);
}

@ -13,7 +13,7 @@ public class TestController {
@GetMapping("/test/python")
public String testPython() {
return audioService.getPythonInfo();
return "Python直接调用方式已移除现在使用HTTP API进行音频分离";
}
@GetMapping("/test/spleeter")

@ -6,6 +6,7 @@ public class AudioSeparationResult {
private String accompanimentPath;
private String message;
private String taskId;
private String originalFileName;
// default constructor
public AudioSeparationResult() {}
@ -18,6 +19,15 @@ public class AudioSeparationResult {
this.message = message;
}
// parameterized constructor with original filename
public AudioSeparationResult(boolean success, String vocalsPath, String accompanimentPath, String message, String originalFileName) {
this.success = success;
this.vocalsPath = vocalsPath;
this.accompanimentPath = accompanimentPath;
this.message = message;
this.originalFileName = originalFileName;
}
// Getters and Setters
public boolean isSuccess() {
return success;
@ -59,6 +69,14 @@ public class AudioSeparationResult {
this.taskId = taskId;
}
public String getOriginalFileName() {
return originalFileName;
}
public void setOriginalFileName(String originalFileName) {
this.originalFileName = originalFileName;
}
@Override
public String toString() {
return "AudioSeparationResult{" +
@ -67,6 +85,7 @@ public class AudioSeparationResult {
", accompanimentPath='" + accompanimentPath + '\'' +
", message='" + message + '\'' +
", taskId='" + taskId + '\'' +
", originalFileName='" + originalFileName + '\'' +
'}';
}
}

@ -6,6 +6,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.BufferedReader;
import java.io.File;
@ -17,8 +21,11 @@ import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@Service
public class AudioSeparationService {
@ -28,18 +35,19 @@ public class AudioSeparationService {
@Value("${app.upload.dir:uploads}")
private String uploadDir;
private static final String OUTPUT_DIR = "separated";
private static final String OUTPUT_DIR = "F:\\traeprojects\\DeAudio\\project\\separated";
@Value("${app.python.commands:python}")
private String pythonCommandsProperty;
@Value("${app.temp.dir:temp}")
private String tempDir;
// 缓存解析后的可用 python 命令token 列表)
private volatile List<String> cachedPythonCommand = null;
@Value("${app.ffmpeg.command:ffmpeg}")
private String ffmpegCommand;
public AudioSeparationResult separateVocals(MultipartFile audioFile) {
return separateVocals(audioFile, "2stems");
}
public AudioSeparationResult separateVocals(MultipartFile audioFile, String model) {
String sessionId = UUID.randomUUID().toString();
try {
@ -49,15 +57,29 @@ public class AudioSeparationService {
// save uploaded file
String originalFileName = audioFile.getOriginalFilename();
String fileExtension = getFileExtension(originalFileName);
// 处理中文文件名使用sessionId作为文件名避免中文路径问题
String inputFileName = sessionId + fileExtension;
Path inputPath = Paths.get(uploadDir, inputFileName);
Files.write(inputPath, audioFile.getBytes());
logger.info("File uploaded: {}", inputPath);
logger.info("Original filename: {}", originalFileName);
logger.info("File size: {} bytes", audioFile.getSize());
logger.info("Using model: {}", model);
// 检查文件大小,对大文件进行特殊处理
long fileSize = audioFile.getSize();
if (fileSize > 100 * 1024 * 1024) { // 超过100MB的文件
logger.warn("Large audio file detected ({} MB), applying optimization for large files", fileSize / (1024 * 1024));
return separateLargeAudioFile(inputPath.toString(), sessionId, model, originalFileName);
}
// 使用FFmpeg预处理音频文件确保格式兼容
String processedFilePath = preprocessAudioWithFFmpeg(inputPath.toString(), sessionId);
// use Spleeter to perform separation
return separateWithSpleeter(inputPath.toString(), sessionId);
return separateWithSpleeter(processedFilePath, sessionId, model, originalFileName);
} catch (Exception e) {
logger.error("Audio separation failed: {}", e.getMessage(), e);
@ -65,28 +87,40 @@ public class AudioSeparationService {
}
}
private AudioSeparationResult separateWithSpleeter(String inputPath, String sessionId) {
/**
* 使FFmpeg
*/
private String preprocessAudioWithFFmpeg(String inputPath, String sessionId) {
try {
String outputDir = Paths.get(OUTPUT_DIR, sessionId).toString();
// 创建临时目录
Path tempDirPath = Paths.get(tempDir, sessionId);
Files.createDirectories(tempDirPath);
// 使用解析/检测到的 python 命令前缀来构建命令
List<String> pythonCmd = resolvePythonCommand();
logger.info("Using Python command: {}", String.join(" ", pythonCmd));
// 输出文件路径转换为WAV格式44.1kHz, 立体声)
String outputFileName = sessionId + "_processed.wav";
Path outputPath = tempDirPath.resolve(outputFileName);
// 构建FFmpeg命令
List<String> command = new ArrayList<>();
command.add(ffmpegCommand);
command.add("-i"); // 输入文件
command.add(inputPath);
command.add("-ar"); // 设置采样率
command.add("44100");
command.add("-ac"); // 设置声道数
command.add("2");
command.add("-acodec"); // 音频编码器
command.add("pcm_s16le");
command.add("-y"); // 覆盖输出文件
command.add(outputPath.toString());
logger.info("Executing FFmpeg command: {}", String.join(" ", command));
List<String> command = new ArrayList<>(pythonCmd);
command.addAll(Arrays.asList("-m", "spleeter", "separate", "-i", inputPath,
"-o", outputDir, "-p", "spleeter:2stems", "-f", "{filename}_{instrument}.{codec}"));
logger.info("Executing Spleeter command: {}", String.join(" ", command));
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
// 设置工作目录为当前目录
pb.directory(new File("."));
Process process = pb.start();
// 读取子进程输出
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), Charset.defaultCharset()));
@ -94,88 +128,110 @@ public class AudioSeparationService {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
logger.info("[Spleeter] {}", line);
logger.info("[FFmpeg] {}", line);
}
// 等待进程完成(最多 300 秒,因为大文件需要更长时间)
boolean finished = process.waitFor(300, TimeUnit.SECONDS);
// 等待进程完成
boolean finished = process.waitFor(60, TimeUnit.SECONDS);
int exitCode = finished ? process.exitValue() : -1;
String outputStr = output.toString();
logger.info("Spleeter process finished with exit code: {}", exitCode);
// If command failed and looks like spleeter expected a positional FILE argument,
// try an alternate invocation that passes the input file as a positional argument
if (exitCode != 0 && outputStr != null && (outputStr.contains("Missing argument") || outputStr.contains("Usage: main_.py separate"))) {
logger.warn("Spleeter initial invocation failed with message suggesting missing FILE argument. Trying alternate invocation format.");
List<String> alt = new ArrayList<>(pythonCmd);
alt.addAll(Arrays.asList("-m", "spleeter", "separate", "-o", outputDir, "-p", "spleeter:2stems", "-f", "{filename}_{instrument}.{codec}", inputPath));
logger.info("Executing alternate Spleeter command: {}", String.join(" ", alt));
ProcessBuilder pb2 = new ProcessBuilder(alt);
pb2.redirectErrorStream(true);
pb2.directory(new File("."));
Process p2 = pb2.start();
BufferedReader reader2 = new BufferedReader(new InputStreamReader(p2.getInputStream(), Charset.defaultCharset()));
StringBuilder output2 = new StringBuilder();
String line2;
while ((line2 = reader2.readLine()) != null) {
output2.append(line2).append("\n");
logger.info("[Spleeter-alt] {}", line2);
}
boolean finished2 = p2.waitFor(300, TimeUnit.SECONDS);
int exitCode2 = finished2 ? p2.exitValue() : -1;
logger.info("Alternate Spleeter process finished with exit code: {}", exitCode2);
if (exitCode != 0) {
logger.error("FFmpeg preprocessing failed with exit code: {}. Output: {}", exitCode, output.toString());
// 如果FFmpeg处理失败返回原始文件路径
return inputPath;
}
// 验证输出文件
if (Files.exists(outputPath) && outputPath.toFile().length() > 0) {
logger.info("FFmpeg preprocessing succeeded. Output file: {}", outputPath);
return outputPath.toString();
} else {
logger.error("FFmpeg output file is invalid. Using original file.");
return inputPath;
}
} catch (Exception e) {
logger.error("FFmpeg preprocessing error: {}", e.getMessage(), e);
// 如果预处理失败,返回原始文件路径
return inputPath;
}
}
private AudioSeparationResult separateWithSpleeter(String inputPath, String sessionId, String model, String originalFileName) {
try {
logger.info("开始通过HTTP API调用音频分离服务");
// 创建HTTP客户端
RestTemplate restTemplate = new RestTemplate();
// 构建HTTP请求
String url = "http://localhost:5000/api/separate";
// 创建多部分请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// 添加文件
File inputFile = new File(inputPath);
org.springframework.core.io.Resource fileResource = new org.springframework.core.io.FileSystemResource(inputFile);
body.add("file", fileResource);
// 添加参数
body.add("model", model);
body.add("enhance", "true");
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
logger.info("发送HTTP请求到音频分离服务: {}", url);
// 发送请求
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
String.class
);
if (response.getStatusCode() == HttpStatus.OK) {
String responseBody = response.getBody();
logger.info("音频分离服务响应: {}", responseBody);
// 解析JSON响应
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> jsonResponse = objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
if (exitCode2 == 0) {
String baseName = getBaseName(new File(inputPath).getName());
Path vocalsPath = Paths.get(outputDir, baseName, "vocals.wav");
Path accompanimentPath = Paths.get(outputDir, baseName, "accompaniment.wav");
if ((Boolean) jsonResponse.get("success")) {
String taskId = (String) jsonResponse.get("task_id");
String vocalsFile = (String) jsonResponse.get("vocals_file");
String accompanimentFile = (String) jsonResponse.get("accompaniment_file");
// 调试输出目录
debugOutputDirectory(outputDir, baseName);
// 构建本地文件路径
String outputDir = Paths.get(OUTPUT_DIR, taskId).toString();
Path vocalsPath = Paths.get(outputDir, vocalsFile);
Path accompanimentPath = Paths.get(outputDir, accompanimentFile);
logger.info("音频分离成功: 人声文件={}, 伴奏文件={}", vocalsPath, accompanimentPath);
// 添加文件验证
if (verifyAudioFile(vocalsPath.toFile()) && verifyAudioFile(accompanimentPath.toFile())) {
logger.info("Alternate separation succeeded: vocals={}, accompaniment={}", vocalsPath, accompanimentPath);
return new AudioSeparationResult(true, vocalsPath.toString(), accompanimentPath.toString(), "Separation succeeded (alternate)");
} else {
logger.error("Alternate invocation: output files invalid. Output:\n{}", output2.toString());
return new AudioSeparationResult(false, null, null, "Spleeter failed (alternate): Generated files are invalid");
}
} else {
logger.error("Alternate Spleeter invocation failed, exit={}. Output:\n{}", exitCode2, output2.toString());
return new AudioSeparationResult(false, null, null, "Spleeter failed (alternate): " + output2.toString());
}
}
if (exitCode == 0) {
// locate output files
String baseName = getBaseName(new File(inputPath).getName());
Path vocalsPath = Paths.get(outputDir, baseName, "vocals.wav");
Path accompanimentPath = Paths.get(outputDir, baseName, "accompaniment.wav");
// 调试输出目录
debugOutputDirectory(outputDir, baseName);
// 添加文件验证
if (verifyAudioFile(vocalsPath.toFile()) && verifyAudioFile(accompanimentPath.toFile())) {
logger.info("Separation succeeded: vocals={}, accompaniment={}", vocalsPath, accompanimentPath);
return new AudioSeparationResult(true, vocalsPath.toString(),
accompanimentPath.toString(), "Separation succeeded");
accompanimentPath.toString(), "音频分离成功", originalFileName);
} else {
logger.error("Output files invalid - exists: vocals={}, accompaniment={}, sizes: vocals={} bytes, accompaniment={} bytes",
Files.exists(vocalsPath), Files.exists(accompanimentPath),
vocalsPath.toFile().length(), accompanimentPath.toFile().length());
return new AudioSeparationResult(false, null, null, "Output files are invalid");
String errorMessage = (String) jsonResponse.get("error");
logger.error("音频分离服务返回错误: {}", errorMessage);
return new AudioSeparationResult(false, null, null, "音频分离失败: " + errorMessage);
}
} else {
logger.error("Spleeter execution failed, exit={}. Output:\n{}", exitCode, output.toString());
return new AudioSeparationResult(false, null, null, "Spleeter failed: " + output.toString());
logger.error("HTTP请求失败状态码: {}", response.getStatusCode());
return new AudioSeparationResult(false, null, null, "HTTP请求失败: " + response.getStatusCode());
}
} catch (Exception e) {
logger.error("Spleeter execution error: {}", e.getMessage(), e);
return new AudioSeparationResult(false, null, null, "Spleeter execution error: " + e.getMessage());
logger.error("HTTP API调用失败: {}", e.getMessage(), e);
// HTTP API调用失败直接返回错误结果
logger.info("HTTP API调用失败音频分离服务不可用");
return new AudioSeparationResult(false, null, null, "音频分离服务不可用: " + e.getMessage());
}
}
@ -202,6 +258,280 @@ public class AudioSeparationService {
return true;
}
/**
*
*/
private AudioSeparationResult separateLargeAudioFile(String inputPath, String sessionId, String model, String originalFileName) {
try {
logger.info("Starting large audio file separation via HTTP API for: {}", originalFileName);
// 创建HTTP客户端
RestTemplate restTemplate = new RestTemplate();
// 构建HTTP请求
String url = "http://localhost:5000/api/separate";
// 创建多部分请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// 添加文件
File inputFile = new File(inputPath);
org.springframework.core.io.Resource fileResource = new org.springframework.core.io.FileSystemResource(inputFile);
body.add("file", fileResource);
// 添加参数
body.add("model", model);
body.add("enhance", "true");
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
logger.info("发送HTTP请求到音频分离服务处理大文件: {}", url);
// 发送请求
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
String.class
);
if (response.getStatusCode() == HttpStatus.OK) {
String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> jsonResponse = objectMapper.readValue(responseBody, new TypeReference<Map<String, Object>>() {});
boolean success = Boolean.TRUE.equals(jsonResponse.get("success"));
String vocalsPath = (String) jsonResponse.get("vocalsPath");
String accompanimentPath = (String) jsonResponse.get("accompanimentPath");
String message = (String) jsonResponse.get("message");
if (success && vocalsPath != null && accompanimentPath != null) {
logger.info("Large audio separation via HTTP API completed successfully");
return new AudioSeparationResult(true, vocalsPath, accompanimentPath, message, originalFileName);
} else {
logger.error("Large audio separation via HTTP API failed: {}", message);
return new AudioSeparationResult(false, null, null, message);
}
} else {
logger.error("HTTP API请求失败状态码: {}", response.getStatusCode());
return new AudioSeparationResult(false, null, null, "HTTP API request failed with status: " + response.getStatusCode());
}
} catch (Exception e) {
logger.error("Large audio separation via HTTP API failed: {}", e.getMessage(), e);
return new AudioSeparationResult(false, null, null, "Large audio separation failed: " + e.getMessage());
}
}
/**
*
*/
private String preprocessLargeAudioWithFFmpeg(String inputPath, String sessionId) {
try {
// 创建临时目录
Path tempDirPath = Paths.get(tempDir, sessionId);
Files.createDirectories(tempDirPath);
// 输出文件路径转换为WAV格式降低采样率以减小文件大小
String outputFileName = sessionId + "_processed_large.wav";
Path outputPath = tempDirPath.resolve(outputFileName);
// 构建优化的FFmpeg命令降低采样率到22.05kHz,单声道)
List<String> command = new ArrayList<>();
command.add(ffmpegCommand);
command.add("-i"); // 输入文件
command.add(inputPath);
command.add("-ar"); // 设置较低的采样率
command.add("22050");
command.add("-ac"); // 设置为单声道
command.add("1");
command.add("-acodec"); // 音频编码器
command.add("pcm_s16le");
command.add("-y"); // 覆盖输出文件
command.add(outputPath.toString());
logger.info("Executing optimized FFmpeg command for large file: {}", String.join(" ", command));
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
Process process = pb.start();
// 读取子进程输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
logger.info("[FFmpeg Large] {}", line);
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("FFmpeg preprocessing failed with exit code: " + exitCode);
}
logger.info("Large audio preprocessing completed: {}", outputPath);
return outputPath.toString();
} catch (Exception e) {
logger.error("Large audio preprocessing failed: {}", e.getMessage(), e);
throw new RuntimeException("Large audio preprocessing failed", e);
}
}
/**
* 使FFmpeg
*/
private AudioSeparationResult enhanceSeparationResults(String vocalsPath, String accompanimentPath, String sessionId, String originalFileName) {
try {
logger.info("开始FFmpeg后处理优化...");
// 创建优化后的文件路径
String baseName = getBaseName(originalFileName);
String enhancedVocalsPath = vocalsPath.replace(".wav", "_enhanced.wav");
String enhancedAccompanimentPath = accompanimentPath.replace(".wav", "_enhanced.wav");
// 优化人声文件
if (!enhanceVocalsWithFFmpeg(vocalsPath, enhancedVocalsPath)) {
logger.warn("人声优化失败,使用原始文件");
enhancedVocalsPath = vocalsPath;
}
// 优化伴奏文件
if (!enhanceAccompanimentWithFFmpeg(accompanimentPath, enhancedAccompanimentPath)) {
logger.warn("伴奏优化失败,使用原始文件");
enhancedAccompanimentPath = accompanimentPath;
}
logger.info("FFmpeg后处理优化完成");
logger.info("优化后人声文件: {}", enhancedVocalsPath);
logger.info("优化后伴奏文件: {}", enhancedAccompanimentPath);
return new AudioSeparationResult(true, enhancedVocalsPath, enhancedAccompanimentPath, "Audio separation completed with FFmpeg enhancement");
} catch (Exception e) {
logger.error("FFmpeg后处理优化失败: {}", e.getMessage(), e);
return null; // 返回null表示优化失败使用原始文件
}
}
/**
* 使FFmpeg
*/
private boolean enhanceVocalsWithFFmpeg(String inputPath, String outputPath) {
try {
// 构建FFmpeg优化命令
List<String> command = new ArrayList<>();
command.add(ffmpegCommand);
command.add("-i");
command.add(inputPath);
// 人声优化参数:
// 1. 高通滤波器去除低频噪音 (highpass=f=100)
// 2. 压缩器增强人声清晰度 (acompressor=threshold=0.1:ratio=20:attack=50:release=300)
// 3. 均衡器提升中频 (equalizer=f=1000:width_type=h:width=200:g=5)
// 4. 噪声抑制 (afftdn=nf=-25)
// 5. 限制器防止削波 (alimiter=level_in=1:level_out=0.8)
command.addAll(Arrays.asList(
"-af", "highpass=f=100,acompressor=threshold=0.1:ratio=20:attack=50:release=300,equalizer=f=1000:width_type=h:width=200:g=5,afftdn=nf=-25,alimiter=level_in=1:level_out=0.8",
"-ar", "44100",
"-ac", "1", // 人声通常更适合单声道
"-acodec", "pcm_s16le",
"-y"
));
command.add(outputPath);
logger.info("执行人声优化命令: {}", String.join(" ", command));
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
Process process = pb.start();
// 读取输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
logger.debug("[FFmpeg Vocals] {}", line);
}
boolean finished = process.waitFor(120, TimeUnit.SECONDS);
int exitCode = finished ? process.exitValue() : -1;
if (exitCode == 0 && new File(outputPath).exists()) {
logger.info("人声优化成功");
return true;
} else {
logger.error("人声优化失败,退出码: {}", exitCode);
return false;
}
} catch (Exception e) {
logger.error("人声优化出错: {}", e.getMessage(), e);
return false;
}
}
/**
* 使FFmpeg
*/
private boolean enhanceAccompanimentWithFFmpeg(String inputPath, String outputPath) {
try {
// 构建FFmpeg优化命令
List<String> command = new ArrayList<>();
command.add(ffmpegCommand);
command.add("-i");
command.add(inputPath);
// 伴奏优化参数:
// 1. 低通滤波器去除高频噪音 (lowpass=f=8000)
// 2. 压缩器平衡动态范围 (acompressor=threshold=0.05:ratio=10:attack=100:release=500)
// 3. 均衡器提升低频 (equalizer=f=200:width_type=h:width=100:g=3)
// 4. 立体声增强 (stereowiden=level_in=0.5:level_out=0.8)
command.addAll(Arrays.asList(
"-af", "lowpass=f=8000,acompressor=threshold=0.05:ratio=10:attack=100:release=500,equalizer=f=200:width_type=h:width=100:g=3,stereowiden=level_in=0.5:level_out=0.8",
"-ar", "44100",
"-ac", "2", // 伴奏保持立体声
"-acodec", "pcm_s16le",
"-y"
));
command.add(outputPath);
logger.info("执行伴奏优化命令: {}", String.join(" ", command));
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
Process process = pb.start();
// 读取输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
logger.debug("[FFmpeg Accompaniment] {}", line);
}
boolean finished = process.waitFor(120, TimeUnit.SECONDS);
int exitCode = finished ? process.exitValue() : -1;
if (exitCode == 0 && new File(outputPath).exists()) {
logger.info("伴奏优化成功");
return true;
} else {
logger.error("伴奏优化失败,退出码: {}", exitCode);
return false;
}
} catch (Exception e) {
logger.error("伴奏优化出错: {}", e.getMessage(), e);
return false;
}
}
/**
*
*/
@ -282,71 +612,60 @@ public class AudioSeparationService {
int dotIndex = filename.lastIndexOf(".");
return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
}
private boolean containsSpecialCharacters(String str) {
if (str == null) return false;
// 检查是否包含Windows路径不允许的字符
String invalidChars = "[<>:\"|?*]+";
return str.matches(".*" + invalidChars + ".*");
}
/**
* python token
*/
private List<String> resolvePythonCommand() {
if (cachedPythonCommand != null) return cachedPythonCommand;
synchronized (this) {
if (cachedPythonCommand != null) return cachedPythonCommand;
String[] candidates = pythonCommandsProperty.split(",");
for (String cand : candidates) {
String trimmed = cand.trim();
if (trimmed.isEmpty()) continue;
List<String> tokens = Arrays.asList(trimmed.split("\\s+"));
logger.info("Testing Python candidate: {}", trimmed);
/**
* Test whether Spleeter is available via HTTP API
*/
public boolean testSpleeter() {
try {
logger.info("Testing Spleeter availability via HTTP API...");
// 创建HTTP客户端
RestTemplate restTemplate = new RestTemplate();
// 调用健康检查接口
String url = "http://localhost:5000/api/health";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> jsonResponse = objectMapper.readValue(responseBody, Map.class);
List<String> probe = new ArrayList<>(tokens);
probe.addAll(Arrays.asList("-c", "import spleeter; print('Spleeter available')"));
try {
ProcessBuilder pb = new ProcessBuilder(probe);
pb.redirectErrorStream(true);
pb.directory(new File("."));
Process p = pb.start();
// 读取输出
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line);
}
boolean finished = p.waitFor(10, TimeUnit.SECONDS);
int code = finished ? p.exitValue() : -1;
if (code == 0) {
cachedPythonCommand = tokens;
logger.info("✅ Python command detected and working: {}", String.join(" ", tokens));
return cachedPythonCommand;
} else {
logger.warn("❌ Python candidate '{}' not usable (exit={}, output={})", trimmed, code, output.toString());
}
} catch (Exception ex) {
logger.warn("❌ Python candidate '{}' failed: {}", trimmed, ex.getMessage());
}
boolean spleeterAvailable = "AVAILABLE".equals(jsonResponse.get("spleeter"));
logger.info("Spleeter test result via HTTP API: {}", spleeterAvailable ? "AVAILABLE" : "NOT AVAILABLE");
return spleeterAvailable;
} else {
logger.warn("HTTP API健康检查失败状态码: {}", response.getStatusCode());
return false;
}
// 兜底:使用第一个候选
String first = candidates.length > 0 ? candidates[0].trim() : "python";
cachedPythonCommand = Arrays.asList(first.split("\\s+"));
logger.error("🚨 No working Python candidate detected! Falling back to: {}", String.join(" ", cachedPythonCommand));
return cachedPythonCommand;
} catch (Exception e) {
logger.warn("HTTP API测试失败: {}", e.getMessage());
return false;
}
}
/**
* Test whether Spleeter is available
* Test whether FFmpeg is available
*/
public boolean testSpleeter() {
public boolean testFFmpeg() {
try {
logger.info("Testing Spleeter availability...");
List<String> pythonCmd = resolvePythonCommand();
List<String> cmd = new ArrayList<>(pythonCmd);
cmd.addAll(Arrays.asList("-c", "import spleeter; print('Spleeter available'); print(spleeter.__version__)"));
logger.info("Testing FFmpeg availability...");
List<String> cmd = Arrays.asList(ffmpegCommand, "-version");
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
@ -356,18 +675,22 @@ public class AudioSeparationService {
// 读取输出
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
boolean versionFound = false;
while ((line = reader.readLine()) != null) {
logger.info("[Spleeter Test] {}", line);
logger.info("[FFmpeg Test] {}", line);
if (line.contains("ffmpeg version")) {
versionFound = true;
}
}
boolean finished = process.waitFor(15, TimeUnit.SECONDS);
int exitCode = finished ? process.exitValue() : -1;
boolean available = exitCode == 0;
logger.info("Spleeter test result: {}", available ? "AVAILABLE" : "NOT AVAILABLE");
boolean available = exitCode == 0 && versionFound;
logger.info("FFmpeg test result: {}", available ? "AVAILABLE" : "NOT AVAILABLE");
return available;
} catch (Exception e) {
logger.error("Test Spleeter failed: {}", e.getMessage(), e);
logger.error("Test FFmpeg failed: {}", e.getMessage(), e);
return false;
}
}
@ -379,31 +702,7 @@ public class AudioSeparationService {
return "completed";
}
/**
* Python
*/
public String getPythonInfo() {
try {
List<String> pythonCmd = resolvePythonCommand();
List<String> cmd = new ArrayList<>(pythonCmd);
cmd.addAll(Arrays.asList("-c", "import sys; print('Python version:', sys.version); print('Python executable:', sys.executable)"));
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
pb.directory(new File("."));
Process process = pb.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
process.waitFor(10, TimeUnit.SECONDS);
return output.toString();
} catch (Exception e) {
return "Error getting Python info: " + e.getMessage();
}
}
}

@ -3,7 +3,7 @@ server.port=8081
app.upload.dir=uploads
# 修复Python路径 - 使用正斜杠或双反斜杠
app.python.commands=C:/Users/32708/AppData/Local/Programs/Python/Python39/python.exe
app.python.commands=C:\Python39\python.exe
# 或者使用转义的反斜杠
# app.python.commands=C:\\Users\\32708\\AppData\\Local\\Programs\\Python\\Python39\\python.exe

Loading…
Cancel
Save