feat(video): 集成可灵AI多图参考生视频生成服务
- 替换视频生成服务为可灵AI多图参考生视频API,支持1-4张多视角图片输入 - 调整图片拼接逻辑,生成横向长图传入即梦API备用 - 实现基于JWT认证的可灵API请求和轮询机制,支持高品质1:1正方形视频生成 - 在设计详情页新增视频展示区域及生成、重新生成和下载视频操作 - 更新后台系统配置,支持配置可灵AI Access Key和Secret Key - 删除即梦视频相关配置及逻辑,所有视频生成功能切换到可灵AI实现 - 优化视频生成提示词,提升视频质感和展示效果 - 增加视频文件本地存储和路径管理,保证视频可访问和下载 - 前端增加视频生成状态管理和用户界面交互提示 - 后端添加PyJWT依赖,支持JWT认证流程
This commit is contained in:
@@ -98,6 +98,38 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展示视频区域 -->
|
||||
<div class="video-section" v-if="design.video_url || generatingVideo">
|
||||
<h4 class="section-title">展示视频</h4>
|
||||
<div v-if="generatingVideo" class="generating-state">
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
<span>正在生成展示视频,预计需要 2-5 分钟...</span>
|
||||
</div>
|
||||
<div v-else-if="design.video_url" class="video-wrapper">
|
||||
<video
|
||||
:key="design.video_url"
|
||||
controls
|
||||
preload="auto"
|
||||
crossorigin="anonymous"
|
||||
class="preview-video"
|
||||
>
|
||||
<source :src="design.video_url" type="video/mp4" />
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<div class="video-actions">
|
||||
<button class="model3d-action-btn" @click="handleDownloadVideo">
|
||||
<el-icon><Download /></el-icon>
|
||||
<span>下载视频</span>
|
||||
</button>
|
||||
<button class="model3d-action-btn" @click="handleRegenVideo">
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
<span>重新生成</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="design-info">
|
||||
<h4 class="info-title">设计详情</h4>
|
||||
<div class="info-grid">
|
||||
@@ -140,6 +172,15 @@
|
||||
<el-icon><Platform /></el-icon>
|
||||
<span>生成3D模型</span>
|
||||
</button>
|
||||
<!-- 生成展示视频按钮 -->
|
||||
<button
|
||||
v-if="!design.video_url && !generatingVideo"
|
||||
class="action-btn video-btn"
|
||||
@click="handleGenerateVideo"
|
||||
>
|
||||
<el-icon><VideoCameraFilled /></el-icon>
|
||||
<span>生成展示视频</span>
|
||||
</button>
|
||||
<button class="action-btn secondary-btn" @click="goToUserCenter">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>查看我的设计</span>
|
||||
@@ -154,7 +195,7 @@ import { useRouter } from 'vue-router'
|
||||
import { Loading, PictureFilled, ZoomIn, ZoomOut, RefreshRight, Download, User, Platform, VideoCameraFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { Design } from '@/stores/design'
|
||||
import { getDesignDownloadUrl, generate3DModelApi } from '@/api/design'
|
||||
import { getDesignDownloadUrl, generate3DModelApi, generateVideoApi } from '@/api/design'
|
||||
import request from '@/api/request'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -179,6 +220,9 @@ const scale = ref(1)
|
||||
// 3D 模型生成状态
|
||||
const generating3D = ref(false)
|
||||
|
||||
// 视频生成状态
|
||||
const generatingVideo = ref(false)
|
||||
|
||||
// 是否有多视角图片
|
||||
const hasMultipleViews = computed(() => {
|
||||
return props.design.images && props.design.images.length > 1
|
||||
@@ -252,6 +296,56 @@ const handleRegen3D = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成展示视频
|
||||
const handleGenerateVideo = async () => {
|
||||
generatingVideo.value = true
|
||||
try {
|
||||
const updated = await generateVideoApi(props.design.id)
|
||||
if (updated.video_url) {
|
||||
props.design.video_url = updated.video_url
|
||||
}
|
||||
ElMessage.success('展示视频生成成功!')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || e?.detail || '视频生成失败,请重试')
|
||||
} finally {
|
||||
generatingVideo.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重新生成展示视频
|
||||
const handleRegenVideo = async () => {
|
||||
generatingVideo.value = true
|
||||
try {
|
||||
const updated = await generateVideoApi(props.design.id, true)
|
||||
if (updated.video_url) {
|
||||
props.design.video_url = updated.video_url
|
||||
}
|
||||
ElMessage.success('展示视频重新生成成功!')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '视频重新生成失败')
|
||||
} finally {
|
||||
generatingVideo.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下载展示视频
|
||||
const handleDownloadVideo = async () => {
|
||||
const videoUrl = props.design.video_url
|
||||
if (!videoUrl) return
|
||||
try {
|
||||
const res = await fetch(videoUrl)
|
||||
if (!res.ok) throw new Error('下载失败')
|
||||
const blob = await res.blob()
|
||||
const category = props.design.category?.name || '设计'
|
||||
const subType = props.design.sub_type?.name || ''
|
||||
const filename = `${category}${subType ? '-' + subType : ''}-展示视频-${props.design.id}.mp4`
|
||||
_downloadBlob(blob, filename)
|
||||
ElMessage.success('视频下载成功')
|
||||
} catch {
|
||||
ElMessage.error('视频下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取图片URL
|
||||
const toImageUrl = (url: string | null): string => {
|
||||
if (!url) return ''
|
||||
@@ -867,4 +961,43 @@ $text-light: #999999;
|
||||
overflow: hidden;
|
||||
background: $bg-color;
|
||||
}
|
||||
|
||||
// 视频区域样式
|
||||
.video-section {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.preview-video {
|
||||
width: 100%;
|
||||
max-height: 500px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.video-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 0 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.video-btn {
|
||||
background: linear-gradient(135deg, #e040fb 0%, #7c4dff 100%) !important;
|
||||
color: #fff !important;
|
||||
border: none !important;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -97,45 +97,38 @@
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 即梦视频生成配置卡片 -->
|
||||
<!-- 可灵AI 视频生成配置卡片 -->
|
||||
<div class="section-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title-row">
|
||||
<h3 class="section-title">即梦 3.0 Pro 视频生成</h3>
|
||||
<el-tag :type="volcVideoStatus" size="small">
|
||||
{{ volcVideoStatusText }}
|
||||
<h3 class="section-title">可灵 AI 视频生成</h3>
|
||||
<el-tag :type="klingVideoStatus" size="small">
|
||||
{{ klingVideoStatusText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="card-desc">火山引擎即梦 3.0 Pro 图生视频 API,将设计图生成 360 度旋转展示视频。需要火山引擎 Access Key 和 Secret Key(非 API Key)</p>
|
||||
<p class="card-desc">可灵 AI 多图参考生视频 API,支持传入多张参考图(1-4张),AI 理解为同一物体多角度参考,生成单品 360 度旋转展示视频</p>
|
||||
</div>
|
||||
<el-form label-width="120px" class="config-form">
|
||||
<el-form-item label="Access Key">
|
||||
<el-input
|
||||
v-model="volcAccessKey"
|
||||
v-model="klingAccessKey"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入火山引擎 Access Key"
|
||||
placeholder="请输入可灵 AI Access Key"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Secret Key">
|
||||
<el-input
|
||||
v-model="volcSecretKey"
|
||||
v-model="klingSecretKey"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="请输入火山引擎 Secret Key"
|
||||
placeholder="请输入可灵 AI Secret Key"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<span class="form-tip">获取方式:火山引擎控制台 → 右上角头像 → API访问密钥,或访问 https://console.volcengine.com/iam/keymanage/</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="视频时长">
|
||||
<el-select v-model="videoFrames" style="width: 200px">
|
||||
<el-option label="2 秒(快速预览)" value="49" />
|
||||
<el-option label="5 秒(完整展示)" value="121" />
|
||||
</el-select>
|
||||
<span class="form-tip">推荐 5 秒,足够展示完整 360 度旋转</span>
|
||||
<span class="form-tip">获取方式:访问 <a href="https://klingai.com" target="_blank">可灵AI开放平台</a> → 注册/登录 → 控制台 → API密钥管理</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="视频提示词">
|
||||
<el-input
|
||||
@@ -144,7 +137,7 @@
|
||||
:rows="3"
|
||||
placeholder="玉雕作品在摄影棚内缓慢旋转360度展示全貌..."
|
||||
/>
|
||||
<span class="form-tip">用于控制视频生成效果,尽情描述旋转展示方式、灯光、背景等</span>
|
||||
<span class="form-tip">用于控制视频生成效果,留空使用默认提示词</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -230,12 +223,11 @@ const siliconflowKey = ref('')
|
||||
const siliconflowUrl = ref('https://api.siliconflow.cn/v1')
|
||||
const volcengineKey = ref('')
|
||||
const volcengineUrl = ref('https://ark.cn-beijing.volces.com/api/v3')
|
||||
const volcAccessKey = ref('')
|
||||
const volcSecretKey = ref('')
|
||||
const klingAccessKey = ref('')
|
||||
const klingSecretKey = ref('')
|
||||
const tencentSecretId = ref('')
|
||||
const tencentSecretKey = ref('')
|
||||
const videoPrompt = ref('')
|
||||
const videoFrames = ref('121')
|
||||
const model3dPrompt = ref('')
|
||||
const imageSize = ref('1024')
|
||||
const saving = ref(false)
|
||||
@@ -243,8 +235,8 @@ const saving = ref(false)
|
||||
// 后端是否已配置 API Key(脱敏值也算已配置)
|
||||
const siliconflowConfigured = ref(false)
|
||||
const volcengineConfigured = ref(false)
|
||||
const volcAccessKeyConfigured = ref(false)
|
||||
const volcSecretKeyConfigured = ref(false)
|
||||
const klingAccessKeyConfigured = ref(false)
|
||||
const klingSecretKeyConfigured = ref(false)
|
||||
const tencentSecretIdConfigured = ref(false)
|
||||
const tencentSecretKeyConfigured = ref(false)
|
||||
|
||||
@@ -259,8 +251,8 @@ const siliconflowStatus = computed(() => (siliconflowKey.value || siliconflowCon
|
||||
const siliconflowStatusText = computed(() => (siliconflowKey.value || siliconflowConfigured.value) ? '已配置' : '未配置')
|
||||
const volcengineStatus = computed(() => (volcengineKey.value || volcengineConfigured.value) ? 'success' : 'info')
|
||||
const volcengineStatusText = computed(() => (volcengineKey.value || volcengineConfigured.value) ? '已配置' : '未配置')
|
||||
const volcVideoStatus = computed(() => ((volcAccessKey.value || volcAccessKeyConfigured.value) && (volcSecretKey.value || volcSecretKeyConfigured.value)) ? 'success' : 'info')
|
||||
const volcVideoStatusText = computed(() => ((volcAccessKey.value || volcAccessKeyConfigured.value) && (volcSecretKey.value || volcSecretKeyConfigured.value)) ? '已配置' : '未配置')
|
||||
const klingVideoStatus = computed(() => ((klingAccessKey.value || klingAccessKeyConfigured.value) && (klingSecretKey.value || klingSecretKeyConfigured.value)) ? 'success' : 'info')
|
||||
const klingVideoStatusText = computed(() => ((klingAccessKey.value || klingAccessKeyConfigured.value) && (klingSecretKey.value || klingSecretKeyConfigured.value)) ? '已配置' : '未配置')
|
||||
const hunyuan3dStatus = computed(() => ((tencentSecretId.value || tencentSecretIdConfigured.value) && (tencentSecretKey.value || tencentSecretKeyConfigured.value)) ? 'success' : 'info')
|
||||
const hunyuan3dStatusText = computed(() => ((tencentSecretId.value || tencentSecretIdConfigured.value) && (tencentSecretKey.value || tencentSecretKeyConfigured.value)) ? '已配置' : '未配置')
|
||||
|
||||
@@ -288,14 +280,14 @@ const loadConfigs = async () => {
|
||||
volcengineKey.value = map['VOLCENGINE_API_KEY']
|
||||
}
|
||||
volcengineUrl.value = map['VOLCENGINE_BASE_URL'] || 'https://ark.cn-beijing.volces.com/api/v3'
|
||||
// 即梦视频 Access Key / Secret Key
|
||||
volcAccessKeyConfigured.value = !!map['VOLC_ACCESS_KEY']
|
||||
volcSecretKeyConfigured.value = !!map['VOLC_SECRET_KEY']
|
||||
if (map['VOLC_ACCESS_KEY'] && !map['VOLC_ACCESS_KEY'].includes('****')) {
|
||||
volcAccessKey.value = map['VOLC_ACCESS_KEY']
|
||||
// 可灵 AI Access Key / Secret Key
|
||||
klingAccessKeyConfigured.value = !!map['KLING_ACCESS_KEY']
|
||||
klingSecretKeyConfigured.value = !!map['KLING_SECRET_KEY']
|
||||
if (map['KLING_ACCESS_KEY'] && !map['KLING_ACCESS_KEY'].includes('****')) {
|
||||
klingAccessKey.value = map['KLING_ACCESS_KEY']
|
||||
}
|
||||
if (map['VOLC_SECRET_KEY'] && !map['VOLC_SECRET_KEY'].includes('****')) {
|
||||
volcSecretKey.value = map['VOLC_SECRET_KEY']
|
||||
if (map['KLING_SECRET_KEY'] && !map['KLING_SECRET_KEY'].includes('****')) {
|
||||
klingSecretKey.value = map['KLING_SECRET_KEY']
|
||||
}
|
||||
// 腾讯云 SecretId / SecretKey
|
||||
tencentSecretIdConfigured.value = !!map['TENCENT_SECRET_ID']
|
||||
@@ -308,7 +300,6 @@ const loadConfigs = async () => {
|
||||
}
|
||||
// 提示词配置
|
||||
videoPrompt.value = map['VIDEO_PROMPT'] || ''
|
||||
videoFrames.value = map['VIDEO_FRAMES'] || '121'
|
||||
model3dPrompt.value = map['MODEL3D_PROMPT'] || ''
|
||||
imageSize.value = map['AI_IMAGE_SIZE'] || '1024'
|
||||
} catch (e) {
|
||||
@@ -338,11 +329,11 @@ const handleSave = async () => {
|
||||
if (volcengineKey.value) {
|
||||
configs['VOLCENGINE_API_KEY'] = volcengineKey.value
|
||||
}
|
||||
if (volcAccessKey.value) {
|
||||
configs['VOLC_ACCESS_KEY'] = volcAccessKey.value
|
||||
if (klingAccessKey.value) {
|
||||
configs['KLING_ACCESS_KEY'] = klingAccessKey.value
|
||||
}
|
||||
if (volcSecretKey.value) {
|
||||
configs['VOLC_SECRET_KEY'] = volcSecretKey.value
|
||||
if (klingSecretKey.value) {
|
||||
configs['KLING_SECRET_KEY'] = klingSecretKey.value
|
||||
}
|
||||
if (tencentSecretId.value) {
|
||||
configs['TENCENT_SECRET_ID'] = tencentSecretId.value
|
||||
@@ -352,7 +343,6 @@ const handleSave = async () => {
|
||||
}
|
||||
// 提示词始终提交(包括空值,允许清空)
|
||||
configs['VIDEO_PROMPT'] = videoPrompt.value
|
||||
configs['VIDEO_FRAMES'] = videoFrames.value
|
||||
configs['MODEL3D_PROMPT'] = model3dPrompt.value
|
||||
await updateConfigsBatch(configs)
|
||||
ElMessage.success('配置已保存')
|
||||
|
||||
Reference in New Issue
Block a user