本项目旨在构建一个 Web 端一键生成 AI 配音视频的系统,提供从“上传视频 → 提取语音 → 翻译 → 合成音频 → 合成字幕与数字人 → 下载结果”的完整流程。后端基于 SpringBoot,前端使用 Thymeleaf + Bootstrap,结合 Redis 实现异步任务状态跟踪与进度轮询,支持多用户并发任务处理。
功能亮点
- 🔁 全流程:上传原视频 → 翻译 → 配音合成 → 视频输出
- 💬 Spring AI:调用 AI 模型实现翻译、合成
- 🎞️ Web UI:Thymeleaf + Bootstrap 实现进度轮询
- 🔧 Redis + Spring Task 实现异步任务与进度管理
- 💡 实际 DEMO:上传 original_video.mp4 → 下载 linly_dubbing.mp4
项目结构
复制com.icoderoad.easydub ├── controller │ └── DubbingController.java ├── service │ ├── DubbingService.java │ └── ProgressService.java ├── config │ └── TaskConfig.java ├── model │ └── TaskStatus.java ├── templates │ └── index.html ├── static │ └── bootstrap + js ├── application.yml └── EasyDubApplication.java
SpringBoot 构建 REST 接口
视频上传与任务创建接口
复制package com.icoderoad.easydub.controller; import com.icoderoad.easydub.service.DubbingService; import com.icoderoad.easydub.service.ProgressService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api") public class DubbingController { @Autowired private DubbingService dubbingService; @PostMapping("/upload") public String upload(@RequestParam("file") MultipartFile file) { return dubbingService.handleUpload(file); } @GetMapping("/progress/{taskId}") public String getProgress(@PathVariable String taskId) { return dubbingService.getProgress(taskId); } @GetMapping("/download/{taskId}") public String getDownloadUrl(@PathVariable String taskId) { return dubbingService.getDownloadUrl(taskId); } }
Spring Task + Redis 实现任务调度
配置异步线程池
复制package com.icoderoad.easydub.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration public class TaskConfig { @Bean(name = "taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(3); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("DubbingTask-"); executor.initialize(); return executor; } }
后台任务处理服务
复制package com.icoderoad.easydub.service; import com.icoderoad.easydub.model.TaskStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import redis.clients.jedis.Jedis; import java.io.File; import java.io.FileOutputStream; import java.util.UUID; @Service public class DubbingService { @Autowired private ProgressService progressService; private final String baseDir = "output/"; public String handleUpload(MultipartFile file) { String taskId = UUID.randomUUID().toString(); File saveFile = new File(baseDir + taskId + "_original.mp4"); try (FileOutputStream fos = new FileOutputStream(saveFile)) { fos.write(file.getBytes()); } catch (Exception e) { return "上传失败:" + e.getMessage(); } progressService.init(taskId); processAsync(taskId, saveFile.getAbsolutePath()); return taskId; } @Async("taskExecutor") public void processAsync(String taskId, String inputPath) { try { progressService.update(taskId, "提取音频中..."); String audioPath = extractAudio(inputPath); progressService.update(taskId, "识别翻译中..."); String translatedText = callSpringAIWhisperAndTranslate(audioPath); progressService.update(taskId, "合成语音中..."); String newVoice = synthesizeAudio(translatedText); progressService.update(taskId, "合成视频中..."); String finalVideo = composeVideo(inputPath, newVoice, taskId); progressService.complete(taskId, finalVideo); } catch (Exception e) { progressService.fail(taskId, e.getMessage()); } } private String extractAudio(String inputPath) throws Exception { String outPath = inputPath.replace(".mp4", ".wav"); String cmd = String.format("ffmpeg -i %s -vn -acodec pcm_s16le -ar 16000 -ac 1 %s", inputPath, outPath); Runtime.getRuntime().exec(cmd).waitFor(); return outPath; } private String callSpringAIWhisperAndTranslate(String audioPath) { // 伪代码:可以集成 Spring AI Whisper + LLM 翻译 return "你好,欢迎来到 EasyDub。"; } private String synthesizeAudio(String text) { // 伪代码:调用 XTTS 合成中文音频 return "output/temp_tts.wav"; } private String composeVideo(String originalVideo, String newAudio, String taskId) throws Exception { String output = baseDir + taskId + "_linly_dubbing.mp4"; String cmd = String.format("ffmpeg -i %s -i %s -map 0:v -map 1:a -c:v copy -c:a aac %s", originalVideo, newAudio, output); Runtime.getRuntime().exec(cmd).waitFor(); return output; } public String getProgress(String taskId) { return progressService.query(taskId); } public String getDownloadUrl(String taskId) { return progressService.getResultUrl(taskId); } }
Redis 进度服务封装
复制package com.icoderoad.easydub.service; import org.springframework.stereotype.Service; import redis.clients.jedis.Jedis; @Service public class ProgressService { private final Jedis redis = new Jedis("localhost", 6379); public void init(String taskId) { redis.set(taskId, "开始处理..."); } public void update(String taskId, String message) { redis.set(taskId, message); } public void complete(String taskId, String path) { redis.set(taskId + ":done", path); redis.set(taskId, "处理完成!"); } public void fail(String taskId, String errorMsg) { redis.set(taskId, "失败:" + errorMsg); } public String query(String taskId) { return redis.get(taskId); } public String getResultUrl(String taskId) { return redis.get(taskId + ":done"); } }
Web 前端 Thymeleaf + Bootstrap
index.html
复制<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>EasyDub 配音生成</title> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.3.1/css/bootstrap.min.css"/> </head> <body class="container mt-5"> <h2>🎬 EasyDub 配音生成平台</h2> <form id="uploadForm" enctype="multipart/form-data"> <input type="file" class="form-control" name="file" required/> <button type="submit" class="btn btn-primary mt-2">上传并开始处理</button> </form> <div class="mt-3" id="status" style="display:none;"> <h5>进度:<span id="progressMsg"></span></h5> </div> <div class="mt-3" id="download" style="display:none;"> <a class="btn btn-success" id="downloadLink" href="#">下载结果视频</a> </div> <script> document.getElementById('uploadForm').addEventListener('submit', function (e) { e.preventDefault(); let formData = new FormData(this); fetch('/api/upload', { method: 'POST', body: formData }).then(res => res.text()).then(taskId => { document.getElementById("status").style.display = "block"; pollProgress(taskId); }); }); function pollProgress(taskId) { let interval = setInterval(() => { fetch('/api/progress/' + taskId).then(res => res.text()).then(msg => { document.getElementById("progressMsg").innerText = msg; if (msg.includes("完成")) { clearInterval(interval); document.getElementById("download").style.display = "block"; fetch('/api/download/' + taskId).then(r => r.text()).then(url => { document.getElementById("downloadLink").href = '/' + url; }); } else if (msg.includes("失败")) { clearInterval(interval); alert("处理失败:" + msg); } }); }, 2000); } </script> </body> </html>
本地 DEMO 流程
- 启动 SpringBoot 应用
- 浏览器打开 http://localhost:8080
- 上传 original_video.mp4
- 等待进度提示,后台完成:
视频 → 音频提取 → Whisper识别 → 翻译 → 合成配音 → 视频合成
- 下载生成的 linly_dubbing.mp4
结语
通过整合 Spring Boot、Thymeleaf、Redis、FFmpeg 与 AI 模型接口(Whisper、XTTSv2 等),我们构建了一个功能强大且易用的 EasyDub Web 配音系统,支持异步处理、状态轮询、数字人合成与完整视频输出。