Commit 4ab1b529 authored by Dazzle Wu's avatar Dazzle Wu

feat: 视频生成配置

parent a0b1ca29
...@@ -25,14 +25,19 @@ export function fetchBackgroundImage<T>() { ...@@ -25,14 +25,19 @@ export function fetchBackgroundImage<T>() {
return request.post<T>('/bizDigitalHumanImageRest/getBackgroundImage.json') return request.post<T>('/bizDigitalHumanImageRest/getBackgroundImage.json')
} }
// 根据人物名称分页获取人物信息 // 上传背景图片
export function fetchInfoByImageName<T>(imageName: string) { export function uploadImageFile<T>(imageName: string, formData: FormData) {
return request.post<T>(`/bizDigitalHumanImageRest/findByImageName.json?imageName=${imageName}`) return request.post<T>(`/baiduDigitalHumanFileRest/uploadImageFile.json?imageName=${imageName}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 12000,
})
} }
// 根据音色ID主键获取音色 // 根据人物名称分页获取人物信息
export function fetchDigitalHumanTimbreById<T>() { export function fetchInfoByImageName<T>(imageName: string) {
return request.post<T>('/bizDigitalHumanTimbreRest/getDigitalHumanTimbreById.json') return request.post<T>(`/bizDigitalHumanImageRest/findByImageName.json?imageName=${imageName}`, {
pagingInfo: { pageNo: 1, pageSize: 10 },
})
} }
// 获取音色列表 // 获取音色列表
...@@ -44,3 +49,11 @@ export function fetchDigitalHumanTimbreList<T>() { ...@@ -44,3 +49,11 @@ export function fetchDigitalHumanTimbreList<T>() {
export function fetchTimbreByExample<T>(condition: string) { export function fetchTimbreByExample<T>(condition: string) {
return request.post<T>(`/bizDigitalHumanTimbreRest/getByExample.json?condition=${condition}`) return request.post<T>(`/bizDigitalHumanTimbreRest/getByExample.json?condition=${condition}`)
} }
// 基础数字人视频
export function createBaseVideoDigitalHumanTask<T>(callbackUrl: string, payload: object) {
return request.post<T>(
`/aiDigitalHumanServiceRest/createBaseVideoDigitalHumanTask.json?callbackUrl=${callbackUrl}`,
payload,
)
}
...@@ -31,7 +31,7 @@ const emit = defineEmits<Emits>() ...@@ -31,7 +31,7 @@ const emit = defineEmits<Emits>()
</div> </div>
<div <div
class="absolute h-8 w-20 rounded-full bg-blue-500 transition-transform duration-300" class="absolute h-8 w-20 rounded-full bg-blue-500 transition-transform duration-300"
:class="`translate-x-${value * 20}`" :style="{ transform: `translateX(${value * 80}px)` }"
></div> ></div>
</div> </div>
</template> </template>
...@@ -3,7 +3,7 @@ import Creation from '@/views/creation/creation.vue' ...@@ -3,7 +3,7 @@ import Creation from '@/views/creation/creation.vue'
export default [ export default [
{ {
path: '/fe/creation', path: '/creation',
name: 'Creation', name: 'Creation',
meta: { meta: {
rank: 1001, rank: 1001,
......
...@@ -3,49 +3,57 @@ import { DigitalTemplate } from '@/store/types/creation' ...@@ -3,49 +3,57 @@ import { DigitalTemplate } from '@/store/types/creation'
function defaultDigitalCreation(): DigitalTemplate { function defaultDigitalCreation(): DigitalTemplate {
return { return {
id: 1, id: null,
templateType: '产品营销', coverUrl: null,
videoName: '营销视频', demonstrationGifUrl: null,
demonstrationVideoUrl: null,
templateType: null,
templateName: null,
taskType: 'BASE_VIDEO', taskType: 'BASE_VIDEO',
requestId: undefined, requestId: null,
inputImageUrl: undefined, inputImageUrl: null,
driveType: 'TEXT', driveType: 'TEXT',
text: '嗨!我是挥手问候家', text: null,
ttsParams: { ttsParams: {
person: '5153', person: null,
speed: '5', speed: '5',
volume: '5', volume: '5',
pitch: '5', pitch: '5',
}, },
inputAudioUrl: undefined, inputAudioUrl: null,
callbackUrl: undefined, callbackUrl: null,
figureId: '', figureId: null,
digitalImageUrl: null,
videoParams: { videoParams: {
width: 1920, width: 0,
height: 0, height: 0,
transparent: false, transparent: false,
}, },
dhParams: { dhParams: {
cameraId: undefined, cameraId: null,
position: { position: {
x: 100, x: 0,
y: 111, y: 0,
w: undefined, w: 0,
h: undefined, h: 0,
}, },
}, },
subtitleParams: { subtitleParams: {
subtitlePolicy: 'SRT', subtitlePolicy: 'SRT',
enabled: true, enabled: false,
}, },
backgroundImageUrl: undefined, backgroundImageUrl: null,
autoAnimoji: true, autoAnimoji: false,
enablePalindrome: false, enablePalindrome: false,
templateId: null, templateId: null,
title: undefined, title: null,
logoParams: null, logoParams: {
bgmParams: null, logoUrl: null,
materialUrl: undefined, },
bgmParams: {
bgmUrl: null,
},
materialUrl: null,
} }
} }
...@@ -57,14 +65,50 @@ export const useDigitalCreationStore = defineStore('digital-creation-store', { ...@@ -57,14 +65,50 @@ export const useDigitalCreationStore = defineStore('digital-creation-store', {
state: (): DigitalTemplate => getLocalState(), state: (): DigitalTemplate => getLocalState(),
actions: { actions: {
setFigureId(figureId: string | null) { setFigureId(figureId: string) {
this.figureId = figureId this.figureId = figureId
}, },
setDigitalImageUrl(digitalImageUrl: string) {
this.digitalImageUrl = digitalImageUrl
},
setPerson(person: string) {
this.ttsParams.person = person
},
setSpeed(speed: string) {
this.ttsParams.speed = speed
},
setPitch(pitch: string) {
this.ttsParams.pitch = pitch
},
setDigitalImagePositionX(x: number) {
this.dhParams.position.x = x
},
setDigitalImagePositionY(y: number) {
this.dhParams.position.y = y
},
setDigitalImagePositionW(w: number) {
this.dhParams.position.w = w
},
setDigitalImagePositionH(h: number) {
this.dhParams.position.h = h
},
setBackgroundImageUrl(backgroundImageUrl: string) { setBackgroundImageUrl(backgroundImageUrl: string) {
this.backgroundImageUrl = backgroundImageUrl this.backgroundImageUrl = backgroundImageUrl
}, },
setSubtitleEnabled(subtitleEnabled: boolean) {
this.subtitleParams.enabled = subtitleEnabled
},
updateDigitalCreation(digitalCreation: DigitalTemplate) { updateDigitalCreation(digitalCreation: DigitalTemplate) {
this.$state = { ...this.$state, ...digitalCreation } this.$state = { ...this.$state, ...digitalCreation }
}, },
......
export interface DigitalTemplate { export interface DigitalTemplate {
id: number id: number | null
templateType: string coverUrl: string | null
videoName: string demonstrationGifUrl: string | null
demonstrationVideoUrl: string | null
templateType: string | null
templateName: string | null
taskType: string taskType: string
requestId?: string requestId: string | null
inputImageUrl?: string inputImageUrl: string | null
driveType: string driveType: string
text: string text: string | null
ttsParams: { ttsParams: {
person: string person: string | null
speed: string speed: string
volume: string volume: string
pitch: string pitch: string
} }
inputAudioUrl?: string inputAudioUrl: string | null
callbackUrl?: string callbackUrl: string | null
figureId: string | null figureId: string | null
digitalImageUrl?: string | null
videoParams: { videoParams: {
width: number width: number | null
height: number height: number | null
transparent: boolean transparent: boolean
} }
dhParams: { dhParams: {
cameraId?: number cameraId: number | null
position: { position: {
x: number x: number
y: number y: number
w?: number w: number
h?: number h: number
} }
} }
subtitleParams: { subtitleParams: {
subtitlePolicy: string subtitlePolicy: string | null
enabled: boolean enabled: boolean
} }
backgroundImageUrl?: string backgroundImageUrl: string | null
autoAnimoji: boolean autoAnimoji: boolean
enablePalindrome: boolean enablePalindrome: boolean
templateId?: any templateId: string | null
title?: string title: string | null
logoParams?: any logoParams: {
bgmParams?: any logoUrl: string | null
materialUrl?: string }
bgmParams: {
bgmUrl: string | null
}
materialUrl: string | null
}
export enum ImageType {
THREE_D = 'THREE_D',
TWO_D_BOUTIQUE = 'TWO_D_BOUTIQUE',
TWO_D_FEW_SHOT = 'TWO_D_FEW_SHOT',
BACKGROUND = 'BACKGROUND',
} }
export interface ImageItem { export interface ImageItem {
...@@ -52,9 +67,12 @@ export interface ImageItem { ...@@ -52,9 +67,12 @@ export interface ImageItem {
imageUrl: string imageUrl: string
} }
export enum ImageType { export interface TimbreItem {
THREE_D = 'THREE_D', id: number
TWO_D_BOUTIQUE = 'TWO_D_BOUTIQUE', name: string
TWO_D_FEW_SHOT = 'TWO_D_FEW_SHOT', timebreId: string
BACKGROUND = 'BACKGROUND', sex: string
style: string[]
applyScene: string[]
audioUrl: string
} }
<script setup lang="ts">
import { fetchBackgroundImage } from '@/apis/digital-creation'
import { useDigitalCreationStore } from '@/store/modules/creation'
import { ImageItem } from '@/store/types/creation'
import { ref } from 'vue'
const digitalCreationStore = useDigitalCreationStore()
const imageList = ref<ImageItem[]>([])
const uploadLoading = ref(false)
const showCropModal = ref(false)
const loaded = ref(Array(imageList.value.length).fill(false))
getBackgroundImageList()
async function getBackgroundImageList() {
const res = await fetchBackgroundImage<ImageItem[]>()
if (res.code === 0) {
imageList.value = res.data.map((i) => ({ ...i, checked: i.imageUrl === digitalCreationStore.backgroundImageUrl }))
loaded.value = Array(res.data.length).fill(false)
}
}
function uploadImage() {
showCropModal.value = true
}
function handleClickImage(image: ImageItem) {
digitalCreationStore.setBackgroundImageUrl(image.imageUrl)
}
function handleDelete() {
window.$dialog.warning({
title: '刪除圖片',
content: '是否刪除該圖片?',
positiveText: '是',
negativeText: '否',
onPositiveClick: () => {
window.$message.success('刪除成功')
},
onNegativeClick: () => {
window.$message.error('刪除失敗')
},
})
}
function onImageLoaded(index: number) {
loaded.value[index] = true
}
</script>
<template>
<n-input round placeholder="搜索">
<template #prefix>
<CustomIcon class="text-lg" icon="mingcute:search-line" />
</template>
</n-input>
<div class="h-4"></div>
<n-grid :x-gap="12" :y-gap="12" :cols="3">
<n-gi>
<n-spin :show="uploadLoading">
<label
class="h-22 w-22 hover:border-blue flex cursor-pointer flex-col items-center justify-center rounded-lg border border-gray-200"
for="upload"
>
<CustomIcon class="text-lg" icon="mingcute:add-line" />
</label>
<input id="upload" type="file" accept="image/*" class="hidden" @change="uploadImage" />
<template #description>上傳中</template>
</n-spin>
</n-gi>
<n-gi v-for="(image, index) in imageList" :key="index">
<n-spin :show="!loaded[index]">
<div
class="h-22 w-22 group relative cursor-pointer overflow-hidden rounded-lg border border-2"
:class="image.imageUrl === digitalCreationStore.backgroundImageUrl ? 'border-blue' : 'border-transparent'"
@click="handleClickImage(image)"
>
<img class="h-full w-full object-contain" :src="image.imageUrl" @load="onImageLoaded(index)" />
<div class="from-gray absolute bottom-0 h-5 w-full bg-gradient-to-t px-1 text-xs leading-5 text-white">
{{ image.imageName }}
</div>
<div
class="absolute right-1 top-1 hidden h-7 w-7 cursor-pointer items-center justify-center rounded-md bg-black/40 p-1 group-hover:flex"
@click.stop="handleDelete"
>
<CustomIcon icon="mi:delete" class="text-lg text-white" />
</div>
</div>
</n-spin>
</n-gi>
</n-grid>
<n-modal v-model:show="showCropModal" preset="card" title="裁剪圖片"> 内容 </n-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const x = ref(0)
const y = ref(0)
const w = ref(0)
const h = ref(0)
const isLock = ref(false)
</script>
<template>
<div>背景位置</div>
<div class="mt-4 flex gap-4">
<n-input-number v-model:value="x">
<template #prefix>X</template>
</n-input-number>
<n-input-number v-model:value="y">
<template #prefix>Y</template>
</n-input-number>
</div>
<div class="mt-4 flex gap-4">
<n-input-number v-model:value="w">
<template #prefix>W</template>
</n-input-number>
<n-input-number v-model:value="h">
<template #prefix>H</template>
</n-input-number>
</div>
<div class="mt-4 flex items-center justify-between">
<span>鎖定背景</span>
<n-switch v-model:value="isLock" />
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import BackgroundImages from './background-images.vue' import { fetchBackgroundImage, fetchInfoByImageName, uploadImageFile } from '@/apis/digital-creation'
// import BackgroundPosition from './background-position.vue' import { useDigitalCreationStore } from '@/store/modules/creation'
import { ImageItem } from '@/store/types/creation'
import { ref } from 'vue'
const digitalCreationStore = useDigitalCreationStore()
const imageList = ref<ImageItem[]>([])
const searchName = ref('')
const uploadLoading = ref(false)
const loaded = ref(Array(imageList.value.length).fill(false))
getBackgroundImageList()
async function getBackgroundImageList() {
const res = await fetchBackgroundImage<ImageItem[]>()
if (res.code === 0) {
imageList.value = res.data.map((i) => ({ ...i, checked: i.imageUrl === digitalCreationStore.backgroundImageUrl }))
}
}
async function handleSearch(value: string) {
const res = await fetchInfoByImageName<ImageItem[]>(value)
if (res.code === 0) {
imageList.value = res.data
}
}
async function uploadImage(event: any) {
const e = window.event || event
const file = e.target.files[0]
const fileName = file.name.substring(0, file.name.lastIndexOf('.'))
const maxImageSize = 1024 * 1024 * 3
if (!['png', 'jpg', 'jpeg'].includes(file.type.split('/')[1])) {
window.$message.error('必须为png或者jpg格式')
return
}
if (file.size > maxImageSize) {
window.$message.error('图片不能超过3MB')
return
}
const URL = window.URL || window.webkitURL
const img = new Image()
img.src = URL.createObjectURL(file)
img.onload = async function () {
const formData = new FormData()
formData.append('file', file)
uploadLoading.value = true
const res = await uploadImageFile(fileName, formData).finally(() => (uploadLoading.value = false))
if (res.code === 0) {
getBackgroundImageList()
}
}
}
function handleClickImage(image: ImageItem) {
digitalCreationStore.setBackgroundImageUrl(image.imageUrl)
}
function handleDelete() {
window.$dialog.warning({
title: '刪除圖片',
content: '是否刪除該圖片?',
positiveText: '是',
negativeText: '否',
onPositiveClick: () => {
window.$message.success('刪除成功')
},
onNegativeClick: () => {
window.$message.error('刪除失敗')
},
})
}
function onImageLoaded(index: number) {
loaded.value[index] = true
}
</script> </script>
<template> <template>
<n-tabs type="line" animated> <n-tabs type="line" animated class="h-full">
<n-tab-pane name="images" tab="圖片"> <n-tab-pane name="images" tab="圖片" class="h-full">
<BackgroundImages /> <div class="h-full overflow-y-auto px-4 py-2">
<n-input v-model:value="searchName" round placeholder="搜索" @input="handleSearch">
<template #prefix>
<CustomIcon class="text-lg" icon="mingcute:search-line" />
</template>
</n-input>
<div class="h-4"></div>
<n-grid :x-gap="12" :y-gap="12" :cols="3">
<n-gi>
<n-spin :show="uploadLoading">
<label
class="h-22 w-22 hover:border-blue flex cursor-pointer flex-col items-center justify-center rounded-lg border border-gray-200"
for="upload"
>
<CustomIcon class="text-lg" icon="mingcute:add-line" />
</label>
<input id="upload" type="file" accept="image/*" class="hidden" @change="uploadImage" />
<template #description>上傳中</template>
</n-spin>
</n-gi>
<n-gi v-for="(image, index) in imageList" :key="index">
<n-spin :show="!loaded[index]">
<div
class="h-22 w-22 group relative cursor-pointer overflow-hidden rounded-lg border border-2 bg-gray-100"
:class="
image.imageUrl === digitalCreationStore.backgroundImageUrl ? 'border-blue' : 'border-transparent'
"
@click="handleClickImage(image)"
>
<img class="h-full w-full object-contain" :src="image.imageUrl" @load="onImageLoaded(index)" />
<div
class="absolute bottom-0 h-5 w-full bg-gradient-to-t from-gray-600 px-1 text-xs leading-5 text-white"
>
{{ image.imageName }}
</div>
<div
class="absolute right-1 top-1 hidden h-7 w-7 cursor-pointer items-center justify-center rounded-md bg-black/40 p-1 group-hover:flex"
@click.stop="handleDelete"
>
<CustomIcon icon="mi:delete" class="text-lg text-white" />
</div>
</div>
</n-spin>
</n-gi>
</n-grid>
</div>
</n-tab-pane> </n-tab-pane>
<!-- <n-tab-pane name="position" tab="位置">
<BackgroundPosition />
</n-tab-pane> -->
</n-tabs> </n-tabs>
</template> </template>
<style lang="scss" scoped>
:deep(.n-tabs-nav-scroll-wrapper) {
padding: 0 16px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'
const active = ref(false)
</script>
<template>
<n-tabs type="line" animated>
<n-tab-pane name="caption" tab="字幕">
<div class="flex items-center justify-between">
<span>是否開啓</span>
<n-switch v-model:value="active" />
</div>
</n-tab-pane>
</n-tabs>
</template>
<script setup lang="ts">
import { useDigitalCreationStore } from '@/store/modules/creation'
import { TimbreItem } from '@/store/types/creation'
import { computed, ref } from 'vue'
interface Props {
value?: TimbreItem
showToggle?: boolean
}
interface Emits {
(e: 'click', value: TimbreItem): void
(e: 'toggle', value: boolean): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
const digitalCreationStore = useDigitalCreationStore()
const digitalAudio = ref<HTMLAudioElement>()
const person = computed(() => digitalCreationStore.ttsParams.person)
function playAudio() {
digitalAudio.value?.play()
}
</script>
<template>
<div
class="relative mb-4 flex items-center gap-2 rounded-2xl border p-2 hover:shadow"
:class="!showToggle && value?.timebreId === person ? 'border-blue' : 'border-gray-200'"
@click="emit('click', value!)"
>
<div class="h-16 w-16 rounded-lg bg-gray-200"></div>
<div class="flex-1 overflow-hidden">
<div class="mb-2 flex items-center gap-2">
<div class="max-w-32 truncate">{{ value?.name }}</div>
<CustomIcon class="cursor-pointer text-lg" icon="mingcute:volume-line" @click.stop.prevent="playAudio" />
</div>
<div class="flex gap-2">
<n-tag v-for="(style, index) in value?.style" :key="index" type="warning" round>{{ style }}</n-tag>
</div>
</div>
<div v-if="showToggle" class="absolute right-2 top-2">
<CustomIcon class="cursor-pointer text-lg" icon="ant-design:swap-outlined" @click="emit('toggle', true)" />
</div>
</div>
<audio ref="digitalAudio" :src="value?.audioUrl"></audio>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { fetchDigitalHumanTimbreList, fetchTimbreByExample } from '@/apis/digital-creation'
import { useDigitalCreationStore } from '@/store/modules/creation'
import { TimbreItem } from '@/store/types/creation'
import { computed, onMounted, ref, watch } from 'vue'
import DigitalAudioCard from './digital-audio-card.vue'
const pace = ref(1) const digitalCreationStore = useDigitalCreationStore()
const intonation = ref(1) const sexValue = ref(0)
const showAll = ref(false) const sexList = [
const tabValue = ref(0) { key: 0, label: '女性' },
const tablist = [ { key: 1, label: '男性' },
{ label: '女性', key: 0 },
{ label: '男性', key: 1 },
] ]
const digitalTimbreValue = ref<TimbreItem>()
const digitalTimbreList = ref<TimbreItem[]>([])
const digitalTimbreFemaleList = ref<TimbreItem[]>([])
const digitalTimbreMaleList = ref<TimbreItem[]>([])
const showAll = ref(false)
const searchName = ref('')
const speed = computed({
get() {
return Number(digitalCreationStore.ttsParams.speed)
},
set(value) {
digitalCreationStore.setSpeed(String(value))
},
})
const pitch = computed({
get() {
return Number(digitalCreationStore.ttsParams.pitch)
},
set(value) {
digitalCreationStore.setPitch(String(value))
},
})
watch(
() => [digitalCreationStore.ttsParams.person, digitalTimbreList.value.length],
([person, len]) => {
if (person && len) {
digitalTimbreValue.value = digitalTimbreList.value.find((i) => i.timebreId === person)
}
},
)
onMounted(() => {
getDigitalTimbreList()
})
async function getDigitalTimbreList() {
const res = await fetchDigitalHumanTimbreList<TimbreItem[]>()
if (res.code === 0) {
digitalTimbreList.value = res.data
digitalTimbreFemaleList.value = digitalTimbreList.value.filter((i) => i.sex === '女')
digitalTimbreMaleList.value = digitalTimbreList.value.filter((i) => i.sex === '男')
}
}
async function handleSearch(value: string) {
const res = await fetchTimbreByExample<TimbreItem[]>(value)
if (res.code === 0) {
digitalTimbreList.value = res.data
digitalTimbreFemaleList.value = digitalTimbreList.value.filter((i) => i.sex === '女')
digitalTimbreMaleList.value = digitalTimbreList.value.filter((i) => i.sex === '男')
}
}
function handleClickAudioCard(timbreItem: TimbreItem) {
digitalCreationStore.setPerson(timbreItem.timebreId)
}
</script> </script>
<template> <template>
<div v-if="!showAll"> <div class="h-full overflow-y-auto px-4 py-2">
<div class="relative flex items-center gap-2 rounded-2xl border p-2"> <div v-if="!showAll">
<div class="h-16 w-16 rounded-lg bg-gray-200"></div> <DigitalAudioCard :value="digitalTimbreValue" show-toggle @toggle="showAll = true" />
<div class="flex-1 overflow-hidden">
<div class="mb-2 flex items-center gap-2"> <div class="mt-4 text-lg">聲音</div>
<div class="max-w-24 truncate">度清風</div> <div class="mt-4 flex items-center gap-2">
<CustomIcon class="cursor-pointer text-lg" icon="mingcute:volume-line" /> <div class="w-12">語速:</div>
</div> <n-slider v-model:value="speed" class="flex-1" :max="15" :min="0" :step="1" />
<div class="flex gap-2"> <div class="w-10">{{ speed }}</div>
<n-tag type="warning" round>知性大方</n-tag>
<n-tag type="success" round>客服助理</n-tag>
<n-tag type="success" round>知性大方</n-tag>
<n-tag type="warning" round>客服助理</n-tag>
</div>
</div> </div>
<div class="absolute right-2 top-2"> <div class="mt-4 flex items-center gap-2">
<CustomIcon class="cursor-pointer text-lg" icon="ant-design:swap-outlined" @click="showAll = true" /> <div class="w-12">語調:</div>
<n-slider v-model:value="pitch" class="flex-1" :max="15" :min="0" :step="1" />
<div class="w-10">{{ pitch }}</div>
</div> </div>
</div> </div>
<div class="mt-4 text-lg">聲音</div> <div v-else>
<div class="mt-4 flex items-center gap-2"> <div class="flex items-center gap-4 pb-3">
<div class="w-12">語速:</div> <n-button text @click="showAll = false">
<n-slider v-model:value="pace" class="flex-1" :max="1.5" :min="0.5" :step="0.25" /> <template #icon>
<div class="w-10">{{ pace }}x</div> <CustomIcon class="text-lg" icon="mingcute:left-line" />
</div> </template>
<div class="mt-4 flex items-center gap-2"> 返回
<div class="w-12">語調:</div> </n-button>
<n-slider v-model:value="intonation" class="flex-1" :max="5" :min="1" :step="1" /> <n-input v-model:value="searchName" round placeholder="搜索" @input="handleSearch">
<div class="w-10">{{ intonation }}</div> <template #prefix>
</div> <CustomIcon class="text-lg" icon="mingcute:search-line" />
</div> </template>
</n-input>
<div v-else> </div>
<div class="flex items-center gap-4 pb-3">
<n-button text @click="showAll = false">
<template #icon>
<CustomIcon class="text-lg" icon="mingcute:left-line" />
</template>
返回
</n-button>
<n-input round placeholder="搜索">
<template #prefix>
<CustomIcon class="text-lg" icon="mingcute:search-line" />
</template>
</n-input>
</div>
<div class="flex justify-end pb-3"> <div class="flex justify-end pb-3">
<HorizontalTabs v-model:value="tabValue" :list="tablist" /> <HorizontalTabs v-model:value="sexValue" :list="sexList" />
</div> </div>
<div> <div>
<div class="flex items-center gap-2 rounded-2xl border p-2"> <DigitalAudioCard
<div class="h-16 w-16 rounded-lg bg-gray-200"></div> v-for="(timbre, index) in sexValue ? digitalTimbreMaleList : digitalTimbreFemaleList"
<div class="flex-1 overflow-hidden"> :key="index"
<div class="mb-2 flex items-center gap-2"> :value="timbre"
<div class="max-w-24 truncate">度清風</div> @click="handleClickAudioCard"
<CustomIcon class="cursor-pointer text-lg" icon="mingcute:volume-line" /> />
</div>
<div class="flex gap-2">
<n-tag type="warning" round>知性大方</n-tag>
<n-tag type="success" round>客服助理</n-tag>
<n-tag type="success" round>知性大方</n-tag>
<n-tag type="warning" round>客服助理</n-tag>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { ImageItem } from '@/store/types/creation'
import { useDigitalCreationStore } from '@/store/modules/creation' import { useDigitalCreationStore } from '@/store/modules/creation'
import { ImageItem } from '@/store/types/creation'
interface Props { interface Props {
value: ImageItem value: ImageItem
} }
interface Emits { interface Emits {
(e: 'click', id: string | null): void (e: 'click', value: ImageItem): void
} }
defineProps<Props>() defineProps<Props>()
...@@ -18,18 +18,18 @@ const digitalCreationStore = useDigitalCreationStore() ...@@ -18,18 +18,18 @@ const digitalCreationStore = useDigitalCreationStore()
<template> <template>
<div <div
class="hover:border-blue relative h-28 w-16 cursor-pointer overflow-hidden rounded border border-2" class="hover:border-blue relative h-28 w-16 cursor-pointer rounded border border-2 bg-gray-100"
:class="digitalCreationStore.figureId === value.figureId ? 'border-blue' : 'border-transparent'" :class="digitalCreationStore.figureId === value.figureId ? 'border-blue' : 'border-transparent'"
@click="emit('click', value.figureId)" @click="emit('click', value)"
> >
<img :src="value.imageUrl" /> <img :src="value.imageUrl" />
<div class="from-gray absolute bottom-0 h-5 w-full bg-gradient-to-t px-1 text-xs leading-5 text-white"> <div class="absolute bottom-0 h-5 w-full bg-gradient-to-t from-gray-600 px-1 text-xs leading-5 text-white">
{{ value.imageName }} {{ value.imageName }}
</div> </div>
<!-- <CustomIcon <CustomIcon
v-if="digitalCreationStore.figureId === value.figureId" v-if="digitalCreationStore.figureId === value.figureId"
icon="teenyicons:tick-circle-solid" icon="si-glyph:checked"
class="text-blue absolute -left-2 -top-2 text-lg" class="text-blue absolute left-0 top-0 text-xs"
/> --> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { fetch2DBoutiqueImageList, fetch2DFewShotImageList, fetch3DImageList } from '@/apis/digital-creation' import {
fetch2DBoutiqueImageList,
fetch2DFewShotImageList,
fetch3DImageList,
fetchInfoByImageName,
} from '@/apis/digital-creation'
import { useDigitalCreationStore } from '@/store/modules/creation'
import { ImageItem, ImageType } from '@/store/types/creation' import { ImageItem, ImageType } from '@/store/types/creation'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import DigitalCard from './digital-card.vue' import DigitalCard from './digital-human-card.vue'
import { useDigitalCreationStore } from '@/store/modules/creation'
const digitalCreationStore = useDigitalCreationStore() const digitalCreationStore = useDigitalCreationStore()
const threeDImageList = ref<ImageItem[]>([]) const threeDImageList = ref<ImageItem[]>([])
...@@ -11,6 +16,7 @@ const twoDBoutiqueImageList = ref<ImageItem[]>([]) ...@@ -11,6 +16,7 @@ const twoDBoutiqueImageList = ref<ImageItem[]>([])
const twoDFewShotImageList = ref<ImageItem[]>([]) const twoDFewShotImageList = ref<ImageItem[]>([])
const allImageList = ref<ImageItem[]>([]) const allImageList = ref<ImageItem[]>([])
const showAll = ref(false) const showAll = ref(false)
const searchName = ref('')
onMounted(() => { onMounted(() => {
getDigitalImageList() getDigitalImageList()
...@@ -27,8 +33,16 @@ async function getDigitalImageList() { ...@@ -27,8 +33,16 @@ async function getDigitalImageList() {
res3.code === 0 && (twoDFewShotImageList.value = res3.data) res3.code === 0 && (twoDFewShotImageList.value = res3.data)
} }
function handleClickDigitalImage(id: string | null) { async function handleSearch(value: string) {
digitalCreationStore.setFigureId(id) const res = await fetchInfoByImageName<ImageItem[]>(value)
if (res.code === 0) {
allImageList.value = res.data
}
}
function handleClickDigitalImage(digitalItem: ImageItem) {
digitalCreationStore.setFigureId(digitalItem.figureId!)
digitalCreationStore.setDigitalImageUrl(digitalItem.imageUrl)
} }
function handleClickAll(imageType: ImageType) { function handleClickAll(imageType: ImageType) {
...@@ -48,69 +62,71 @@ function handleClickAll(imageType: ImageType) { ...@@ -48,69 +62,71 @@ function handleClickAll(imageType: ImageType) {
</script> </script>
<template> <template>
<div v-if="!showAll"> <div class="h-full overflow-y-auto px-4 py-2">
<div class="pb-4"> <div v-if="!showAll">
<div class="flex items-center justify-between pb-3"> <div class="pb-4">
<span>3D數字人</span> <div class="flex items-center justify-between pb-3 leading-8">
<span class="text-gray cursor-pointer text-xs" @click="handleClickAll(ImageType.THREE_D)">全部</span> <span>3D數字人</span>
</div> <span class="text-gray cursor-pointer text-xs" @click="handleClickAll(ImageType.THREE_D)">全部</span>
<div class="flex flex-wrap gap-3"> </div>
<DigitalCard <div class="flex flex-wrap gap-3">
v-for="item in threeDImageList.slice(0, 4)" <DigitalCard
:key="item.id" v-for="item in threeDImageList.slice(0, 4)"
:value="item" :key="item.id"
@click="handleClickDigitalImage" :value="item"
/> @click="handleClickDigitalImage"
/>
</div>
</div> </div>
</div>
<div class="pb-4"> <div class="pb-4">
<div class="flex items-center justify-between pb-3"> <div class="flex items-center justify-between pb-3 leading-8">
<span>2D精品數字人</span> <span>2D精品數字人</span>
<span class="text-gray cursor-pointer text-xs" @click="handleClickAll(ImageType.TWO_D_BOUTIQUE)">全部</span> <span class="text-gray cursor-pointer text-xs" @click="handleClickAll(ImageType.TWO_D_BOUTIQUE)">全部</span>
</div>
<div class="flex flex-wrap gap-3">
<DigitalCard
v-for="item in twoDBoutiqueImageList.slice(0, 4)"
:key="item.id"
:value="item"
@click="handleClickDigitalImage"
/>
</div>
</div> </div>
<div class="flex flex-wrap gap-3">
<DigitalCard <div class="pb-4">
v-for="item in twoDBoutiqueImageList.slice(0, 4)" <div class="flex items-center justify-between pb-3 leading-8">
:key="item.id" <span>2D小樣本數字人</span>
:value="item" <span class="text-gray cursor-pointer text-xs" @click="handleClickAll(ImageType.TWO_D_FEW_SHOT)">全部</span>
@click="handleClickDigitalImage" </div>
/> <div class="flex flex-wrap gap-3">
<DigitalCard
v-for="item in twoDFewShotImageList.slice(0, 4)"
:key="item.id"
:value="item"
@click="handleClickDigitalImage"
/>
</div>
</div> </div>
</div> </div>
<div class="pb-4"> <div v-else>
<div class="flex items-center justify-between pb-3"> <div class="flex items-center gap-4 pb-3">
<span>2D小樣本數字人</span> <n-button text @click="showAll = false">
<span class="text-gray cursor-pointer text-xs" @click="handleClickAll(ImageType.TWO_D_FEW_SHOT)">全部</span> <template #icon>
<CustomIcon class="text-lg" icon="mingcute:left-line" />
</template>
返回
</n-button>
<n-input v-model:value="searchName" round placeholder="搜索" @input="handleSearch">
<template #prefix>
<CustomIcon class="text-lg" icon="mingcute:search-line" />
</template>
</n-input>
</div> </div>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
<DigitalCard <DigitalCard v-for="item in allImageList" :key="item.id" :value="item" @click="handleClickDigitalImage" />
v-for="item in twoDFewShotImageList.slice(0, 4)"
:key="item.id"
:value="item"
@click="handleClickDigitalImage"
/>
</div> </div>
</div> </div>
</div> </div>
<div v-else>
<div class="flex items-center gap-4 pb-3">
<n-button text @click="showAll = false">
<template #icon>
<CustomIcon class="text-lg" icon="mingcute:left-line" />
</template>
返回
</n-button>
<n-input round placeholder="搜索">
<template #prefix>
<CustomIcon class="text-lg" icon="mingcute:search-line" />
</template>
</n-input>
</div>
<div class="flex flex-wrap gap-3">
<DigitalCard v-for="item in allImageList" :key="item.id" :value="item" @click="handleClickDigitalImage" />
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { useDigitalCreationStore } from '@/store/modules/creation'
import { computed } from 'vue'
const x = ref(0) const digitalCreationStore = useDigitalCreationStore()
const y = ref(0)
const w = ref(0) const digitalImagePositionX = computed({
const h = ref(0) get() {
return digitalCreationStore.dhParams.position.x
},
set(value) {
digitalCreationStore.setDigitalImagePositionX(value)
},
})
const digitalImagePositionY = computed({
get() {
return digitalCreationStore.dhParams.position.y
},
set(value) {
digitalCreationStore.setDigitalImagePositionY(value)
},
})
const digitalImagePositionW = computed({
get() {
return digitalCreationStore.dhParams.position.w
},
set(width) {
const height = (width * 16) / 9
digitalCreationStore.setDigitalImagePositionW(width)
digitalCreationStore.setDigitalImagePositionH(parseInt(height + ''))
},
})
const digitalImagePositionH = computed({
get() {
return digitalCreationStore.dhParams.position.h
},
set(height) {
const width = (height * 9) / 16
digitalCreationStore.setDigitalImagePositionH(height)
digitalCreationStore.setDigitalImagePositionW(parseInt(width + ''))
},
})
</script> </script>
<template> <template>
<div>數字人位置</div> <div class="h-full overflow-y-auto px-4 py-2">
<div class="mt-4 flex gap-4"> <div>數字人位置</div>
<n-input-number v-model:value="x"> <div class="mt-4 flex gap-4">
<template #prefix>X</template> <n-input-number v-model:value="digitalImagePositionX">
</n-input-number> <template #prefix><div class="text-gray w-4 text-center text-xs">X</div></template>
<n-input-number v-model:value="y"> </n-input-number>
<template #prefix>Y</template> <n-input-number v-model:value="digitalImagePositionY">
</n-input-number> <template #prefix><div class="text-gray w-4 text-center text-xs">Y</div></template>
</div> </n-input-number>
<div class="mt-4 flex gap-4"> </div>
<n-input-number v-model:value="w"> <div class="mt-4 flex">
<template #prefix>W</template> <n-input-number v-model:value="digitalImagePositionW" class="flex-1">
</n-input-number> <template #prefix><div class="text-gray w-4 text-center text-xs">W</div></template>
<n-input-number v-model:value="h"> </n-input-number>
<template #prefix>H</template> <div class="flex w-4 items-center justify-center">
</n-input-number> <CustomIcon class="text-gray text-xs" icon="fa6-solid:lock" />
</div>
<n-input-number v-model:value="digitalImagePositionH" class="flex-1">
<template #prefix><div class="text-gray w-4 text-center text-xs">H</div></template>
</n-input-number>
</div>
</div> </div>
</template> </template>
...@@ -5,15 +5,21 @@ import DigitalPosition from './digital-position.vue' ...@@ -5,15 +5,21 @@ import DigitalPosition from './digital-position.vue'
</script> </script>
<template> <template>
<n-tabs type="line" animated> <n-tabs type="line" animated class="h-full">
<n-tab-pane name="human" tab="選擇"> <n-tab-pane name="human" tab="選擇" class="h-full">
<DigitalHuman /> <DigitalHuman />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="position" tab="位置"> <n-tab-pane name="position" tab="位置" class="h-full">
<DigitalPosition /> <DigitalPosition />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="audio" tab="聲音"> <n-tab-pane name="audio" tab="聲音" class="h-full">
<DigitalAudio /> <DigitalAudio />
</n-tab-pane> </n-tab-pane>
</n-tabs> </n-tabs>
</template> </template>
<style lang="scss" scoped>
:deep(.n-tabs-nav-scroll-wrapper) {
padding: 0 16px;
}
</style>
<script setup lang="ts"> <script setup lang="ts">
import { useDigitalCreationStore } from '@/store/modules/creation' import { useDigitalCreationStore } from '@/store/modules/creation'
import { computed, ref } from 'vue'
const digitalCreationStore = useDigitalCreationStore() const digitalCreationStore = useDigitalCreationStore()
const previewContent = ref<HTMLElement>()
const previewContentWidth = computed(() => previewContent.value?.offsetWidth)
const previewContentHeight = computed(() => previewContent.value?.offsetHeight)
const digitalHumanPosition = computed(() => digitalCreationStore.dhParams.position)
const digitalHumanWidth = computed(() => (digitalHumanPosition.value.w * previewContentWidth.value!) / 1080)
const digitalHumanHeight = computed(() => (digitalHumanPosition.value.h * previewContentHeight.value!) / 1920)
const digitalHumanLeft = computed(() => (digitalHumanPosition.value.x * previewContentWidth.value!) / 1080)
const digitalHumanTop = computed(() => (digitalHumanPosition.value.y * previewContentHeight.value!) / 1920)
</script> </script>
<template> <template>
<div class="flex flex-col overflow-hidden rounded-2xl"> <div class="flex flex-col overflow-hidden rounded-2xl">
<div class="flex-1 overflow-hidden bg-gray-200"> <div class="flex-1 overflow-hidden bg-gray-200">
<div class="relative mx-auto aspect-[9/16] h-full bg-green-50"> <div ref="previewContent" class="relative mx-auto aspect-[9/16] h-full overflow-hidden bg-gray-100">
<img <img
v-show="digitalCreationStore.backgroundImageUrl" v-show="digitalCreationStore.backgroundImageUrl"
:src="digitalCreationStore.backgroundImageUrl" :src="digitalCreationStore.backgroundImageUrl!"
class="absolute h-full w-full object-cover" class="absolute h-full w-full object-cover"
/> />
<img
v-show="digitalCreationStore.digitalImageUrl"
:src="digitalCreationStore.digitalImageUrl!"
class="absolute max-w-none object-fill"
:style="{
width: `${digitalHumanWidth}px`,
height: `${digitalHumanHeight}px`,
left: `${digitalHumanLeft}px`,
top: `${digitalHumanTop}px`,
}"
/>
</div> </div>
</div> </div>
<div class="flex bg-white p-4"> <div class="flex bg-white p-4">
......
<script setup lang="ts">
import { useDigitalCreationStore } from '@/store/modules/creation'
import { computed } from 'vue'
const digitalCreationStore = useDigitalCreationStore()
const subtitleEnabled = computed({
get() {
return digitalCreationStore.subtitleParams.enabled
},
set(value) {
digitalCreationStore.setSubtitleEnabled(value)
},
})
</script>
<template>
<n-tabs type="line" animated class="h-full">
<n-tab-pane name="subtitle" tab="字幕" class="h-full">
<div class="flex h-full items-center justify-between overflow-y-auto px-4 py-2">
<span>是否開啓</span>
<n-switch v-model:value="subtitleEnabled" />
</div>
</n-tab-pane>
</n-tabs>
</template>
<style lang="scss" scoped>
:deep(.n-tabs-nav-scroll-wrapper) {
padding: 0 16px;
}
</style>
<script setup lang="ts"> <script setup lang="ts">
import { fetchDigitalHumanTemplateStatus } from '@/apis/digital-creation' import { fetchDigitalHumanTemplateStatus } from '@/apis/digital-creation'
import { useDigitalCreationStore } from '@/store/modules/creation'
import { DigitalTemplate } from '@/store/types/creation' import { DigitalTemplate } from '@/store/types/creation'
import { onMounted } from 'vue'
import HeaderBar from './header-bar.vue' import HeaderBar from './header-bar.vue'
import MainContent from './main-content.vue' import MainContent from './main-content.vue'
import SideBar from './side-bar.vue' import SideBar from './side-bar.vue'
import { onMounted } from 'vue'
import { useDigitalCreationStore } from '@/store/modules/creation'
const digitalCreationStore = useDigitalCreationStore() const digitalCreationStore = useDigitalCreationStore()
...@@ -16,7 +16,15 @@ onMounted(() => { ...@@ -16,7 +16,15 @@ onMounted(() => {
async function getDigitalImageList(id: number) { async function getDigitalImageList(id: number) {
const res = await fetchDigitalHumanTemplateStatus<DigitalTemplate>(id) const res = await fetchDigitalHumanTemplateStatus<DigitalTemplate>(id)
if (res.code === 0) { if (res.code === 0) {
digitalCreationStore.updateDigitalCreation(res.data) const digitalTemplate = res.data
const {
dhParams: { position },
} = digitalTemplate
position.x = position.x || 0
position.y = position.y || 0
position.w = position.w || 1080
position.h = position.h || 1920
digitalCreationStore.updateDigitalCreation(digitalTemplate)
} }
} }
</script> </script>
......
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import BackgroundSetting from '../components/background/background-setting.vue' import BackgroundSetting from '../components/background/background-setting.vue'
import CaptionSetting from '../components/caption/caption-setting.vue' import SubtitleSetting from '../components/subtitle/subtitle-setting.vue'
import DigitalSetting from '../components/digital/digital-setting.vue' import DigitalSetting from '../components/digital/digital-setting.vue'
const value = ref('') const value = ref('digital')
const barList = [ const barList = [
{ {
key: 'digital', key: 'digital',
...@@ -17,7 +17,7 @@ const barList = [ ...@@ -17,7 +17,7 @@ const barList = [
icon: 'icon-park-outline:background-color', icon: 'icon-park-outline:background-color',
}, },
{ {
key: 'caption', key: 'subtitle',
label: '字幕', label: '字幕',
icon: 'icon-park-outline:text-message', icon: 'icon-park-outline:text-message',
}, },
...@@ -27,10 +27,10 @@ const barList = [ ...@@ -27,10 +27,10 @@ const barList = [
<template> <template>
<section class="h-full pl-4"> <section class="h-full pl-4">
<div class="flex h-full rounded-2xl bg-white"> <div class="flex h-full rounded-2xl bg-white">
<div class="flex-1 overflow-hidden px-4 py-2"> <div class="flex-1 overflow-hidden py-2">
<DigitalSetting v-if="value === 'digital'" /> <DigitalSetting v-if="value === 'digital'" />
<BackgroundSetting v-if="value === 'background'" /> <BackgroundSetting v-if="value === 'background'" />
<CaptionSetting v-if="value === 'caption'" /> <SubtitleSetting v-if="value === 'subtitle'" />
</div> </div>
<VerticalTabs v-model:value="value" class="border-l" :list="barList"></VerticalTabs> <VerticalTabs v-model:value="value" class="border-l" :list="barList"></VerticalTabs>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment