diff --git a/frontend/app/web-gold/src/api/http.js b/frontend/app/web-gold/src/api/http.js index 2948a9b092..3ebd0cebe2 100644 --- a/frontend/app/web-gold/src/api/http.js +++ b/frontend/app/web-gold/src/api/http.js @@ -1,6 +1,17 @@ import createClientAxios from '@gold/api/axios/client' import { refreshToken } from '@/api/auth' +import { clearUserInfoCache } from '@/api/userinfo' import router from '@/router' +import tokenManager from '@gold/utils/token-manager' + +/** + * 处理401/403错误 - 清理凭证并跳转登录页 + */ +const handleAuthError = (error) => { + tokenManager.clearTokens() + clearUserInfoCache() + router.push('/login') +} /** * 创建HTTP客户端实例 @@ -16,8 +27,8 @@ export function createHttpClient(options = {}) { baseURL: '/', timeout: 180000, refreshTokenFn: refreshToken, - on401: on401 || ((error) => router.push('/login')), - on403: on403 || ((error) => router.push('/login')), + on401: on401 || handleAuthError, + on403: on403 || handleAuthError, }) } diff --git a/frontend/app/web-gold/src/api/kling.js b/frontend/app/web-gold/src/api/kling.js index e2dedc2673..02733e4c13 100644 --- a/frontend/app/web-gold/src/api/kling.js +++ b/frontend/app/web-gold/src/api/kling.js @@ -90,17 +90,13 @@ export function getLipSyncTask(taskId) { * 识别已上传的视频 */ export async function identifyUploadedVideo(videoFile) { - try { - const urlRes = await MaterialService.getVideoPlayUrl(videoFile.fileId) - if (urlRes.code !== 0 || !urlRes.data) { - throw new Error(urlRes.msg || '获取播放链接失败') - } - - const identifyData = await performFaceIdentification(urlRes.data) - return buildIdentifyResponse(videoFile.id, urlRes.data, identifyData, false) - } catch (error) { - throw error + const urlRes = await MaterialService.getVideoPlayUrl(videoFile.fileId) + if (urlRes.code !== 0 || !urlRes.data) { + throw new Error(urlRes.msg || '获取播放链接失败') } + + const identifyData = await performFaceIdentification(urlRes.data) + return buildIdentifyResponse(videoFile.id, urlRes.data, identifyData, false) } /** @@ -109,23 +105,19 @@ export async function identifyUploadedVideo(videoFile) { export async function uploadAndIdentifyVideo(file) { const coverBase64 = await extractVideoCoverOptional(file) - try { - const uploadRes = await MaterialService.uploadFile(file, 'video', coverBase64, null, null) - if (uploadRes.code !== 0) { - throw new Error(uploadRes.msg || '上传失败') - } - - const fileId = uploadRes.data - - const urlRes = await MaterialService.getVideoPlayUrl(fileId) - if (urlRes.code !== 0) { - throw new Error(urlRes.msg || '获取播放链接失败') - } - - const identifyData = await performFaceIdentification(urlRes.data) - return buildIdentifyResponse(fileId, urlRes.data, identifyData, true) - } catch (error) { - throw error + const uploadRes = await MaterialService.uploadFile(file, 'digital_human', coverBase64, null, null) + if (uploadRes.code !== 0) { + throw new Error(uploadRes.msg || '上传失败') } + + const fileId = uploadRes.data + + const urlRes = await MaterialService.getVideoPlayUrl(fileId) + if (urlRes.code !== 0) { + throw new Error(urlRes.msg || '获取播放链接失败') + } + + const identifyData = await performFaceIdentification(urlRes.data) + return buildIdentifyResponse(fileId, urlRes.data, identifyData, true) } diff --git a/frontend/app/web-gold/src/components/VideoSelector.vue b/frontend/app/web-gold/src/components/VideoSelector.vue index 46afffba0f..984af1e442 100644 --- a/frontend/app/web-gold/src/components/VideoSelector.vue +++ b/frontend/app/web-gold/src/components/VideoSelector.vue @@ -176,7 +176,7 @@ const fetchVideoList = async () => { const params = { page: currentPage.value, pageSize: pageSize.value, - fileCategory: 'video', + fileCategory: 'digital_human', fileName: searchKeyword.value.trim() || undefined } @@ -239,32 +239,18 @@ const formatDuration = (seconds) => { const formatFileSize = (bytes) => { if (!bytes) return '0 B' const units = ['B', 'KB', 'MB', 'GB'] - let size = bytes - let unitIndex = 0 - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024 - unitIndex++ - } - return `${size.toFixed(1)} ${units[unitIndex]}` + const index = Math.floor(Math.log2(bytes) / 10) + const size = bytes / Math.pow(1024, index) + return `${size.toFixed(1)} ${units[index]}` } const getVideoPreviewUrl = (video) => { if (video.coverBase64) { - if (!video.coverBase64.startsWith('data:')) { - return `data:image/jpeg;base64,${video.coverBase64}` - } - return video.coverBase64 + return video.coverBase64.startsWith('data:') + ? video.coverBase64 + : `data:image/jpeg;base64,${video.coverBase64}` } - - if (video.previewUrl) { - return video.previewUrl - } - - if (video.coverUrl) { - return video.coverUrl - } - - return defaultCover + return video.previewUrl || video.coverUrl || defaultCover } const handleCancel = () => { diff --git a/frontend/app/web-gold/src/composables/useUpload.js b/frontend/app/web-gold/src/composables/useUpload.js index 0436b34d07..124ffd39a5 100644 --- a/frontend/app/web-gold/src/composables/useUpload.js +++ b/frontend/app/web-gold/src/composables/useUpload.js @@ -7,11 +7,43 @@ import { ref, reactive } from 'vue' import { message } from 'ant-design-vue' import { MaterialService } from '@/api/material' +/** + * 获取视频时长(秒) + * @param {File} file - 视频文件对象 + * @returns {Promise} 时长(秒) + */ +function getVideoDuration(file) { + return new Promise((resolve) => { + if (!file.type.startsWith('video/')) { + resolve(null) + return + } + + const video = document.createElement('video') + video.preload = 'metadata' + video.muted = true + + video.onloadedmetadata = function() { + const duration = Math.round(video.duration) + URL.revokeObjectURL(video.src) + resolve(duration) + } + + video.onerror = function() { + URL.revokeObjectURL(video.src) + resolve(null) + } + + video.src = URL.createObjectURL(file) + }) +} + /** * @typedef {Object} UploadOptions - * @property {string} fileCategory - 文件分类(video/voice/audio/image) + * @property {string} fileCategory - 文件分类(video/voice/audio/image/digital_human) * @property {number|null} groupId - 分组编号(可选,仅素材库模块使用) * @property {string|null} coverBase64 - 封面base64(可选) + * @property {number|null} duration - 视频时长(可选,视频文件自动获取) * @property {Function} onProgress - 进度回调(可选) * @property {Function} onStart - 开始回调(可选) * @property {Function} onSuccess - 成功回调(可选) @@ -94,6 +126,7 @@ export function useUpload() { fileCategory, groupId = null, coverBase64 = null, + duration: inputDuration, onProgress, onStart, onSuccess, @@ -108,7 +141,13 @@ export function useUpload() { state.progress = 0 // 通知开始 - onStart && onStart() + onStart?.() + + // 获取视频时长(如果是视频文件且未提供时长) + let duration = inputDuration + if (duration === undefined && file.type.startsWith('video/')) { + duration = await getVideoDuration(file) + } // 第一步:获取预签名URL const presignedData = await MaterialService.getPresignedUrl({ @@ -121,7 +160,7 @@ export function useUpload() { // 第二步:直传文件到OSS await uploadToOSS(file, presignedData.data, (progress) => { state.progress = progress - onProgress && onProgress(progress) + onProgress?.(progress) }) // 第三步:确认上传并保存记录 @@ -133,7 +172,7 @@ export function useUpload() { fileType: file.type, groupId, coverBase64, - duration: file.type.startsWith('video/') ? null : undefined + duration }) state.uploading = false @@ -142,14 +181,14 @@ export function useUpload() { const fileId = completeData.data?.infraFileId || completeData.data?.userFileId const fileUrl = presignedData.data.presignedUrl - onSuccess && onSuccess(fileId, fileUrl) + onSuccess?.(fileId, fileUrl) return fileId } catch (error) { state.uploading = false state.status = 'error' state.error = error.message || '上传失败' - onError && onError(error) + onError?.(error) throw error } } diff --git a/frontend/app/web-gold/src/views/kling/IdentifyFace.vue b/frontend/app/web-gold/src/views/kling/IdentifyFace.vue index 6351163a9b..b9903e95d7 100644 --- a/frontend/app/web-gold/src/views/kling/IdentifyFace.vue +++ b/frontend/app/web-gold/src/views/kling/IdentifyFace.vue @@ -205,7 +205,7 @@ diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/enums/TikFileCategoryEnum.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/enums/TikFileCategoryEnum.java index aa278d0e7a..ac4a5156b7 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/enums/TikFileCategoryEnum.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/enums/TikFileCategoryEnum.java @@ -19,7 +19,8 @@ public enum TikFileCategoryEnum implements ArrayValuable { GENERATE(2, "generate", "生成集"), AUDIO(3, "audio", "配音集"), MIX(4, "mix", "混剪集"), - VOICE(5, "voice", "声音集"); + VOICE(5, "voice", "声音集"), + DIGITAL_HUMAN(6, "digital_human", "数字人素材"); public static final Integer[] ARRAYS = Arrays.stream(values()).map(TikFileCategoryEnum::getCode).toArray(Integer[]::new); diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikOssInitServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikOssInitServiceImpl.java index 1c2774b5c3..4cf356ecd4 100644 --- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikOssInitServiceImpl.java +++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/file/service/TikOssInitServiceImpl.java @@ -77,24 +77,14 @@ public class TikOssInitServiceImpl implements TikOssInitService { /** * OSS路径信息 */ - private static class OssPathInfo { - final String ossRootPath; - final String videoPath; - final String generatePath; - final String audioPath; - final String mixPath; - final String voicePath; - - OssPathInfo(String ossRootPath, String videoPath, String generatePath, - String audioPath, String mixPath, String voicePath) { - this.ossRootPath = ossRootPath; - this.videoPath = videoPath; - this.generatePath = generatePath; - this.audioPath = audioPath; - this.mixPath = mixPath; - this.voicePath = voicePath; - } - } + private record OssPathInfo( + String ossRootPath, + String videoPath, + String generatePath, + String audioPath, + String mixPath, + String voicePath + ) {} /** * 构建OSS路径信息 @@ -176,6 +166,7 @@ public class TikOssInitServiceImpl implements TikOssInitService { case "audio" -> ossInit.getAudioPath(); case "mix" -> ossInit.getMixPath(); case "voice" -> ossInit.getVoicePath(); + case "digital_human" -> ossInit.getOssRootPath() + "/digital_human"; default -> throw exception(OSS_INIT_FAILED, "不支持的文件分类:" + fileCategory); }; }