头像视频合成

This commit is contained in:
ZLI263
2025-10-08 08:03:30 +08:00
parent 5f0d5a0daf
commit 6209b691ab
7 changed files with 586 additions and 106 deletions

View File

@@ -0,0 +1,474 @@
<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 label="dashscope" value="dashscope" />
<el-option label="其他" value="other" />
</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,
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: []
}
},
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
}))
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 || '未知错误'}`)
// 使用模拟数据
this.imageOptions = [
{ id: 'img-001', name: '头像1', url: 'http://example.com/images/avatar1.jpg', ownerName: '张三', ownerPhone: '13800138000' },
{ id: 'img-002', name: '头像2', url: 'http://example.com/images/avatar2.jpg', ownerName: '李四', ownerPhone: '13900139000' },
{ id: 'img-003', name: '头像3', url: 'http://example.com/images/avatar3.jpg', ownerName: '王五', ownerPhone: '13700137000' }
]
} else {
console.error('Unexpected face detect response format:', response)
this.$message.error('获取头像列表失败: 响应数据格式不正确')
// 使用模拟数据
this.imageOptions = [
{ id: 'img-001', name: '头像1', url: 'http://example.com/images/avatar1.jpg', ownerName: '张三', ownerPhone: '13800138000' },
{ id: 'img-002', name: '头像2', url: 'http://example.com/images/avatar2.jpg', ownerName: '李四', ownerPhone: '13900139000' },
{ id: 'img-003', name: '头像3', url: 'http://example.com/images/avatar3.jpg', ownerName: '王五', ownerPhone: '13700137000' }
]
}
}).catch(error => {
this.imageLoading = false
console.error('获取头像列表失败:', error)
this.$message.error(`网络请求失败: ${error.message || '请检查网络连接'}`)
// 开发环境使用模拟数据
this.imageOptions = [
{ id: 'img-001', name: '头像1', url: 'http://example.com/images/avatar1.jpg', ownerName: '张三', ownerPhone: '13800138000' },
{ id: 'img-002', name: '头像2', url: 'http://example.com/images/avatar2.jpg', ownerName: '李四', ownerPhone: '13900139000' },
{ id: 'img-003', name: '头像3', url: 'http://example.com/images/avatar3.jpg', ownerName: '王五', ownerPhone: '13700137000' }
]
})
},
// 获取音频列表
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('获取音频列表失败: 响应数据格式不正确')
// 使用模拟数据
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' }
]
}
}).catch(error => {
this.audioLoading = false
console.error('获取音频列表失败:', error)
this.$message.error(`网络请求失败: ${error.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' }
]
})
},
// 头像选择变化
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
}
}
},
// 音频选择变化
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
synthesizeVideo(this.form).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
}
},
// 返回
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>