package com.fuyuanshen.app.service; import com.fuyuanshen.equipment.utils.AlibabaTTSUtil; import com.fuyuanshen.equipment.utils.AudioProcessUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.Arrays; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; /** * 音频处理服务 */ @Slf4j @Service @RequiredArgsConstructor public class AudioProcessService { // 配置参数 private static final int MAX_AUDIO_SIZE = 5 * 1024 * 1024; // 5MB private static final List SUPPORTED_FORMATS = Arrays.asList( ".wav", ".mp3", ".aac", ".flac", ".m4a", ".ogg" ); private final AudioProcessUtil audioProcessUtil; private final AlibabaTTSUtil alibabaTTSUtil; /** * 处理上传的音频文件 */ public List processAudio(MultipartFile file) { // 1. 参数校验 validateAudioFile(file); File tempFile = null; try { // 2. 创建临时文件 tempFile = createTempAudioFile(file); // 3. 转码为标准PCM-WAV格式 byte[] pcmData = audioProcessUtil.convertToStandardWav(tempFile); log.info("音频处理成功,输出数据大小: {} bytes", pcmData.length); // 获取音频信息 // String audioInfo = audioProcessUtil.getAudioInfo(pcmData); // log.info("音频处理成功,音频信息: {}", audioInfo); // // // 保存测试文件(用于验证) // String savedPath = audioProcessUtil.saveWavToFile(pcmData, "test_output.wav"); // if (savedPath != null) { // log.info("测试文件已保存: {}", savedPath); // } // 将byte[]转换为16进制字符串列表 List hexList = audioProcessUtil.bytesToHexList(pcmData); log.info("音频处理完成,原始数据大小: {} bytes, 16进制数据长度: {}", pcmData.length, hexList.size()); return hexList; } catch (Exception e) { log.error("音频处理失败", e); throw new RuntimeException("音频处理失败", e); } finally { // 4. 清理临时文件 deleteTempFile(tempFile); } } /** * 生成标准PCM数据(单声道,16K采样率,16bit深度,包含44字节WAV头) * 数据总大小不超过2MB,如果超过将抛出异常 * @param text 要转换的文本内容 * @return 标准PCM数据字节数组(WAV格式) * @throws IOException 处理失败时抛出 * @throws IllegalArgumentException 如果生成的数据超过2MB */ public List generateStandardPcmData(String text) throws IOException { // 参数校验 if (text == null || text.trim().isEmpty()) { throw new IllegalArgumentException("文本内容不能为空"); } if (text.length() > 100) { throw new IllegalArgumentException("文本长度超过限制(最大100字符)"); } log.info("输入文本长度: {}", text.length()); try { byte[] rawPcmData = alibabaTTSUtil.generateStandardPcmData(text); // 使用AudioProcessUtil转换成带头44字节 PCM byte[] pcmData = audioProcessUtil.rawPcmToStandardWav(rawPcmData); // String savedPath = audioProcessUtil.saveWavToFile(pcmData, "test_output.wav"); // if (savedPath != null) { // log.info("测试文件已保存: {}", savedPath); // } // 保存WAV文件到本地 String savedPath = saveByteArrayToFile(pcmData, "tts_output.wav"); if (savedPath != null) { log.info("WAV文件已保存: {}", savedPath); } // 将byte[]转换为16进制字符串列表 List hexList = audioProcessUtil.bytesToHexList(pcmData); log.info("generateStandardPcmData音频处理完成,原始数据大小: {} bytes, 16进制数据长度: {}", pcmData.length, hexList.size()); return hexList; } finally { // 4. 清理临时文件 } } public String saveWavFileLocally(String text, String filename) throws IOException { // 参数校验 if (text == null || text.trim().isEmpty()) { throw new IllegalArgumentException("文本内容不能为空"); } if (filename == null || filename.trim().isEmpty()) { filename = "tts_output.wav"; // 默认文件名 } try { // 生成PCM数据 byte[] rawPcmData = alibabaTTSUtil.generateStandardPcmData(text); // 转换为标准WAV格式(添加44字节头部) byte[] wavData = audioProcessUtil.rawPcmToStandardWav(rawPcmData); // 保存到本地文件 String filePath = saveByteArrayToFile(wavData, filename); log.info("WAV文件已保存: {}", filePath); return filePath; } catch (Exception e) { log.error("保存WAV文件失败: {}", e.getMessage(), e); throw new IOException("保存WAV文件失败", e); } } private String saveByteArrayToFile(byte[] data, String filename) throws IOException { // 确定保存路径(可以是临时目录或指定目录) String directory = System.getProperty("java.io.tmpdir"); // 使用系统临时目录 File dir = new File(directory); if (!dir.exists()) { dir.mkdirs(); } // 创建完整文件路径 File file = new File(dir, filename); // 写入文件 try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(data); } return file.getAbsolutePath(); } /** * 验证音频文件 */ private void validateAudioFile(MultipartFile file) { if (file == null || file.isEmpty()) { throw new IllegalArgumentException("上传文件不能为空"); } if (!isAudioFile(file.getOriginalFilename())) { throw new IllegalArgumentException("只允许上传音频文件"); } if (file.getSize() > MAX_AUDIO_SIZE) { throw new IllegalArgumentException("音频大小不能超过5MB"); } } /** * 判断是否是支持的音频格式 */ private boolean isAudioFile(String filename) { if (filename == null || filename.lastIndexOf('.') == -1) { return false; } String ext = filename.substring(filename.lastIndexOf('.')).toLowerCase(); return SUPPORTED_FORMATS.contains(ext); } /** * 创建临时音频文件 */ private File createTempAudioFile(MultipartFile file) throws IOException { String originalFilename = file.getOriginalFilename(); String extension = ""; if (originalFilename != null && originalFilename.contains(".")) { extension = originalFilename.substring(originalFilename.lastIndexOf(".")); } File tempFile = File.createTempFile("audio-", extension); file.transferTo(tempFile); log.debug("创建临时音频文件: {}", tempFile.getAbsolutePath()); return tempFile; } /** * 删除临时文件 */ private void deleteTempFile(File file) { if (file != null && file.exists()) { if (file.delete()) { log.debug("删除临时文件成功: {}", file.getAbsolutePath()); } else { log.warn("无法删除临时文件: {}", file.getAbsolutePath()); } } } /** * 提取文本 */ public String extract(MultipartFile file) throws Exception { String name = file.getOriginalFilename(); if (name == null || (!name.endsWith(".txt") && !name.endsWith(".docx"))) { throw new IllegalArgumentException("仅支持 .txt 或 .docx"); } if (file.getSize() > MAX_AUDIO_SIZE) { throw new IllegalArgumentException("文件超过5MB"); } String text; /* 全程流式,不落地磁盘,不一次性读字节数组 */ try (InputStream in = file.getInputStream()) { if (name.endsWith(".txt")) { text = readTxt(in); } else { text = readDocx(in); } } return text; } /* ---------- txt:按行读,StringBuilder 复用 ---------- */ private String readTxt(InputStream in) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(4096); String line; while ((line = br.readLine()) != null) { sb.append(line).append('\n'); } return sb.toString(); } /* ---------- docx:ZipInputStream 只扫 document.xml ---------- */ private String readDocx(InputStream in) throws IOException { ZipInputStream zin = new ZipInputStream(in); ZipEntry e; while ((e = zin.getNextEntry()) != null) { if ("word/document.xml".equals(e.getName())) { return staxExtract(zin); // 流式读 XML } } return ""; } /* ---------- StAX 流式提取 ---------- */ private String staxExtract(InputStream xml) throws IOException { XMLStreamReader r = null; StringBuilder sb = new StringBuilder(4096); try { //System.out.println(new String(xml.readAllBytes())); r = XMLInputFactory.newInstance().createXMLStreamReader(xml); while (r.hasNext()) { if (r.next() == XMLStreamConstants.START_ELEMENT && "t".equals(r.getLocalName())) { String elementText = r.getElementText(); sb.append(elementText); } } } catch (XMLStreamException ex) { throw new IOException(ex); } finally { if (r != null) try { r.close(); } catch (XMLStreamException ignore) {} } return sb.toString(); } }