Files
smartDriveEEFront/src/views/video/avatar-synthesis.vue
2025-10-12 20:48:29 +08:00

521 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>