package com.fuyuanshen.app.service; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.fuyuanshen.app.domain.AppBusinessFile; import com.fuyuanshen.app.domain.bo.AppBusinessFileBo; import com.fuyuanshen.app.domain.dto.AppAudioFileDto; import com.fuyuanshen.app.domain.dto.AppFileRenameDto; import com.fuyuanshen.app.domain.vo.AppFileVo; import com.fuyuanshen.app.http.HttpTtsClient; import com.fuyuanshen.app.mapper.AppBusinessFileMapper; import com.fuyuanshen.common.core.domain.R; import com.fuyuanshen.common.satoken.utils.AppLoginHelper; import com.fuyuanshen.common.satoken.utils.LoginHelper; import com.fuyuanshen.equipment.utils.AlibabaTTSUtil; import com.fuyuanshen.equipment.utils.AudioProcessUtil; import com.fuyuanshen.equipment.utils.FileHashUtil; import com.fuyuanshen.equipment.utils.Mp3Duration; import com.fuyuanshen.global.mqtt.utils.FfmpegVolumeUtil; import com.fuyuanshen.global.mqtt.utils.FfmpegVolumeUtil3; import com.fuyuanshen.system.domain.vo.SysOssVo; import com.fuyuanshen.system.service.ISysOssService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; 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.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.UUID; 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; private final FileHashUtil fileHashUtil; private final ISysOssService ossService; private final IAppBusinessFileService appBusinessFileService; private final AppBusinessFileMapper appBusinessFileMapper; // String accessKeyId = "LTAI5t66moCkhNC32TDJ5ReP"; // String accessKeySecret = "2F3sdoBJ08bYvJcuDgSkLnJwGXsvYH"; // String appKey = "lbGuq5K5bEH4uxmT"; @Value("${alibaba.tts.appKey}") private String appKey; @Value("${alibaba.tts.akId}") private String accessKeyId; @Value("${alibaba.tts.akSecret}") private String accessKeySecret; /** * 处理上传的音频文件 */ 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 String saveByteArrayToFile(InputStream inputStream, 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); InputStream is = inputStream) { byte[] buffer = new byte[8192]; // 8KB缓冲区 int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } 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(); } public String uploadAudioToOss(AppAudioFileDto bo) { MultipartFile file = bo.getFile(); // 校验文件格式和大小 validateAudioFileForRestful(file); // 上传文件 // SysOssVo upload = sysOssService.upload(file); String filename = file.getOriginalFilename(); String savedPath = null; try { String fileHash = fileHashUtil.hash(file); SysOssVo upload = ossService.updateHash(file, fileHash); // 强制将HTTP替换为HTTPS if (upload.getUrl() != null && upload.getUrl().startsWith("http://")) { upload.setUrl(upload.getUrl().replaceFirst("^http://", "https://")); } String fileSuffix = filename.substring(filename.lastIndexOf('.')).toLowerCase(); AppBusinessFileBo appBusinessFileBo = new AppBusinessFileBo(); appBusinessFileBo.setFileId(upload.getOssId()); appBusinessFileBo.setBusinessId(bo.getDeviceId()); appBusinessFileBo.setFileType(3L); appBusinessFileBo.setCreateBy(AppLoginHelper.getUserId()); savedPath = saveByteArrayToFile(file.getInputStream(), generateRandomFileName(fileSuffix)); if (savedPath != null) { log.info("MP3文件已保存: {}", savedPath); Integer mp3Duration = Mp3Duration.getMp3Duration(savedPath); log.info("MP3文件时长: {} 秒", mp3Duration); appBusinessFileBo.setDuration(mp3Duration); } appBusinessFileService.insertByBo(appBusinessFileBo); if (upload != null) { return upload.getUrl(); } } catch (Exception e){ log.error("上传音频文件失败", e); }finally { log.info("删除临时文件: {}", savedPath); if(savedPath != null){ deleteTempFile(new File(savedPath)); } } return null; } /** * 校验音频文件格式 */ private void validateAudioFileForRestful(MultipartFile file) { if (file == null || file.isEmpty()) { throw new IllegalArgumentException("上传文件不能为空"); } String originalFilename = file.getOriginalFilename(); if (originalFilename == null) { throw new IllegalArgumentException("文件名不能为空"); } List SUPPORTED_FORMATS = Arrays.asList( ".wav", ".mp3", ".pcm" ); String ext = originalFilename.substring(originalFilename.lastIndexOf('.')).toLowerCase(); // 检查文件扩展名 if (!SUPPORTED_FORMATS.contains(ext)) { throw new IllegalArgumentException("只允许上传MP3、WAV、PCM格式的音频文件"); } // 检查文件大小 if (file.getSize() > MAX_AUDIO_SIZE) { throw new IllegalArgumentException("音频大小不能超过5MB"); } } /** * 判断文件是否为支持的音频格式 */ private boolean isSupportedFormat(String filename) { if (filename == null || filename.lastIndexOf('.') == -1) { return false; } String ext = filename.substring(filename.lastIndexOf('.')).toLowerCase(); return SUPPORTED_FORMATS.contains(ext); } public String textToSpeech(Long deviceId,String text, String fileSuffix) { //支持PCM/WAV/MP3格式 if (fileSuffix == null || fileSuffix.isEmpty()) { fileSuffix = "mp3"; } fileSuffix = fileSuffix.toLowerCase(); List SUPPORTED_FORMATS = Arrays.asList( "wav", "mp3", "pcm" ); boolean contains = SUPPORTED_FORMATS.contains(fileSuffix); if (!contains) { throw new IllegalArgumentException("不支持的音频格式"); } // String accessKeyId = "LTAI5t66moCkhNC32TDJ5ReP"; // String accessKeySecret = "2F3sdoBJ08bYvJcuDgSkLnJwGXsvYH"; // String appKey = "lbGuq5K5bEH4uxmT"; String savedPath = null; String savedMp3VolumePath = null; try { // 使用HTTP方式调用 HttpTtsClient httpClient = new HttpTtsClient(accessKeyId, accessKeySecret, appKey); // byte[] mp3Data = httpClient.synthesizeTextToMp3(text,fileSuffix); // byte[] mp3Data = alibabaTTSUtil.synthesizeTextToMp3(text,fileSuffix); AppBusinessFileBo appBusinessFileBo = new AppBusinessFileBo(); savedPath = saveByteArrayToFile(mp3Data, generateRandomFileName(fileSuffix)); Integer mp3Duration = getMp3Duration2(savedPath); appBusinessFileBo.setDuration(mp3Duration); String directory = System.getProperty("java.io.tmpdir"); // 使用系统临时目录 String fileName = generateRandomFileName(fileSuffix); savedMp3VolumePath = directory + "/" + fileName; log.info("保存MP3文件: {}", savedMp3VolumePath); FfmpegVolumeUtil.increaseMp3Volume(savedPath, savedMp3VolumePath, 12); File file = new File(savedMp3VolumePath); String fileHash = fileHashUtil.getFileHash(file,"SHA-256"); SysOssVo upload = ossService.updateHash(file, fileHash); // 强制将HTTP替换为HTTPS if (upload.getUrl() != null && upload.getUrl().startsWith("http://")) { upload.setUrl(upload.getUrl().replaceFirst("^http://", "https://")); } appBusinessFileBo.setFileId(upload.getOssId()); appBusinessFileBo.setBusinessId(deviceId); appBusinessFileBo.setFileType(3L); appBusinessFileBo.setCreateBy(AppLoginHelper.getUserId()); appBusinessFileService.insertByBo(appBusinessFileBo); if (upload != null) { return upload.getUrl(); } } catch (Exception e){ log.error("上传音频文件失败", e); } finally { log.info("删除savedPath临时文件: {}", savedPath); if(savedPath != null){ deleteTempFile(new File(savedPath)); } if(savedMp3VolumePath != null){ log.info("删除savedMp3VolumePath临时文件: {}", savedMp3VolumePath); deleteTempFile(new File(savedMp3VolumePath)); } } return null; } private Integer getMp3Duration2(String savedPath) { if (savedPath != null) { log.info("MP3文件已保存: {}", savedPath); Integer mp3Duration = Mp3Duration.getMp3Duration(savedPath); log.info("MP3文件时长: {} 秒", mp3Duration); return mp3Duration; } return 0; } private static final Random random = new Random(); private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); /** * 生成纯随机文件名(无原文件名依赖) */ public static String generateRandomFileName(String extension) { String timestamp = LocalDateTime.now().format(formatter); int randomNum = random.nextInt(10000); String uuidPart = UUID.randomUUID().toString().substring(0, 8); if (!extension.startsWith(".")) { extension = "." + extension; } return timestamp + "_" + String.format("%04d", randomNum) + "_" + uuidPart + extension; } public List queryAudioFileList(Long deviceId) { if(deviceId == null){ return null; } Long userId = LoginHelper.getUserId(); AppBusinessFileBo bo = new AppBusinessFileBo(); bo.setBusinessId(deviceId); bo.setCreateBy(userId); bo.setFileType(3L); return appBusinessFileService.queryAppFileList(bo); } public R deleteAudioFile(Long fileId,Long deviceId) { UpdateWrapper updateWrapper = new UpdateWrapper<>(); updateWrapper.eq("file_id",fileId); updateWrapper.eq("business_id",deviceId); appBusinessFileMapper.delete(updateWrapper); return R.ok(); } public R renameAudioFile(AppFileRenameDto bo) { UpdateWrapper updateWrapper = new UpdateWrapper<>(); updateWrapper.eq("file_id",bo.getFileId()); updateWrapper.eq("business_id",bo.getDeviceId()); updateWrapper.set("re_name",bo.getFileName()); appBusinessFileMapper.update(updateWrapper); return R.ok(); } }