EasyDub 配音視頻生成平臺:SpringBoot + Thymeleaf + Spring AI 實戰開發
作者:編程疏影
通過整合 Spring Boot、Thymeleaf、Redis、FFmpeg 與 AI 模型接口(Whisper、XTTSv2 等),我們構建了一個功能強大且易用的 EasyDub Web 配音系統,支持異步處理、狀態輪詢、數字人合成與完整視頻輸出。?
本項目旨在構建一個 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" />
</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 配音系統,支持異步處理、狀態輪詢、數字人合成與完整視頻輸出。
責任編輯:武曉燕
來源:
路條編程