Commit daf04614 by haojie

新增ai换脸页面

parent aebc5461
......@@ -271,52 +271,61 @@ export default defineComponent({
{props.icon}
<span>{props.label}</span>
</div>
<div class="izable-page-upload-box">
{enableV2Vocal() ? (
<MultipleUpload
v-model={files.value}
v-model:audioTotolTime={audioTotolTime.value}
label={'选择音频'}
config={ossConfig.value}
accept={props.accept}
></MultipleUpload>
) : (
<Upload
v-model={file.value}
uploadInfo={props.uploadInfo}
accept={props.accept}
config={ossConfig.value}
></Upload>
)}
<div class={['upload-box-footer', enableV2Vocal() ? 'upload-box-footer-v2' : '']}>
{slots.header ? (
slots.header()
) : (
<div class="izable-page-upload-box">
{enableV2Vocal() ? (
<div class="upload-total-time-box">
<div class="label">上传的音频文件时间总和需要达到{audioMinTime.value / 60}分钟以上</div>
<div class="value">
<span class="current-total">{computedTotalTime.value} /</span>
<span class="role-total">{transformTime(audioMinTime.value)}</span>
</div>
</div>
<MultipleUpload
v-model={files.value}
v-model:audioTotolTime={audioTotolTime.value}
label={'选择音频'}
config={ossConfig.value}
accept={props.accept}
></MultipleUpload>
) : (
''
<Upload
v-model={file.value}
uploadInfo={props.uploadInfo}
accept={props.accept}
config={ossConfig.value}
></Upload>
)}
<div class="footer-buttons">
<Button theme="opacity" onClick={reset}>
重置
</Button>
<Button theme="green" onClick={submit}>
生成
</Button>
<div class={['upload-box-footer', enableV2Vocal() ? 'upload-box-footer-v2' : '']}>
{enableV2Vocal() ? (
<div class="upload-total-time-box">
<div class="label">上传的音频文件时间总和需要达到{audioMinTime.value / 60}分钟以上</div>
<div class="value">
<span class="current-total">{computedTotalTime.value} /</span>
<span class="role-total">{transformTime(audioMinTime.value)}</span>
</div>
</div>
) : (
''
)}
<div class="footer-buttons">
<Button theme="opacity" onClick={reset}>
重置
</Button>
<Button theme="green" onClick={submit}>
生成
</Button>
</div>
</div>
</div>
</div>
)}
<div class="izable-page-tabs">{slots.default?.()}</div>
<Dialog
v-model:value={taskName.value}
dialogInfo={props.dialogInfo}
v-model={nameDialog.value}
onConfirm={onConfirm}
></Dialog>
{slots.header ? (
''
) : (
<Dialog
v-model:value={taskName.value}
dialogInfo={props.dialogInfo}
v-model={nameDialog.value}
onConfirm={onConfirm}
></Dialog>
)}
</div>
);
},
......
......@@ -15,6 +15,19 @@ export default defineComponent({
type: Array,
default: [],
},
mode: {
type: String,
default: '1',
},
modeInfo: {
type: Object as any,
default: {
label1: '选择需要换脸的视频',
label2: '或拖视频到此处上传',
icon: '',
buttonLabel: '选择视频',
},
},
// 是否截取视频第一针
firstFrame: {
type: Boolean,
......@@ -317,7 +330,7 @@ export default defineComponent({
draggable={true}
>
<div class="custom-upload-click-box" ref={uploadRef}>
<TButton class="custom-chose-file">{props.label}</TButton>
{props.mode == '1' ? <TButton class="custom-chose-file">{props.label}</TButton> : <div></div>}
</div>
</TUpload>
);
......
......@@ -9,6 +9,7 @@
}"
>
<div class="custom-card-two-image">
<!-- v-lazy="img" -->
<img alt="" :src="img" />
<div v-show="showHover" :class="['hover']">
<slot name="hover"></slot>
......@@ -27,6 +28,7 @@
<script lang="ts" setup>
import { ref } from 'vue';
import ChangeName from '@/components/changeName.vue';
import { vLazy } from '@/utils/command';
const props = withDefaults(
defineProps<{
id: string | number;
......
......@@ -106,6 +106,13 @@ export const getRoutes = () => {
component: () => import('@/pages/createAction/index'),
meta: { title: 'snowhome', keepAlive: true },
},
// ai换脸
{
path: routerConfig.faceTransplant.path,
name: routerConfig.faceTransplant.name,
component: () => import('@/pages/faceTransplant/index.vue'),
meta: { title: 'snowhome', keepAlive: true },
},
];
}
};
......@@ -143,6 +150,9 @@ export const audioAccept = 'wav,mpeg,mp3';
// 视频上传格式限制
export const videoAccept = 'mp4';
// 图片上传格式限制
export const imageAccept = 'png,jpg,jpeg';
// 音频切割间隔时长
export const audioSplitDuration = 300;
......@@ -158,3 +168,13 @@ export const getTestUuid = () => {
return false;
}
};
// navigation各个路由的label
export const navigationLabels = {
createLive: '直播创建',
imageCustomization: '形象定制',
vocalCustomization: '声音定制',
createInteract: '互动回答',
createAction: '动作创建',
faceTransplant: '智能换脸',
};
import { onMounted, ref } from 'vue';
import { NotifyPlugin } from 'tdesign-vue-next';
import { injectWindow } from '@/utils/pyqt';
export default function () {
const notifyStatus = ref(false);
// 通知
const notify = ref({
title: '',
content: '',
duration: 0,
closeBtn: true,
});
const showNotifyPlugin = (status: boolean = true) => {
// 已经创建就不能再创建了
if (notifyStatus.value) {
return;
}
notifyStatus.value = status;
if (status) {
NotifyPlugin('info', notify.value);
} else {
// 销毁
NotifyPlugin.closeAll();
}
};
const changeNotifyPlugin = () => {
notify.value.title = '你好啊';
};
onMounted(() => {
// 注入通知方法
injectWindow('showNotifyPlugin', showNotifyPlugin);
injectWindow('changeNotifyPlugin', changeNotifyPlugin);
});
return {
showNotifyPlugin,
changeNotifyPlugin,
};
}
<template>
<div class="face-transplant-header">
<div class="face-transplant-upload">
<div class="upload">
<Upload
class="face-transplant-reset-upload"
v-model="videoFile"
:config="ossConfig"
:accept="videoAccept"
:uploadInfo="videoUploadInfo"
></Upload>
</div>
<div class="divide">+</div>
<div class="upload">
<Upload
class="face-transplant-reset-upload"
v-model="imageFile"
:config="ossConfig"
:accept="imageAccept"
:uploadInfo="imageUploadInfo"
></Upload>
</div>
</div>
<div class="face-transplant-footer">
<Button theme="opacity" class="face-transplant-footer-reset">重置</Button>
<Button theme="green" class="face-transplant-create" @click="openDialog">生成</Button>
</div>
<ConfirmDialog v-model="confirmDialogVisible" title="确定生成吗?" @confirm="confirm"></ConfirmDialog>
</div>
</template>
<script lang="tsx" setup>
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import Button from '@/components/Button.vue';
import Upload from '@/components/upload';
import { videoAccept, imageAccept } from '@/constants/token';
import { getUploadConfig } from '@/service/Common';
import { onMounted, ref } from 'vue';
const videoUploadInfo = {
label1: '选择需要换脸的视频',
label2: '或拖视频到此处上传',
buttonLabel: '选择视频',
successIcon: '',
successButtonLabel: '替换视频',
};
const imageUploadInfo = {
label1: '选择需要换脸的图片',
label2: '或将图片拖拽到此处上传',
buttonLabel: '选择图片',
successIcon: '',
successButtonLabel: '替换图片',
};
const ossConfig = ref({});
// 弹窗状态
const confirmDialogVisible = ref(false);
// 视频文件
const videoFile = ref('');
// 图片文件
const imageFile = ref('');
const getConfig = async () => {
ossConfig.value = await getUploadConfig();
};
const openDialog = () => {
confirmDialogVisible.value = true;
};
// 确定生成
const confirm = async () => {
try {
//
} catch (e) {
console.log(e);
}
};
onMounted(() => {
// 获取上传配置
getConfig();
});
</script>
<style lang="less">
@import '@/style/variables.less';
.face-transplant-header {
height: 650px;
border-radius: 6px;
background: #303030;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
.face-transplant-upload {
padding: 30px;
flex: 1;
display: flex;
align-items: center;
.divide {
font-size: 60px;
font-weight: bold;
color: white;
padding: 0 12px;
}
.upload {
flex: 1;
.face-transplant-reset-upload {
background-color: transparent;
border: 2px dashed #9f9f9f;
height: 500px;
}
}
}
.face-transplant-footer {
height: 60px;
border-top: 1px solid #9f9f9f;
display: flex;
justify-content: flex-end;
align-items: center;
padding: 0 20px;
.t-button {
font-weight: 700;
font-size: @size-14;
width: 76px;
}
.face-transplant-footer-reset {
color: #9f9f9f;
border-color: #9f9f9f;
}
.face-transplant-create {
margin-left: 20px;
}
}
}
</style>
<template>
<div class="face-transplant-record">
<div class="record-items" v-for="item in list" :key="item.id">
<div class="left">
<img :src="item.cover_url" alt="" />
</div>
<div class="center">
<div class="name">
名称:
<span>{{ item.name }}</span>
</div>
<div class="create">
创建时间:
<span>{{ item.created_at }}</span>
</div>
<div class="download-box">
<Button
theme="green"
:class="['download-button', item.audit_status != 3 ? 'download-button__disabled' : '']"
@click="onDownloadVideo(item)"
>下载</Button
>
</div>
</div>
<CustomizationStatus :status="item.audit_status"></CustomizationStatus>
</div>
</div>
</template>
<script lang="tsx" setup>
import CustomizationStatus from '@/components/CustomizationStatus';
import Button from '@/components/Button.vue';
import { callPyjsInWindow } from '@/utils/pyqt';
import { downloadMp4 } from '@/utils/tool';
// import useNotify from '@/hooks/useNotify';
// const { showNotifyPlugin } = useNotify();
const props = withDefaults(
defineProps<{
list?: any[];
loading: boolean;
}>(),
{
list: () => [],
},
);
// 下载视频
const onDownloadVideo = (item: any) => {
if (item.audit_status != 3) {
return;
}
// 通知python下载视频
let url =
'http://yunyi-live.oss-cn-hangzhou.aliyuncs.com/upload/1/2023-08-22c130e428-cab2-4e1e-8904-88054d84bc1b.mp4';
callPyjsInWindow('downloadVideo', url);
downloadMp4(url);
};
</script>
<style lang="less">
@import '@/style/variables.less';
.face-transplant-record {
border-radius: 0px 6px 0px 0px;
background: #303030;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.04);
min-height: 200px;
padding: 20px;
& > :not(:first-child) {
margin-top: 12px;
}
.record-items {
border-radius: 6px;
background: #1e1e1e;
height: 150px;
display: flex;
align-items: center;
padding: 0 60px;
.left {
width: 250px;
height: 120px;
img {
border-radius: 8px;
width: 200px;
height: 100%;
object-fit: contain;
}
}
.center {
padding-left: 30px;
flex: 1;
color: #fff;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
.name {
font-size: @size-18;
}
.create {
font-size: @size-16;
}
}
.download-box {
.download-button {
height: 28px !important;
font-weight: 700;
font-size: @size-14;
width: 76px;
}
.download-button__disabled {
background: #6a6a6a;
color: #9f9f9f;
border-color: #6a6a6a;
--ripple-color: transparent !important;
cursor: not-allowed;
}
}
}
}
</style>
<template>
<div class="">
<Customizable
:video="true"
:submit="submit"
:icon="getIcon()"
:uploadInfo="uploadInfo"
:dialogInfo="dialogInfo"
:label="navigationLabels.faceTransplant"
>
<template #header>
<Header></Header>
</template>
<CustomTabs v-model="currentTab" theme="dark2" class="custom-tabs-flex">
<CustomTabPanel name="1" label="生成记录">
<Record :list="personList.list" :loading="loading"></Record>
</CustomTabPanel>
</CustomTabs>
</Customizable>
</div>
</template>
<script lang="tsx">
export default {
name: routerConfig.ImageCustomization.name,
};
</script>
<script lang="tsx" setup>
import { navigationLabels } from '@/constants/token';
import Header from './components/header.vue';
import Record from './components/record.vue';
import CustomTabs from '@/components/CustomTabs';
import CustomTabPanel from '@/components/CustomTabPanel';
import Customizable from '@/components/Customizable';
import PersonSvg from '@/assets/svg/home/faceTransplant.svg';
import { onMounted, ref, reactive } from 'vue';
import { customizedImageSubmission } from '@/utils/api/userApi';
import { show_message } from '@/utils/tool';
import routerConfig from '@/router/tool';
import { useStore } from 'vuex';
import { jumpPageAddNavigation } from '@/router/jump';
import { getDigitalPeopleList } from '@/service/Common';
const { addNavigation } = jumpPageAddNavigation();
const store = useStore();
const currentTab = ref('1');
// 子组件loading
const loading = ref(false);
const personList = reactive({
list: [],
});
const imgs = {
success: new URL('../../assets/svg/upload/success2.svg', import.meta.url).href,
};
const dialogInfo = {
title: navigationLabels.faceTransplant,
inputLabel: '数字人名称',
placeholder: '请输入数字人名称',
};
const getIcon = () => {
return <PersonSvg></PersonSvg>;
};
const uploadInfo = {
label1: '选择视频',
label2: '或拖视频到此处上传',
buttonLabel: '选择视频',
successIcon: imgs.success,
successButtonLabel: '替换视频',
};
// 获取我的数字人列表
const getList = async () => {
loading.value = true;
let res = await getDigitalPeopleList();
personList.list = res.myList;
loading.value = false;
};
const submit = async (params: any) => {
try {
let res: any = await customizedImageSubmission(params);
if (res.code == 0) {
show_message('提交成功', 'success');
// 更新记录
getList();
return true;
}
} catch (e) {
console.log(e);
}
};
onMounted(() => {
addNavigation(routerConfig.faceTransplant.path);
// 获取数字人列表
getList();
});
</script>
<style lang="less"></style>
......@@ -24,7 +24,7 @@
>
</template>
<div class="digtal-people-hover-tool">
<Button size="13" theme="dark" @click="onEdit(item)">编辑</Button>
<!-- <Button size="13" theme="dark" @click="onEdit(item)">编辑</Button> -->
<Button size="13" theme="dark" @click="downLoadVideo(item)">下载</Button>
<Button size="13" theme="dark" @click="onDelete(item)">删除</Button>
</div>
......@@ -278,6 +278,7 @@ watch(
.digtal-people-hover-tool {
flex: 1;
display: flex;
justify-content: center;
align-items: flex-end;
margin-bottom: 20px;
}
......
......@@ -106,6 +106,7 @@ const imgs = {
speaking: new URL('../../assets/svg/home/speaking.svg', import.meta.url).href,
interaction: new URL('../../assets/svg/home/interaction.svg', import.meta.url).href,
action: new URL('../../assets/svg/home/action.svg', import.meta.url).href,
faceTransplant: new URL('../../assets/svg/home/faceTransplant.svg', import.meta.url).href,
};
const toolList = [
{
......@@ -132,6 +133,12 @@ const toolList = [
path: routerConfig.createAction.path,
name: routerConfig.createAction.name,
},
{
label: '智能换脸',
icon: imgs.faceTransplant,
path: routerConfig.faceTransplant.path,
name: routerConfig.faceTransplant.name,
},
];
// 跳转到创建直播页面
......
......@@ -48,4 +48,9 @@ export default {
path: '/createAction',
name: 'createAction',
},
// ai换脸
faceTransplant: {
path: '/faceTransplant',
name: 'faceTransplant',
},
};
......@@ -2,6 +2,7 @@ import routerConfig from '@/router/tool';
import { createLiveRouteKey } from '@/constants/token';
import { getSiteRouter } from '@/config/site';
import router from '@/router';
import { navigationLabels } from '@/constants/token';
const imgs = {
home: new URL('../../assets/svg/home/home.svg', import.meta.url).href,
......@@ -10,6 +11,7 @@ const imgs = {
speak: new URL('../../assets/svg/home/speaking.svg', import.meta.url).href,
interaction: new URL('../../assets/svg/home/interaction.svg', import.meta.url).href,
action: new URL('../../assets/svg/home/action.svg', import.meta.url).href,
faceTransplant: new URL('../../assets/svg/home/faceTransplant.svg', import.meta.url).href,
};
const filterKeepAlive = () => {
......@@ -31,6 +33,40 @@ const filterKeepAlive = () => {
return list;
};
// 导航列表
const navigationList = [
{
path: routerConfig.createLive.path,
icon: imgs.live,
label: navigationLabels.createLive,
},
{
path: routerConfig.ImageCustomization.path,
icon: imgs.person,
label: navigationLabels.imageCustomization,
},
{
path: routerConfig.VocalCustomization.path,
icon: imgs.speak,
label: navigationLabels.vocalCustomization,
},
{
path: routerConfig.createInteract.path,
icon: imgs.interaction,
label: navigationLabels.createInteract,
},
{
path: routerConfig.createAction.path,
icon: imgs.action,
label: navigationLabels.createAction,
},
{
path: routerConfig.faceTransplant.path,
icon: imgs.faceTransplant,
label: navigationLabels.faceTransplant,
},
];
const state = {
version: 'v1',
navbarList: [],
......@@ -52,21 +88,10 @@ const mutations = {
// 替换
state.navbarList[index].query = info.query;
} else {
if (info.path == routerConfig.createLive.path) {
info.icon = imgs.live;
info.label = '直播创建';
} else if (info.path == routerConfig.ImageCustomization.path) {
info.icon = imgs.person;
info.label = '形象定制';
} else if (info.path == routerConfig.VocalCustomization.path) {
info.icon = imgs.speak;
info.label = '声音定制';
} else if (info.path == routerConfig.createInteract.path) {
info.icon = imgs.interaction;
info.label = '互动回答';
} else if (info.path == routerConfig.createAction.path) {
info.icon = imgs.action;
info.label = '动作创建';
let currentInfo = navigationList.find((item: any) => item.path == info.path);
if (currentInfo) {
info.icon = currentInfo.icon;
info.label = currentInfo.label;
}
state.navbarList.push(info);
}
......
export const vLazy = (el: HTMLImageElement, image: any) => {
el.src = 'https://cdn.staticaly.com/gh/1024huijia/QingChunMeizi@master/loading.5e3wpezjapc0.gif';
// 使用obaesrve监听图片是否在可视区域内
const observe = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
......
......@@ -8,7 +8,7 @@ const error_messaage = '请求错误';
const getBaseUrl = async () => {
if (isDev()) {
return 'http://156.247.11.21:93';
// return 'http://156.247.11.21:93';
return '';
}
// 默认线上地址
......
......@@ -366,7 +366,35 @@ export const getFile = (url: string) => {
});
};
// 用户下载文件
// 下载单个MP4
export const downloadMp4 = async (url: string, name: string = '') => {
if (!url) {
return;
}
try {
const res: Blob = await request.get(url, {
responseType: 'blob',
});
downloadFile(URL.createObjectURL(res), name);
} catch (e) {
show_message('下载失败', 'error');
console.log(e);
}
};
// a标签下载文件
export const downloadFile = (url: string, name: string = '') => {
// 通过链接获取文件后缀
const suffix = getFileSuffixInUrl(url);
const a = document.createElement('a');
a.href = url;
a.download = `${name ? name : 'example'}.${suffix}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
// 用户下载文件压缩包
// export const downloadFiles = (list: string[]) => {
// let zip = new JSZip();
// const promises = [];
......
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