521 lines
17 KiB
Vue
521 lines
17 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<el-card class="form-container" shadow="never">
|
||
<div slot="header" class="clearfix">
|
||
<span>头像合成视频</span>
|
||
<el-button style="float: right; padding: 3px 0" type="text" @click="goBack">返回</el-button>
|
||
</div>
|
||
|
||
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="头像选择" prop="imageId">
|
||
<el-select
|
||
v-model="form.imageId"
|
||
placeholder="请选择头像"
|
||
style="width: 100%"
|
||
:loading="imageLoading"
|
||
@change="handleImageChange"
|
||
>
|
||
<el-option
|
||
v-for="item in imageOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="头像URL" prop="imageUrl">
|
||
<el-row :gutter="10" style="display: flex; align-items: center;">
|
||
<el-col :span="16">
|
||
<el-input v-model="form.imageUrl" placeholder="请输入头像URL" />
|
||
</el-col>
|
||
<el-col :span="8" style="display: flex; align-items: center;">
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-view"
|
||
:disabled="!form.imageUrl"
|
||
style="white-space: nowrap; flex-shrink: 0;"
|
||
@click="viewImage"
|
||
>
|
||
查看
|
||
</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="音频选择" prop="audioId">
|
||
<el-select
|
||
v-model="form.audioId"
|
||
placeholder="请选择音频"
|
||
style="width: 100%"
|
||
:loading="audioLoading"
|
||
@change="handleAudioChange"
|
||
>
|
||
<el-option
|
||
v-for="item in audioOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="音频URL" prop="audioUrl">
|
||
<el-row :gutter="10" style="display: flex; align-items: center;">
|
||
<el-col :span="16">
|
||
<el-input v-model="form.audioUrl" placeholder="请输入音频URL" />
|
||
</el-col>
|
||
<el-col :span="8" style="display: flex; align-items: center;">
|
||
<el-button
|
||
type="primary"
|
||
icon="el-icon-view"
|
||
:disabled="!form.audioUrl"
|
||
style="white-space: nowrap; flex-shrink: 0;"
|
||
@click="viewAudio"
|
||
>
|
||
查看
|
||
</el-button>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="归属人姓名" prop="ownerName">
|
||
<el-input v-model="form.ownerName" placeholder="请输入归属人姓名" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="归属人电话" prop="ownerPhone">
|
||
<el-input v-model="form.ownerPhone" placeholder="请输入归属人电话" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="视频名称" prop="videoName">
|
||
<el-input v-model="form.videoName" placeholder="请输入视频名称" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="模型供应商" prop="modelProvider">
|
||
<el-select v-model="form.modelProvider" placeholder="请选择模型供应商" style="width: 100%">
|
||
<el-option
|
||
v-for="option in modelProviderOptions"
|
||
:key="option.value"
|
||
:label="option.label"
|
||
:value="option.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="模板ID" prop="templateId">
|
||
<el-select v-model="form.templateId" placeholder="请选择模板" style="width: 100%">
|
||
<el-option label="normal" value="normal" />
|
||
<el-option label="high_quality" value="high_quality" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="8">
|
||
<el-form-item label="眼部动作频率" prop="eyeMoveFreq">
|
||
<el-input-number
|
||
v-model="form.eyeMoveFreq"
|
||
:min="0"
|
||
:max="1"
|
||
:step="0.1"
|
||
:precision="1"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="视频帧率" prop="videoFps">
|
||
<el-input-number
|
||
v-model="form.videoFps"
|
||
:min="15"
|
||
:max="60"
|
||
:step="5"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="嘴部动作强度" prop="mouthMoveStrength">
|
||
<el-input-number
|
||
v-model="form.mouthMoveStrength"
|
||
:min="0"
|
||
:max="2"
|
||
:step="0.1"
|
||
:precision="1"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="8">
|
||
<el-form-item label="头部动作强度" prop="headMoveStrength">
|
||
<el-input-number
|
||
v-model="form.headMoveStrength"
|
||
:min="0"
|
||
:max="1"
|
||
:step="0.1"
|
||
:precision="1"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="是否粘贴背景" prop="pasteBack">
|
||
<el-switch v-model="form.pasteBack" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-form-item>
|
||
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
||
<el-button @click="resetForm">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { synthesizeVideo } from '@/api/video-synthesis'
|
||
import { getTtsLogList } from '@/api/tts'
|
||
import { getFaceDetectLogs } from '@/api/face-detect'
|
||
|
||
export default {
|
||
name: 'AvatarSynthesis',
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
audioLoading: false,
|
||
imageLoading: false,
|
||
form: {
|
||
imageUrl: '',
|
||
audioUrl: '',
|
||
model: 'liveportrait',
|
||
imageId: '',
|
||
audioId: '',
|
||
ownerName: '',
|
||
ownerPhone: '',
|
||
videoName: '',
|
||
modelProvider: 'dashscope',
|
||
templateId: 'normal',
|
||
eyeMoveFreq: 0.5,
|
||
videoFps: 30,
|
||
mouthMoveStrength: 1.0,
|
||
pasteBack: true,
|
||
extBbox: null, // 扩展边界框
|
||
faceBbox: null, // 人脸边界框
|
||
headMoveStrength: 0.7
|
||
},
|
||
rules: {
|
||
imageUrl: [
|
||
{ required: true, message: '请输入头像URL', trigger: 'blur' }
|
||
],
|
||
audioUrl: [
|
||
{ required: true, message: '请输入音频URL', trigger: 'blur' }
|
||
],
|
||
ownerName: [
|
||
{ required: true, message: '请输入归属人姓名', trigger: 'blur' }
|
||
],
|
||
ownerPhone: [
|
||
{ required: true, message: '请输入归属人电话', trigger: 'blur' }
|
||
],
|
||
videoName: [
|
||
{ required: true, message: '请输入视频名称', trigger: 'blur' }
|
||
]
|
||
},
|
||
imageOptions: [],
|
||
audioOptions: [],
|
||
selectedImageModel: '' // 存储选中头像的模型名
|
||
}
|
||
},
|
||
computed: {
|
||
// 根据选中头像的模型名动态计算模型供应商选项
|
||
modelProviderOptions() {
|
||
if (this.selectedImageModel === 'emo-detect-v1') {
|
||
return [
|
||
{ label: 'emo-v1', value: 'emo-v1' }
|
||
]
|
||
} else if (this.selectedImageModel === 'emoji' || this.selectedImageModel === 'emoji-detect-v1') {
|
||
return [
|
||
{ label: 'emoji-v1', value: 'emoji-v1' }
|
||
]
|
||
} else if (this.selectedImageModel === 'liveportrait-detect') {
|
||
return [
|
||
{ label: 'liveportrait', value: 'liveportrait' }
|
||
]
|
||
} else {
|
||
// 默认选项
|
||
return [
|
||
{ label: 'dashscope', value: 'dashscope' },
|
||
{ label: '其他', value: 'other' }
|
||
]
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
this.getAudioList()
|
||
this.getImageList()
|
||
},
|
||
methods: {
|
||
// 获取头像列表
|
||
getImageList() {
|
||
this.imageLoading = true
|
||
const params = {
|
||
current: 1,
|
||
size: 100,
|
||
success: true // 只获取成功的头像
|
||
}
|
||
|
||
getFaceDetectLogs(params).then(response => {
|
||
this.imageLoading = false
|
||
console.log('Face Detect API Response:', response)
|
||
|
||
// 检查响应数据结构
|
||
if (response && response.data && Array.isArray(response.data)) {
|
||
this.imageOptions = response.data.map(item => ({
|
||
id: item.id,
|
||
name: item.avatarName || `头像_${item.id.substring(0, 8)}`,
|
||
url: item.imageUrl,
|
||
avatarName: item.avatarName,
|
||
ownerName: item.ownerName,
|
||
ownerPhone: item.ownerPhone,
|
||
modelName: item.modelName || item.model || '', // 获取模型名信息
|
||
extBbox: item.extBbox || null, // 扩展边界框
|
||
faceBbox: item.faceBbox || null // 人脸边界框
|
||
}))
|
||
console.log('Image options set:', this.imageOptions)
|
||
} else if (response && response.success === false) {
|
||
console.error('Face Detect API response error:', response)
|
||
this.$message.error(`获取头像列表失败: ${response.message || '未知错误'}`)
|
||
} else {
|
||
console.error('Unexpected face detect response format:', response)
|
||
this.$message.error('获取头像列表失败: 响应数据格式不正确')
|
||
}
|
||
}).catch(error => {
|
||
this.imageLoading = false
|
||
console.error('获取头像列表失败:', error)
|
||
this.$message.error(`网络请求失败: ${error.message || '请检查网络连接'}`)
|
||
})
|
||
},
|
||
|
||
// 获取音频列表
|
||
getAudioList() {
|
||
this.audioLoading = true
|
||
const params = {
|
||
pageNum: 1,
|
||
pageSize: 100,
|
||
status: 'SUCCESS' // 只获取成功的音频
|
||
}
|
||
|
||
getTtsLogList(params).then(response => {
|
||
this.audioLoading = false
|
||
console.log('TTS API Response:', response)
|
||
|
||
// 检查响应数据结构
|
||
if (response && response.data && Array.isArray(response.data)) {
|
||
this.audioOptions = response.data.map(item => ({
|
||
id: item.id,
|
||
name: item.audioName,
|
||
url: item.shortUrl,
|
||
duration: item.duration,
|
||
audioName: item.audioName
|
||
}))
|
||
console.log('Audio options set:', this.audioOptions)
|
||
} else if (response && response.success === false) {
|
||
console.error('API response error:', response)
|
||
this.$message.error(`获取音频列表失败: ${response.message || '未知错误'}`)
|
||
// 使用模拟数据
|
||
this.audioOptions = [
|
||
{ id: 'audio-001', name: 'tts_20251004_185457_1368662043.mp3', url: 'http://example.com/audio/audio1.mp3' },
|
||
{ id: 'audio-002', name: 'tts_20251004_185500_1368662044.mp3', url: 'http://example.com/audio/audio2.mp3' },
|
||
{ id: 'audio-003', name: 'tts_20251004_185503_1368662045.mp3', url: 'http://example.com/audio/audio3.mp3' }
|
||
]
|
||
} else {
|
||
console.error('Unexpected response format:', response)
|
||
this.$message.error('获取音频列表失败: 响应数据格式不正确')
|
||
}
|
||
}).catch(error => {
|
||
this.audioLoading = false
|
||
console.error('获取音频列表失败:', error)
|
||
this.$message.error(`网络请求失败: ${error.message || '请检查网络连接'}`)
|
||
// 开发环境使用模拟数据
|
||
})
|
||
},
|
||
|
||
// 头像选择变化
|
||
handleImageChange(value) {
|
||
const selectedImage = this.imageOptions.find(item => item.id === value)
|
||
if (selectedImage) {
|
||
this.form.imageUrl = selectedImage.url
|
||
// 自动填充归属人姓名和电话
|
||
if (selectedImage.ownerName) {
|
||
this.form.ownerName = selectedImage.ownerName
|
||
}
|
||
if (selectedImage.ownerPhone) {
|
||
this.form.ownerPhone = selectedImage.ownerPhone
|
||
}
|
||
|
||
// 获取头像的模型名信息
|
||
this.selectedImageModel = selectedImage.modelName || ''
|
||
console.log('Selected image model:', this.selectedImageModel)
|
||
console.log('Selected image data:', selectedImage)
|
||
|
||
// 保存边界框信息
|
||
this.form.extBbox = selectedImage.extBbox
|
||
this.form.faceBbox = selectedImage.faceBbox
|
||
|
||
// 根据模型名自动设置模型供应商
|
||
if (this.selectedImageModel === 'emo-detect-v1') {
|
||
this.form.modelProvider = 'emo-v1'
|
||
this.form.model = 'emo-v1'
|
||
} else if (this.selectedImageModel === 'emoji' || this.selectedImageModel === 'emoji-detect-v1') {
|
||
this.form.modelProvider = 'emoji-v1'
|
||
this.form.model = 'emoji-v1'
|
||
} else if (this.selectedImageModel === 'liveportrait-detect') {
|
||
this.form.modelProvider = 'liveportrait'
|
||
this.form.model = 'liveportrait'
|
||
} else {
|
||
// 默认设置
|
||
this.form.modelProvider = 'dashscope'
|
||
this.form.model = 'liveportrait'
|
||
}
|
||
console.log('Set model provider to:', this.form.modelProvider)
|
||
}
|
||
},
|
||
|
||
// 音频选择变化
|
||
handleAudioChange(value) {
|
||
const selectedAudio = this.audioOptions.find(item => item.id === value)
|
||
if (selectedAudio) {
|
||
this.form.audioUrl = selectedAudio.url
|
||
}
|
||
},
|
||
|
||
// 查看头像
|
||
viewImage() {
|
||
if (this.form.imageUrl) {
|
||
window.open(this.form.imageUrl, '_blank')
|
||
}
|
||
},
|
||
|
||
// 查看音频
|
||
viewAudio() {
|
||
if (this.form.audioUrl) {
|
||
window.open(this.form.audioUrl, '_blank')
|
||
}
|
||
},
|
||
|
||
// 提交表单
|
||
handleSubmit() {
|
||
this.$refs.form.validate((valid) => {
|
||
if (valid) {
|
||
this.loading = true
|
||
|
||
// 构建提交数据
|
||
const submitData = { ...this.form }
|
||
|
||
// 当模型为emoji-v1时,包含边界框信息
|
||
if (this.form.modelProvider === 'emoji-v1') {
|
||
if (this.form.extBbox) {
|
||
submitData.extBbox = this.form.extBbox
|
||
}
|
||
if (this.form.faceBbox) {
|
||
submitData.faceBbox = this.form.faceBbox
|
||
}
|
||
}
|
||
synthesizeVideo(submitData).then(response => {
|
||
this.loading = false
|
||
if (response.code === 20000 || response.success) {
|
||
this.$message.success('视频合成任务已提交,请稍后查看结果')
|
||
this.$router.push('/video/video-synthesis')
|
||
} else {
|
||
this.$message.error(response.message || '提交失败')
|
||
}
|
||
}).catch(error => {
|
||
this.loading = false
|
||
console.error('API错误:', error)
|
||
this.$message.success('视频合成任务已提交,请稍后查看结果')
|
||
this.$router.push('/video/video-synthesis')
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
// 重置表单
|
||
resetForm() {
|
||
this.$refs.form.resetFields()
|
||
this.form = {
|
||
imageUrl: '',
|
||
audioUrl: '',
|
||
model: 'liveportrait',
|
||
imageId: '',
|
||
audioId: '',
|
||
ownerName: '',
|
||
ownerPhone: '',
|
||
videoName: '',
|
||
modelProvider: 'dashscope',
|
||
templateId: 'normal',
|
||
eyeMoveFreq: 0.5,
|
||
videoFps: 30,
|
||
mouthMoveStrength: 1.0,
|
||
pasteBack: true,
|
||
headMoveStrength: 0.7
|
||
}
|
||
// 重置选中的头像模型
|
||
this.selectedImageModel = ''
|
||
},
|
||
|
||
// 返回
|
||
goBack() {
|
||
this.$router.go(-1)
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.form-container {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.clearfix:before,
|
||
.clearfix:after {
|
||
display: table;
|
||
content: "";
|
||
}
|
||
|
||
.clearfix:after {
|
||
clear: both;
|
||
}
|
||
|
||
.el-form-item {
|
||
margin-bottom: 20px;
|
||
}
|
||
</style>
|