混剪优化
This commit is contained in:
78
.promptx/pouch.json
Normal file
78
.promptx/pouch.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"currentState": "initial",
|
||||
"stateHistory": [
|
||||
{
|
||||
"from": "initial",
|
||||
"command": "project",
|
||||
"timestamp": "2025-12-06T14:49:15.887Z",
|
||||
"args": [
|
||||
{
|
||||
"workingDirectory": "d:\\projects\\sionrui",
|
||||
"ideType": "vscode"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "initial",
|
||||
"command": "action",
|
||||
"timestamp": "2025-12-06T14:49:17.391Z",
|
||||
"args": [
|
||||
"nuwa"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "initial",
|
||||
"command": "toolx",
|
||||
"timestamp": "2025-12-06T14:50:24.336Z",
|
||||
"args": [
|
||||
"@tool://role-creator",
|
||||
"manual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "initial",
|
||||
"command": "toolx",
|
||||
"timestamp": "2025-12-06T14:51:32.727Z",
|
||||
"args": [
|
||||
"@tool://role-creator",
|
||||
"execute",
|
||||
"{\"role\":\"video-editing-pm\",\"action\":\"write\",\"file\":\"video-editing-pm.role.md\",\"content\":\"<role>\\n<personality>\\n我是一名专业的视频剪辑产品经理,专注于AI驱动的智能剪辑工具和技术架构设计。\\n\\n核心特征:\\n- 技术嗅觉敏锐:快速理解新兴AI视频技术,评估其商业价值和可行性\\n- 架构思维清晰:擅长设计工具组合方案,优化自动化流程\\n- 决策果断务实:基于数据和市场洞察做出清晰的产品决策\\n- 前瞻性强:关注行业趋势,预判技术发展方向\\n\\n思维模式:\\n- 以技术可行性为前提,以用户体验为核心\\n- 平衡创新与稳定,评估风险与收益\\n- 从系统工程角度思考工具集成和流程优化\\n</personality>\\n\\n<principle>\\n1. 技术评估三原则:先进性、成熟度、兼容性\\n2. 决策透明化:清晰阐述决策依据和权衡过程\\n3. 用户价值导向:所有技术选择最终服务于用户需求\\n4. 快速迭代验证:用MVP方式测试技术方案可行性\\n5. 架构可持续性:设计可扩展、可维护的技术架构\\n</principle>\\n\\n<knowledge>\\n视频剪辑技术栈:\\n- AI视觉:目标检测、场景识别、自动剪辑点检测\\n- AI听觉:语音识别、音频分离、自动配乐\\n- 云渲染:分布式渲染、GPU集群、实时预览\\n- 自动化工作流:任务编排、批量处理、监控告警\\n\\n主流AI剪辑工具:\\n- Runway ML:AI视频生成和编辑\\n- Descript:基于文本的视频编辑\\n- Pictory:自动化短视频生成\\n- Synthesia:AI虚拟人视频制作\\n- Lumen5:AI驱动的内容转视频\\n\\n评估维度:\\n- 技术能力:AI模型精度、处理速度、稳定性\\n- 集成难度:API友好度、文档完整性、SDK支持\\n- 成本结构:授权费用、计算资源、人力成本\\n- 市场接受度:用户规模、客户案例、行业口碑\\n</knowledge>\\n</role>\\n\"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "initial",
|
||||
"command": "toolx",
|
||||
"timestamp": "2025-12-06T14:52:03.041Z",
|
||||
"args": [
|
||||
"@tool://role-creator",
|
||||
"execute",
|
||||
"{\"role\":\"video-editing-pm\",\"action\":\"write\",\"file\":\"thought/technical-analysis.thought.md\",\"content\":\"<thought>\\n<reasoning>\\n## 技术评估推理框架\\n\\n### 维度分析\\n**技术成熟度评估**\\n- 技术发展阶段:实验室/原型/商用/成熟\\n- 性能基准:准确率、速度、稳定性指标\\n- 资源需求:计算资源、存储需求、网络带宽\\n\\n**商业价值分析**\\n- 市场需求强度:痛点解决程度\\n- 竞争态势:市场空白、差异化优势\\n- 成本效益比:投入产出比、ROI预测\\n\\n**集成复杂度评估**\\n- API可用性:文档质量、调用限制\\n- 架构适配:与现有系统的兼容性\\n- 迁移成本:替换现有方案的成本\\n\\n### 决策矩阵构建\\n对每个技术方案,从以下维度打分(1-5分):\\n1. 技术先进性(创新程度、技术领先性)\\n2. 成熟度(稳定性、案例验证)\\n3. 集成难度(实施复杂度、时间成本)\\n4. 成本可控性(授权费、运维成本)\\n5. 市场接受度(用户需求、行业趋势)\\n\\n权重建议:技术40%、成熟度30%、集成20%、成本10%\\n</reasoning>\\n</thought>\\n\"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "initial",
|
||||
"command": "toolx",
|
||||
"timestamp": "2025-12-06T14:52:04.464Z",
|
||||
"args": [
|
||||
"@tool://role-creator",
|
||||
"execute",
|
||||
"{\"role\":\"video-editing-pm\",\"action\":\"write\",\"file\":\"execution/architecture-design.execution.md\",\"content\":\"<execution>\\n<process>\\n## 视频剪辑技术架构设计流程\\n\\n### Step 1: 需求分析\\n1. 明确业务场景和用户画像\\n2. 识别核心痛点和功能需求\\n3. 定义性能指标和SLA要求\\n4. 确定技术约束和边界条件\\n\\n### Step 2: 技术调研\\n1. 评估AI技术栈能力边界\\n2. 分析主流工具和平台特性\\n3. 研究竞品技术架构方案\\n4. 识别技术趋势和未来方向\\n\\n### Step 3: 方案设计\\n1. 构建工具组合方案\\n2. 设计自动化工作流程\\n3. 规划系统集成策略\\n4. 制定容错和降级机制\\n\\n### Step 4: 评估决策\\n1. 多维度打分和权重计算\\n2. 风险识别和缓解策略\\n3. 成本效益分析和ROI预测\\n4. 选择最优方案和备选方案\\n\\n### Step 5: 实施规划\\n1. 制定技术选型决策文档\\n2. 规划分阶段实施路线图\\n3. 定义成功指标和验证标准\\n4. 制定监控和优化机制\\n</process>\\n</execution>\\n\"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "initial",
|
||||
"command": "discover",
|
||||
"timestamp": "2025-12-06T14:52:39.427Z",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"from": "initial",
|
||||
"command": "action",
|
||||
"timestamp": "2025-12-06T15:01:23.095Z",
|
||||
"args": [
|
||||
"video-editing-pm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"lastUpdated": "2025-12-06T15:01:23.101Z"
|
||||
}
|
||||
17
.promptx/resource/project.registry.json
Normal file
17
.promptx/resource/project.registry.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"source": "project",
|
||||
"metadata": {
|
||||
"version": "2.0.0",
|
||||
"description": "project 级资源注册表",
|
||||
"createdAt": "2025-12-06T14:52:39.437Z",
|
||||
"updatedAt": "2025-12-06T14:52:39.438Z",
|
||||
"resourceCount": 0
|
||||
},
|
||||
"resources": [],
|
||||
"stats": {
|
||||
"totalResources": 0,
|
||||
"byProtocol": {},
|
||||
"bySource": {}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,13 @@ export const MixTaskService = {
|
||||
*/
|
||||
cancelTask(id) {
|
||||
return http.post(`${BASE_URL}/cancel/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取任务输出文件的签名URL
|
||||
*/
|
||||
getSignedUrls(id) {
|
||||
return http.get(`${BASE_URL}/signed-url/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -533,7 +533,6 @@ const loadLastTask = async () => {
|
||||
if (task.status === 'SUCCESS' && task.resultVideoUrl) {
|
||||
previewVideoUrl.value = task.resultVideoUrl
|
||||
currentTaskStatus.value = 'SUCCESS'
|
||||
message.success('已自动加载最近一次任务结果')
|
||||
} else if (task.status === 'PROCESSING') {
|
||||
// 如果任务还在处理中,继续轮询
|
||||
currentTaskStatus.value = 'PROCESSING'
|
||||
|
||||
@@ -877,7 +877,6 @@ const loadLastTask = async () => {
|
||||
previewVideoUrl.value = task.resultVideoUrl
|
||||
currentTaskStatus.value = 'SUCCESS'
|
||||
currentTaskError.value = ''
|
||||
message.success('已自动加载最近一次任务结果')
|
||||
} else if (task.status === 'PROCESSING') {
|
||||
// 如果任务还在处理中,继续轮询
|
||||
currentTaskStatus.value = 'PROCESSING'
|
||||
|
||||
@@ -119,23 +119,16 @@
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="record.outputUrls && record.outputUrls.length > 0"
|
||||
v-if="record.status === 'success' && record.outputUrls && record.outputUrls.length > 0"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleDownloadAll(record.outputUrls)"
|
||||
@click="handleDownloadAll(record.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
<span>下载</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'failed'"
|
||||
size="small"
|
||||
@click="handleRetry(record.id)"
|
||||
>
|
||||
重新生成
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'running'"
|
||||
size="small"
|
||||
@@ -171,17 +164,25 @@
|
||||
:key="index"
|
||||
class="result-item"
|
||||
>
|
||||
<a :href="url" target="_blank">
|
||||
<a-button
|
||||
v-if="record.status === 'success'"
|
||||
type="link"
|
||||
@click="handlePreviewSignedUrl(record.id, index)"
|
||||
>
|
||||
<PlayCircleOutlined />
|
||||
视频 {{ index + 1 }}
|
||||
</a>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="record.status === 'success'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleDownload(url)"
|
||||
@click="handleDownloadSignedUrl(record.id, index)"
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</a-button>
|
||||
<span v-else class="processing-tip">
|
||||
视频 {{ index + 1 }} (处理中...)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,7 +204,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
@@ -218,6 +219,7 @@ import { formatDate } from '@/utils/file'
|
||||
const loading = ref(false)
|
||||
const taskList = ref([])
|
||||
const expandedRowKeys = ref([])
|
||||
const refreshInterval = ref(null) // 定时刷新定时器
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
@@ -429,25 +431,73 @@ const handleDownload = (url) => {
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
// 批量下载所有视频
|
||||
const handleDownloadAll = (urls) => {
|
||||
if (!urls || urls.length === 0) {
|
||||
message.warning('没有可下载的视频')
|
||||
return
|
||||
// 下载单个视频(使用签名URL)
|
||||
|
||||
const handlePreviewSignedUrl = async (taskId, index) => {
|
||||
try {
|
||||
message.loading('正在获取预览链接...', 0)
|
||||
const res = await MixTaskService.getSignedUrls(taskId)
|
||||
message.destroy()
|
||||
|
||||
if (res.code === 0 && res.data && res.data[index]) {
|
||||
window.open(res.data[index], '_blank')
|
||||
} else {
|
||||
message.warning('获取预览链接失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预览链接失败:', error)
|
||||
message.destroy()
|
||||
message.error('获取预览链接失败')
|
||||
}
|
||||
}
|
||||
|
||||
message.loading('正在准备下载...', 0)
|
||||
const handleDownloadSignedUrl = async (taskId, index) => {
|
||||
try {
|
||||
message.loading('正在获取下载链接...', 0)
|
||||
const res = await MixTaskService.getSignedUrls(taskId)
|
||||
message.destroy()
|
||||
|
||||
// 逐个触发下载,避免浏览器阻止多个弹窗
|
||||
urls.forEach((url, index) => {
|
||||
setTimeout(() => {
|
||||
console.log('下载视频:', url)
|
||||
handleDownload(url)
|
||||
}, index * 500) // 每个下载间隔500ms
|
||||
})
|
||||
if (res.code === 0 && res.data && res.data[index]) {
|
||||
handleDownload(res.data[index])
|
||||
} else {
|
||||
message.warning('获取下载链接失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下载链接失败:', error)
|
||||
message.destroy()
|
||||
message.error('获取下载链接失败')
|
||||
}
|
||||
}
|
||||
|
||||
message.destroy()
|
||||
message.success(`已触发 ${urls.length} 个视频的下载`)
|
||||
// 批量下载所有视频
|
||||
const handleDownloadAll = async (taskId) => {
|
||||
try {
|
||||
message.loading('正在获取下载链接...', 0)
|
||||
const res = await MixTaskService.getSignedUrls(taskId)
|
||||
message.destroy()
|
||||
|
||||
if (res.code === 0 && res.data && res.data.length > 0) {
|
||||
const urls = res.data
|
||||
message.loading('正在准备下载...', 0)
|
||||
|
||||
// 逐个触发下载,避免浏览器阻止多个弹窗
|
||||
urls.forEach((url, index) => {
|
||||
setTimeout(() => {
|
||||
console.log('下载视频:', url)
|
||||
handleDownload(url)
|
||||
}, index * 500) // 每个下载间隔500ms
|
||||
})
|
||||
|
||||
message.destroy()
|
||||
message.success(`已触发 ${urls.length} 个视频的下载`)
|
||||
} else {
|
||||
message.warning('没有可下载的视频')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取下载链接失败:', error)
|
||||
message.destroy()
|
||||
message.error('获取下载链接失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
@@ -486,7 +536,35 @@ const getProgressStatus = (status) => {
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadTaskList()
|
||||
// 开启定时刷新,每30秒检查一次running状态的任务
|
||||
startAutoRefresh()
|
||||
})
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
})
|
||||
|
||||
// 开启自动刷新
|
||||
const startAutoRefresh = () => {
|
||||
// 清除可能存在的旧定时器
|
||||
stopAutoRefresh()
|
||||
|
||||
refreshInterval.value = setInterval(() => {
|
||||
// 只在页面可见时刷新,避免后台浪费资源
|
||||
if (document.visibilityState === 'visible') {
|
||||
loadTaskList()
|
||||
}
|
||||
}, 30000) // 30秒刷新一次
|
||||
}
|
||||
|
||||
// 停止自动刷新
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value)
|
||||
refreshInterval.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -578,6 +656,11 @@ onMounted(() => {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.processing-tip {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.task-error {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@@ -85,4 +85,12 @@ public interface FileService {
|
||||
*/
|
||||
byte[] getFileContent(Long configId, String path) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据OSS URL删除文件
|
||||
*
|
||||
* @param ossUrl OSS文件完整URL
|
||||
* @throws Exception 删除失败时抛出异常
|
||||
*/
|
||||
void deleteFileByUrl(String ossUrl) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
@@ -31,6 +32,7 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EX
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class FileServiceImpl implements FileService {
|
||||
|
||||
@@ -198,4 +200,44 @@ public class FileServiceImpl implements FileService {
|
||||
return client.getContent(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFileByUrl(String ossUrl) throws Exception {
|
||||
if (StrUtil.isEmpty(ossUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从URL中提取相对路径
|
||||
// 例如: https://bucket.oss-region.aliyuncs.com/mix/20241206/video.mp4
|
||||
// 需要提取出: mix/20241206/video.mp4
|
||||
String path = extractPathFromUrl(ossUrl);
|
||||
if (StrUtil.isEmpty(path)) {
|
||||
log.warn("[FileService][删除文件][URL解析路径失败] ossUrl={}", ossUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用默认的master客户端删除文件
|
||||
FileClient client = fileConfigService.getMasterFileClient();
|
||||
Assert.notNull(client, "客户端(master) 不能为空");
|
||||
client.delete(path);
|
||||
log.info("[FileService][删除文件成功] path={}, ossUrl={}", path, ossUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从OSS URL中提取相对路径
|
||||
*/
|
||||
private String extractPathFromUrl(String ossUrl) {
|
||||
if (StrUtil.isEmpty(ossUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查找 ".aliyuncs.com/" 的位置
|
||||
int index = ossUrl.indexOf(".aliyuncs.com/");
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 提取 "/" 后的路径
|
||||
return ossUrl.substring(index + ".aliyuncs.com/".length());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.module.tik.media;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.api.file.FileApi;
|
||||
import cn.iocoder.yudao.module.tik.file.service.TikOssInitService;
|
||||
import cn.iocoder.yudao.module.tik.mix.config.IceProperties;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
@@ -36,6 +37,7 @@ public class BatchProduceAlignment {
|
||||
|
||||
private final IceProperties properties;
|
||||
private final FileApi fileApi;
|
||||
private final TikOssInitService ossInitService;
|
||||
private Client iceClient;
|
||||
|
||||
public void initClient() throws Exception {
|
||||
@@ -60,7 +62,7 @@ public class BatchProduceAlignment {
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> batchProduceAlignment(String title, String[] videoArray, int produceCount) throws Exception {
|
||||
public List<String> batchProduceAlignment(String title, String[] videoArray, int produceCount, Long userId) throws Exception {
|
||||
// 初始化 ICE 客户端
|
||||
if (iceClient == null) {
|
||||
initClient();
|
||||
@@ -71,7 +73,7 @@ public class BatchProduceAlignment {
|
||||
|
||||
if (produceCount <= 1) {
|
||||
// 生成1个视频,包含所有片段
|
||||
String jobIdWithUrl = produceSingleVideo(title, videoArray);
|
||||
String jobIdWithUrl = produceSingleVideo(title, videoArray, userId);
|
||||
jobIdWithUrls.add(jobIdWithUrl);
|
||||
} else {
|
||||
// 生成多个视频:将视频数组分成多份,每份生成一个视频
|
||||
@@ -90,7 +92,7 @@ public class BatchProduceAlignment {
|
||||
String[] groupVideos = Arrays.copyOfRange(videoArray, start, start + groupSize);
|
||||
|
||||
// 生成单个视频
|
||||
String jobIdWithUrl = produceSingleVideo(title, groupVideos);
|
||||
String jobIdWithUrl = produceSingleVideo(title, groupVideos, userId);
|
||||
jobIdWithUrls.add(jobIdWithUrl);
|
||||
|
||||
start += groupSize;
|
||||
@@ -101,7 +103,7 @@ public class BatchProduceAlignment {
|
||||
return jobIdWithUrls;
|
||||
}
|
||||
|
||||
public String produceSingleVideo(String title, String[] videoArray) throws Exception {
|
||||
public String produceSingleVideo(String title, String[] videoArray, Long userId) throws Exception {
|
||||
// 初始化 ICE 客户端
|
||||
if (iceClient == null) {
|
||||
initClient();
|
||||
@@ -113,7 +115,6 @@ public class BatchProduceAlignment {
|
||||
|
||||
// 按顺序拼接视频片段(不随机打乱)
|
||||
for (int i = 0; i < videoArray.length; i++) {
|
||||
String clipId = "clip" + i;
|
||||
String videoUrl = videoArray[i];
|
||||
|
||||
// 验证视频URL必须是阿里云OSS地址
|
||||
@@ -130,32 +131,33 @@ public class BatchProduceAlignment {
|
||||
// 为每个视频片段添加静音的音频轨道
|
||||
JSONObject audioClip = new JSONObject();
|
||||
audioClip.put("MediaURL", videoUrl);
|
||||
// 添加静音效果
|
||||
JSONObject volumeEffect = new JSONObject();
|
||||
volumeEffect.put("Type", "Volume");
|
||||
volumeEffect.put("Gain", 0); // 0 表示静音
|
||||
JSONArray effects = new JSONArray();
|
||||
effects.add(volumeEffect);
|
||||
audioClip.put("Effects", effects);
|
||||
audioClip.put("Effects", new JSONArray() {{
|
||||
add(new JSONObject() {{
|
||||
put("Type", "Volume");
|
||||
put("Gain", 0); // 0 表示静音
|
||||
}});
|
||||
}});
|
||||
audioClipArray.add(audioClip);
|
||||
}
|
||||
|
||||
// 构建时间线,包含视频轨道和音频轨道
|
||||
String timeline = "{\"VideoTracks\":[{\"VideoTrackClips\":"+videoClipArray.toJSONString()+"}],\"AudioTracks\":[{\"AudioTrackClips\":"+audioClipArray.toJSONString()+"}]}";
|
||||
|
||||
//
|
||||
// 生成输出文件路径:{用户目录}/mix/{yyyyMMdd}/{uuid}.mp4
|
||||
String targetFileName = UUID.randomUUID().toString().replace("-", "");
|
||||
String mixDirectory = ossInitService.getOssDirectoryByCategory(userId, "mix");
|
||||
String dateDir = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
String outputMediaPath = mixDirectory + "/" + dateDir + "/" + targetFileName + ".mp4";
|
||||
|
||||
// 使用默认的阿里云OSS endpoint格式
|
||||
String bucketEndpoint = "https://" + properties.getBucket() + ".oss-" + properties.getRegionId() + ".aliyuncs.com";
|
||||
String outputMediaPath = "/ice_output/" + targetFileName + ".mp4";
|
||||
String outputMediaUrl = bucketEndpoint + outputMediaPath;
|
||||
|
||||
// 生成签名URL(有效期24小时)使用公共API
|
||||
String signedUrl = fileApi.presignGetUrl(outputMediaUrl, 24 * 3600);
|
||||
String outputMediaUrl = bucketEndpoint + "/" + outputMediaPath;
|
||||
|
||||
// ICE需要将处理结果写入到该URL,签名URL会导致写入失败
|
||||
int width = 720;
|
||||
int height = 1280;
|
||||
int bitrate = 2000; // 输出码率 2000 Kbit/s(符合 ICE API 文档推荐)
|
||||
String outputMediaConfig = "{\"MediaURL\":\"" + signedUrl + "\",\"Width\":" + width + ",\"Height\":" + height + ",\"Bitrate\":" + bitrate + "}";
|
||||
String outputMediaConfig = "{\"MediaURL\":\"" + outputMediaUrl + "\",\"Width\":" + width + ",\"Height\":" + height + ",\"Bitrate\":" + bitrate + "}";
|
||||
|
||||
SubmitMediaProducingJobRequest request = new SubmitMediaProducingJobRequest();
|
||||
request.setTimeline(timeline);
|
||||
@@ -166,6 +168,7 @@ public class BatchProduceAlignment {
|
||||
|
||||
String jobId = response.getBody().getJobId();
|
||||
log.info("[ICE][任务提交成功][jobId={}, outputMediaUrl={}]", jobId, outputMediaUrl);
|
||||
// 返回原始URL,签名URL在用户下载时生成
|
||||
return jobId + " : " + outputMediaUrl;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.tik.media.controller;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.module.tik.media.BatchProduceAlignment;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -31,8 +32,14 @@ public class BatchProduceAlignmentController {
|
||||
// 生成的成片数
|
||||
int produceCount) {
|
||||
try {
|
||||
// 获取当前登录用户ID
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (userId == null) {
|
||||
return CommonResult.error(new ErrorCode(401, "用户未登录"));
|
||||
}
|
||||
|
||||
// 纯画面模式:仅传入视频数组,无需text和bgMusicArray
|
||||
List<String> jobIds = batchProduceAlignment.batchProduceAlignment(title, videoArray, produceCount);
|
||||
List<String> jobIds = batchProduceAlignment.batchProduceAlignment(title, videoArray, produceCount, userId);
|
||||
return CommonResult.success(jobIds);
|
||||
} catch (Exception e) {
|
||||
return CommonResult.error(new ErrorCode(500, "Produce failed. Exception: " + e));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.tik.mix.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
@@ -14,6 +15,9 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 混剪任务 Controller
|
||||
*
|
||||
@@ -98,4 +102,38 @@ public class MixTaskController {
|
||||
mixTaskService.cancelTask(id);
|
||||
return CommonResult.success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/signed-url/{id}")
|
||||
@Operation(summary = "获取任务输出文件的签名URL")
|
||||
public CommonResult<List<String>> getSignedUrls(@PathVariable Long id) {
|
||||
try {
|
||||
// 获取任务信息
|
||||
MixTaskRespVO mixTaskVO = mixTaskService.getMixTask(id);
|
||||
if (mixTaskVO == null || mixTaskVO.getOutputUrls() == null) {
|
||||
return CommonResult.success(List.of());
|
||||
}
|
||||
|
||||
// 重新生成所有输出URL的签名URL(避免使用过期的签名URL)
|
||||
List<String> signedUrls = new ArrayList<>();
|
||||
for (String outputUrl : mixTaskVO.getOutputUrls()) {
|
||||
try {
|
||||
// 每次都重新生成签名URL(有效期24小时)
|
||||
String signedUrl = mixTaskService.generateSignedUrl(outputUrl, 24 * 3600);
|
||||
if (StrUtil.isNotEmpty(signedUrl)) {
|
||||
signedUrls.add(signedUrl);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][生成签名URL失败] url={}, error={}", outputUrl, e.getMessage(), e);
|
||||
// 单个URL失败不影响其他URL
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[MixTask][获取签名URL成功] taskId={}, urlCount={}", id, signedUrls.size());
|
||||
return CommonResult.success(signedUrls);
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][获取签名URL失败] taskId={}, error={}", id, e.getMessage(), e);
|
||||
// 不返回详细错误信息给前端,避免XML/HTML解析问题
|
||||
return CommonResult.error(500, "获取签名URL失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,13 @@ public interface MixTaskService {
|
||||
* 保存任务结果
|
||||
*/
|
||||
void saveTaskResult(Long taskId, List<String> outputUrls);
|
||||
|
||||
/**
|
||||
* 生成签名URL
|
||||
*
|
||||
* @param ossUrl OSS文件完整URL
|
||||
* @param expirationSeconds 有效期(秒)
|
||||
* @return 签名URL
|
||||
*/
|
||||
String generateSignedUrl(String ossUrl, Integer expirationSeconds);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.tik.mix.service;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.infra.service.file.FileService;
|
||||
import cn.iocoder.yudao.module.tik.mix.client.IceClient;
|
||||
import cn.iocoder.yudao.module.tik.mix.constants.MixTaskConstants;
|
||||
import cn.iocoder.yudao.module.tik.mix.dal.dataobject.MixTaskDO;
|
||||
@@ -9,6 +11,7 @@ import cn.iocoder.yudao.module.tik.mix.dal.mysql.MixTaskMapper;
|
||||
import cn.iocoder.yudao.module.tik.mix.util.MixTaskUtils;
|
||||
import cn.iocoder.yudao.module.tik.mix.vo.*;
|
||||
import cn.iocoder.yudao.module.tik.media.BatchProduceAlignment;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -16,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -36,6 +40,9 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
private final BatchProduceAlignment batchProduceAlignment;
|
||||
private final IceClient iceClient;
|
||||
|
||||
@Resource
|
||||
private FileService fileService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createMixTask(MixTaskSaveReqVO createReqVO, Long userId) {
|
||||
@@ -51,9 +58,9 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
// 3. 异步提交到阿里云 ICE
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
submitToICE(task.getId(), createReqVO);
|
||||
submitToICE(task.getId(), createReqVO, userId);
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][提交ICE失败] taskId={}, error={}", task.getId(), e.getMessage(), e);
|
||||
log.error("[MixTask][提交ICE失败] taskId={}", task.getId(), e);
|
||||
updateTaskError(task.getId(), "提交任务失败: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
@@ -88,7 +95,24 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 执行删除
|
||||
// 2. 删除OSS文件
|
||||
try {
|
||||
List<String> outputUrls = existTask.getOutputUrlList();
|
||||
if (outputUrls != null && !outputUrls.isEmpty()) {
|
||||
log.info("[MixTask][删除OSS文件] taskId={}, fileCount={}", id, outputUrls.size());
|
||||
for (String url : outputUrls) {
|
||||
try {
|
||||
fileService.deleteFileByUrl(url);
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][删除单个OSS文件失败] taskId={}, url={}", id, url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][删除OSS文件失败] taskId={}", id, e);
|
||||
}
|
||||
|
||||
// 3. 执行删除
|
||||
mixTaskMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@@ -149,9 +173,9 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
saveReqVO.setTitle(existTask.getTitle());
|
||||
saveReqVO.setVideoUrls(existTask.getVideoUrlList());
|
||||
saveReqVO.setProduceCount(existTask.getProduceCount());
|
||||
submitToICE(id, saveReqVO);
|
||||
submitToICE(id, saveReqVO, existTask.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][重新提交失败] taskId={}, error={}", id, e.getMessage(), e);
|
||||
log.error("[MixTask][重新提交失败] taskId={}", id, e);
|
||||
updateTaskError(id, "重新提交失败: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
@@ -218,7 +242,7 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
syncTaskStatus(task.getId(), jobId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][单个任务检查失败] taskId={}, error={}", task.getId(), e.getMessage(), e);
|
||||
log.error("[MixTask][单个任务检查失败] taskId={}", task.getId(), e);
|
||||
failureCount++;
|
||||
// 单个任务失败不影响其他任务
|
||||
}
|
||||
@@ -247,8 +271,10 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
|
||||
// 根据ICE状态更新任务
|
||||
if ("Success".equalsIgnoreCase(status) || "success".equalsIgnoreCase(status)) {
|
||||
// 任务成功完成,更新为100%
|
||||
updateTaskStatus(taskId, MixTaskConstants.STATUS_SUCCESS, MixTaskConstants.PROGRESS_COMPLETED);
|
||||
// 任务成功完成,需要获取实际的输出URL
|
||||
log.info("[MixTask][ICE任务执行成功] taskId={}, jobId={}", taskId, jobId);
|
||||
// 重新生成签名URL并更新任务
|
||||
updateTaskSuccess(taskId, jobId);
|
||||
} else if ("Failed".equalsIgnoreCase(status) || "failed".equalsIgnoreCase(status) || "Failure".equalsIgnoreCase(status)) {
|
||||
// 任务失败 - 获取详细错误信息
|
||||
String errorMsg = "ICE任务执行失败";
|
||||
@@ -262,18 +288,38 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
taskId, jobId, status, errorMsg);
|
||||
updateTaskError(taskId, errorMsg);
|
||||
} else if ("Running".equalsIgnoreCase(status) || "running".equalsIgnoreCase(status) || "Processing".equalsIgnoreCase(status)) {
|
||||
// 任务仍在运行,更新进度为70%
|
||||
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, 70);
|
||||
// 任务仍在运行,逐步更新进度
|
||||
// 1. 获取当前进度
|
||||
MixTaskDO currentTask = mixTaskMapper.selectById(taskId);
|
||||
if (currentTask != null) {
|
||||
int currentProgress = currentTask.getProgress() != null ? currentTask.getProgress() : 0;
|
||||
// 2. 根据当前进度逐步提升,但不超过95%
|
||||
int newProgress;
|
||||
if (currentProgress < 50) {
|
||||
newProgress = 50; // 首次看到Running时更新到50%
|
||||
} else if (currentProgress < 80) {
|
||||
newProgress = 80; // 第二次更新到80%
|
||||
} else if (currentProgress < 95) {
|
||||
newProgress = 95; // 最后更新到95%
|
||||
} else {
|
||||
newProgress = currentProgress; // 已经很高了,保持不变
|
||||
}
|
||||
|
||||
if (newProgress != currentProgress) {
|
||||
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, newProgress);
|
||||
log.info("[MixTask][进度更新] taskId={}, from={}%, to={}%", taskId, currentProgress, newProgress);
|
||||
}
|
||||
}
|
||||
} else if ("Pending".equalsIgnoreCase(status) || "pending".equalsIgnoreCase(status)) {
|
||||
// 任务等待中,更新进度为60%
|
||||
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, 60);
|
||||
// 任务等待中,更新进度为30%
|
||||
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, 30);
|
||||
} else {
|
||||
// 未知状态,记录日志但不更新
|
||||
log.warn("[MixTask][未知ICE状态] taskId={}, jobId={}, iceStatus={}", taskId, jobId, status);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][状态同步异常] taskId={}, jobId={}, error={}", taskId, jobId, e.getMessage(), e);
|
||||
log.error("[MixTask][状态同步异常] taskId={}, jobId={}", taskId, jobId, e);
|
||||
updateTaskError(taskId, "查询任务状态失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -291,75 +337,49 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
mixTaskMapper.updateById(updateTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSignedUrl(String ossUrl, Integer expirationSeconds) {
|
||||
try {
|
||||
if (StrUtil.isEmpty(ossUrl)) {
|
||||
return null;
|
||||
}
|
||||
// 使用FileService生成签名URL
|
||||
return fileService.presignGetUrl(ossUrl, expirationSeconds);
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][生成签名URL失败] url={}", ossUrl, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交任务到阿里云 ICE
|
||||
*/
|
||||
private void submitToICE(Long taskId, MixTaskSaveReqVO createReqVO) {
|
||||
private void submitToICE(Long taskId, MixTaskSaveReqVO createReqVO, Long userId) {
|
||||
try {
|
||||
// 1. 更新任务状态为运行中,进度10%
|
||||
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, MixTaskConstants.PROGRESS_SUBMITTED);
|
||||
|
||||
// 2. 转换为ICE需要的参数格式
|
||||
// 1. 转换为ICE需要的参数格式
|
||||
String[] videoArray = createReqVO.getVideoUrls().toArray(new String[0]);
|
||||
|
||||
// 3. 调用ICE批量生成接口(纯画面模式:无需text和bgMusic)
|
||||
// 2. 调用ICE批量生成接口(纯画面模式:无需text和bgMusic)
|
||||
List<String> jobIdWithUrls = batchProduceAlignment.batchProduceAlignment(
|
||||
createReqVO.getTitle(),
|
||||
videoArray,
|
||||
createReqVO.getProduceCount()
|
||||
createReqVO.getProduceCount(),
|
||||
userId
|
||||
);
|
||||
|
||||
// 4. 解析jobId和输出URL
|
||||
// 3. 解析jobId和输出URL
|
||||
MixTaskUtils.JobIdUrlPair jobIdUrlPair = MixTaskUtils.parseJobIdsAndUrls(jobIdWithUrls);
|
||||
|
||||
// 5. 更新任务信息
|
||||
// 4. 更新任务信息(包含状态和进度)
|
||||
updateTaskWithResults(taskId, jobIdUrlPair.getJobIds(), jobIdUrlPair.getOutputUrls(),
|
||||
MixTaskConstants.STATUS_RUNNING, MixTaskConstants.PROGRESS_UPLOADED);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][ICE提交失败] taskId={}, error={}", taskId, e.getMessage(), e);
|
||||
updateTaskError(taskId, "ICE处理失败: " + e.getMessage());
|
||||
// 注意:异步线程中不抛出异常,避免未处理异常
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交任务到阿里云 ICE(新版本,使用 IceClient)
|
||||
*/
|
||||
private void submitToIceV2(Long taskId, MixTaskSaveReqVO createReqVO) {
|
||||
try {
|
||||
// 1. 更新任务状态
|
||||
updateTaskStatus(taskId, MixTaskConstants.STATUS_RUNNING, MixTaskConstants.PROGRESS_SUBMITTED);
|
||||
|
||||
// 2. 构建 timeline 和 outputMediaConfig
|
||||
String timeline = buildTimeline(createReqVO);
|
||||
String outputMediaConfig = buildOutputMediaConfig();
|
||||
|
||||
// 3. 提交任务
|
||||
// TODO: 这里需要将 BatchProduceAlignment 的逻辑移到 IceClient 中
|
||||
// String jobId = iceClient.submitMediaProducingJob(timeline, outputMediaConfig);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][V2提交失败] taskId={}, error={}", taskId, e.getMessage(), e);
|
||||
log.error("[MixTask][ICE提交失败] taskId={}", taskId, e);
|
||||
updateTaskError(taskId, "ICE处理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 ICE 时间线
|
||||
*/
|
||||
private String buildTimeline(MixTaskSaveReqVO reqVO) {
|
||||
// TODO: 实现 timeline 构建逻辑
|
||||
return "{}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建输出媒体配置
|
||||
*/
|
||||
private String buildOutputMediaConfig() {
|
||||
// TODO: 实现输出配置构建逻辑
|
||||
return "{}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态
|
||||
@@ -399,6 +419,62 @@ public class MixTaskServiceImpl implements MixTaskService {
|
||||
mixTaskMapper.updateById(updateTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务成功状态(重新生成签名URL)
|
||||
*/
|
||||
private void updateTaskSuccess(Long taskId, String jobId) {
|
||||
try {
|
||||
// 查询任务信息
|
||||
MixTaskDO task = mixTaskMapper.selectById(taskId);
|
||||
if (task == null) {
|
||||
log.warn("[MixTask][任务不存在] taskId={}", taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取原始输出URL(不是签名URL)
|
||||
List<String> outputUrls = task.getOutputUrlList();
|
||||
if (outputUrls == null || outputUrls.isEmpty()) {
|
||||
log.warn("[MixTask][任务成功但没有输出URL] taskId={}", taskId);
|
||||
updateTaskStatus(taskId, MixTaskConstants.STATUS_SUCCESS, MixTaskConstants.PROGRESS_COMPLETED);
|
||||
return;
|
||||
}
|
||||
|
||||
// 任务成功时,直接保存原始URL(不是签名URL)
|
||||
// 签名URL在用户下载时通过 /signed-url/{id} 接口动态生成
|
||||
MixTaskDO updateTask = new MixTaskDO();
|
||||
updateTask.setId(taskId);
|
||||
updateTask.setStatus(MixTaskConstants.STATUS_SUCCESS);
|
||||
updateTask.setProgress(MixTaskConstants.PROGRESS_COMPLETED);
|
||||
updateTask.setOutputUrlList(outputUrls);
|
||||
updateTask.setFinishTime(java.time.LocalDateTime.now());
|
||||
mixTaskMapper.updateById(updateTask);
|
||||
|
||||
log.info("[MixTask][任务成功更新] taskId={}, urlCount={}", taskId, outputUrls.size());
|
||||
} catch (Exception e) {
|
||||
log.error("[MixTask][更新任务成功状态失败] taskId={}", taskId, e);
|
||||
// 即使更新失败,也标记为成功
|
||||
updateTaskStatus(taskId, MixTaskConstants.STATUS_SUCCESS, MixTaskConstants.PROGRESS_COMPLETED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从OSS URL中提取相对路径
|
||||
*/
|
||||
private String extractPathFromUrl(String ossUrl) {
|
||||
if (StrUtil.isEmpty(ossUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查找 ".aliyuncs.com/" 的位置
|
||||
int index = ossUrl.indexOf(".aliyuncs.com/");
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 提取 "/" 后的路径
|
||||
return ossUrl.substring(index + ".aliyuncs.com/".length());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将DO分页结果转换为VO分页结果,消除冗余代码
|
||||
* 优化点:
|
||||
|
||||
@@ -12,8 +12,8 @@ spring:
|
||||
servlet:
|
||||
# 文件上传相关配置项
|
||||
multipart:
|
||||
max-file-size: 100MB # 单个文件大小(配音文件建议50MB以内)
|
||||
max-request-size: 200MB # 设置总上传的文件大小(支持多文件上传)
|
||||
max-file-size: 300MB # 单个文件大小
|
||||
max-request-size: 500MB # 设置总上传的文件大小(支持多文件上传)
|
||||
|
||||
# Jackson 配置项
|
||||
jackson:
|
||||
|
||||
Reference in New Issue
Block a user