|
|
|
|
@ -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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|