forked from dyf/fys-Multi-tenant
feat(equipment): 添加阿里巴巴TTS语音合成工具类
- 实现文本转语音功能,支持多种声音、语速、音量等参数调节 - 集成阿里云TTS服务,支持访问令牌自动刷新与缓存 - 提供HTTP客户端配置与请求处理逻辑 - 支持生成标准PCM数据及WAV格式音频文件 - 实现音频文件保存与错误处理机制 - 添加参数校验与日志记录功能 - 集成Redis缓存管理访问令牌- 支持URL编码与请求构建逻辑 - 实现响应处理与音频数据写入文件功能 - 添加静默删除临时文件与错误响应处理机制
This commit is contained in:
@ -0,0 +1,194 @@
|
||||
package com.fuyuanshen.web.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||
import org.bytedeco.javacv.Frame;
|
||||
import org.bytedeco.javacv.Java2DFrameUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 视频处理工具类
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class VideoProcessUtil {
|
||||
|
||||
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
|
||||
|
||||
/**
|
||||
* 创建临时视频文件
|
||||
*/
|
||||
public File createTempVideoFile(MultipartFile file) throws Exception {
|
||||
File tempFile = Files.createTempFile("upload-", ".mp4").toFile();
|
||||
file.transferTo(tempFile);
|
||||
log.debug("创建临时视频文件: {}", tempFile.getAbsolutePath());
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理视频并转换为Hex字符串列表
|
||||
*/
|
||||
public List<String> processVideoToHex(File videoFile, int frameRate, int duration, int width, int height) throws Exception {
|
||||
// 1. 提取视频帧
|
||||
List<BufferedImage> frames = extractFramesFromVideo(videoFile, frameRate, duration, width, height);
|
||||
|
||||
// 2. 转换为RGB565格式
|
||||
byte[] binaryData = convertFramesToRGB565(frames);
|
||||
|
||||
// 3. 转换为Hex字符串列表
|
||||
return bytesToHexList(binaryData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从视频中提取帧
|
||||
*/
|
||||
private List<BufferedImage> extractFramesFromVideo(File videoFile, int frameRate, int duration, int width, int height) throws Exception {
|
||||
List<BufferedImage> frames = new ArrayList<>();
|
||||
int totalFramesToExtract = frameRate * duration;
|
||||
|
||||
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(videoFile)) {
|
||||
grabber.start();
|
||||
|
||||
long totalFramesInVideo = grabber.getLengthInFrames();
|
||||
int fps = (int) Math.round(grabber.getFrameRate());
|
||||
if (fps <= 0) fps = 30;
|
||||
|
||||
double durationSeconds = (double) totalFramesInVideo / fps;
|
||||
if (durationSeconds < duration) {
|
||||
throw new IllegalArgumentException("视频太短,至少需要 " + duration + " 秒");
|
||||
}
|
||||
|
||||
double frameInterval = (double) totalFramesInVideo / totalFramesToExtract;
|
||||
|
||||
for (int i = 0; i < totalFramesToExtract; i++) {
|
||||
int targetFrameNumber = (int) Math.round(i * frameInterval);
|
||||
|
||||
if (targetFrameNumber >= totalFramesInVideo) {
|
||||
throw new IllegalArgumentException("目标帧超出范围: " + targetFrameNumber);
|
||||
}
|
||||
|
||||
grabber.setFrameNumber(targetFrameNumber);
|
||||
Frame frame = grabber.grab();
|
||||
|
||||
if (frame != null && frame.image != null) {
|
||||
BufferedImage bufferedImage = Java2DFrameUtils.toBufferedImage(frame);
|
||||
frames.add(cropImage(bufferedImage, width, height));
|
||||
} else {
|
||||
throw new IllegalArgumentException("无法获取第 " + targetFrameNumber + "帧");
|
||||
}
|
||||
}
|
||||
|
||||
grabber.stop();
|
||||
}
|
||||
|
||||
log.debug("从视频中提取了 {} 帧", frames.size());
|
||||
return frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所有帧转换为 RGB565 格式字节数组
|
||||
*/
|
||||
private byte[] convertFramesToRGB565(List<BufferedImage> frames) throws Exception {
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
for (BufferedImage image : frames) {
|
||||
byte[] rgb565Bytes = convertToRGB565(image);
|
||||
byteArrayOutputStream.write(rgb565Bytes);
|
||||
}
|
||||
|
||||
byte[] result = byteArrayOutputStream.toByteArray();
|
||||
log.debug("转换RGB565数据完成,总字节数: {}", result.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字节数组转换为Hex字符串列表
|
||||
*/
|
||||
private List<String> bytesToHexList(byte[] bytes) {
|
||||
List<String> hexList = new ArrayList<>();
|
||||
for (byte b : bytes) {
|
||||
int value = b & 0xFF;
|
||||
char high = HEX_ARRAY[value >>> 4];
|
||||
char low = HEX_ARRAY[value & 0x0F];
|
||||
hexList.add(String.valueOf(high) + low);
|
||||
}
|
||||
return hexList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除临时文件
|
||||
*/
|
||||
public void deleteTempFile(File file) {
|
||||
if (file != null && file.exists()) {
|
||||
if (file.delete()) {
|
||||
log.debug("删除临时文件成功: {}", file.getAbsolutePath());
|
||||
} else {
|
||||
log.warn("无法删除临时文件: {}", file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪图像到目标尺寸
|
||||
*/
|
||||
private BufferedImage cropImage(BufferedImage img, int targetWidth, int targetHeight) {
|
||||
int w = Math.min(img.getWidth(), targetWidth);
|
||||
int h = Math.min(img.getHeight(), targetHeight);
|
||||
return img.getSubimage(0, 0, w, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 BufferedImage 转换为 RGB565 格式的字节数组
|
||||
*/
|
||||
private byte[] convertToRGB565(BufferedImage image) {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
byte[] result = new byte[width * height * 2];
|
||||
int index = 0;
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int rgb = image.getRGB(x, y);
|
||||
int r = ((rgb >> 16) & 0xFF) >> 3;
|
||||
int g = ((rgb >> 8) & 0xFF) >> 2;
|
||||
int b = (rgb & 0xFF) >> 3;
|
||||
short pixel = (short) ((r << 11) | (g << 5) | b);
|
||||
|
||||
result[index++] = (byte) (pixel >> 8);
|
||||
result[index++] = (byte) pixel;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存帧到本地(用于调试)
|
||||
*/
|
||||
public void saveFramesToLocal(List<BufferedImage> frames, String prefix) {
|
||||
File outputDir = new File("output_frames");
|
||||
if (!outputDir.exists()) {
|
||||
outputDir.mkdirs();
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
for (BufferedImage frame : frames) {
|
||||
try {
|
||||
File outputImage = new File(outputDir, prefix + "_" + (index++) + ".png");
|
||||
ImageIO.write(frame, "png", outputImage);
|
||||
log.debug("保存帧图片成功: {}", outputImage.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
log.error("保存帧图片失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user