mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-09-18 01:06:39 +08:00
662 lines
23 KiB
Vue
662 lines
23 KiB
Vue
<template>
|
||
<div class="page-sd">
|
||
<div class="inner custom-scroll">
|
||
<div class="sd-box">
|
||
<h2>MidJourney 创作中心</h2>
|
||
|
||
<div class="sd-params" :style="{ height: mjBoxHeight + 'px' }">
|
||
<el-form :model="params" label-width="80px" label-position="left">
|
||
<div class="param-line" style="padding-top: 10px">
|
||
<el-form-item label="采样方法">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-select v-model="params.sampler" size="small">
|
||
<el-option v-for="item in samplers" :label="item" :value="item" :key="item"/>
|
||
</el-select>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="出图效果比较好的一般是 Euler 和 DPM 系列算法"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon>
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-form-item label="图片尺寸">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-input v-model.number="params.width" size="small" placeholder="图片宽度"/>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-input v-model.number="params.height" size="small" placeholder="图片高度"/>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-form-item label="迭代步数">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-input v-model.number="params.steps" size="small"/>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="值越大则代表细节越多,同时也意味着出图速度越慢"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon>
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-form-item label="引导系数">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-input v-model.number="params.cfg_scale" size="small"/>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon>
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-form-item label="随机因子">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-input v-model.number="params.seed" size="small"/>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon>
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-form-item label="面部修复">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-switch v-model="params.face_fix" style="--el-switch-on-color: #47fff1;"/>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="仅对绘制人物图像有效果。"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon style="margin-top: 6px">
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-form-item label="高清修复">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1;"/>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon style="margin-top: 6px">
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div v-show="params.hd_fix">
|
||
<div class="param-line">
|
||
<el-form-item label="重绘幅度">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1"
|
||
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon style="margin-top: 6px">
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-form-item label="放大算法">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-select v-model="params.hd_scale_alg" size="small">
|
||
<el-option v-for="item in scaleAlg" :label="item" :value="item" :key="item"/>
|
||
</el-select>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="高清修复放大算法,主流算法有Latent和ESRGAN_4x"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon>
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-form-item label="放大倍数">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-input v-model.number="params.hd_scale" size="small"/>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon>
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-form-item label="迭代步数">
|
||
<template #default>
|
||
<div class="form-item-inner">
|
||
<el-input v-model.number="params.hd_steps" size="small"/>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="重绘迭代步数,如果设置为0,则设置跟原图相同的迭代步数"
|
||
raw-content
|
||
placement="right"
|
||
>
|
||
<el-icon>
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="param-line">
|
||
<el-input
|
||
v-model="params.prompt"
|
||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||
type="textarea"
|
||
ref="promptRef"
|
||
placeholder="正向提示词,例如:A chinese girl walking in the middle of a cobblestone street"
|
||
/>
|
||
</div>
|
||
|
||
<div class="param-line pt">
|
||
<span>图片比例:</span>
|
||
<el-tooltip
|
||
effect="light"
|
||
content="不希望出现的元素,下面给了默认的起手式"
|
||
placement="right"
|
||
>
|
||
<el-icon>
|
||
<InfoFilled/>
|
||
</el-icon>
|
||
</el-tooltip>
|
||
</div>
|
||
<div class="param-line">
|
||
<el-input
|
||
v-model="params.negative_prompt"
|
||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||
type="textarea"
|
||
placeholder="反向提示词"
|
||
/>
|
||
</div>
|
||
|
||
<div class="param-line pt">
|
||
<el-form-item label="剩余次数">
|
||
<template #default>
|
||
<el-tag type="info">{{ imgCalls }}</el-tag>
|
||
</template>
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
<div class="submit-btn">
|
||
<el-button color="#47fff1" :dark="false" round @click="generate">立即生成</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="task-list-box">
|
||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
||
<h2>任务列表</h2>
|
||
<div class="running-job-list">
|
||
<ItemList :items="runningJobs" v-if="runningJobs.length > 0">
|
||
<template #default="scope">
|
||
<div class="job-item">
|
||
<el-popover
|
||
placement="top-start"
|
||
title="绘画提示词"
|
||
:width="240"
|
||
trigger="hover"
|
||
>
|
||
<template #reference>
|
||
<div v-if="scope.item.progress > 0" class="job-item-inner">
|
||
<el-image :src="scope.item['img_url']"
|
||
fit="cover"
|
||
loading="lazy">
|
||
<template #placeholder>
|
||
<div class="image-slot">
|
||
正在加载图片
|
||
</div>
|
||
</template>
|
||
|
||
<template #error>
|
||
<div class="image-slot">
|
||
<el-icon v-if="scope.item['img_url'] !== ''">
|
||
<Picture/>
|
||
</el-icon>
|
||
</div>
|
||
</template>
|
||
</el-image>
|
||
|
||
<div class="progress">
|
||
<el-progress type="circle" :percentage="scope.item.progress" :width="100" color="#47fff1"/>
|
||
</div>
|
||
</div>
|
||
<el-image fit="cover" v-else>
|
||
<template #error>
|
||
<div class="image-slot">
|
||
<i class="iconfont icon-quick-start"></i>
|
||
<span>任务正在排队中</span>
|
||
</div>
|
||
</template>
|
||
</el-image>
|
||
</template>
|
||
|
||
<template #default>
|
||
<div class="mj-list-item-prompt">
|
||
<span>{{ scope.item.prompt }}</span>
|
||
<el-icon class="copy-prompt" :data-clipboard-text="scope.item.prompt">
|
||
<DocumentCopy/>
|
||
</el-icon>
|
||
</div>
|
||
</template>
|
||
</el-popover>
|
||
</div>
|
||
</template>
|
||
</ItemList>
|
||
<el-empty :image-size="100" v-else/>
|
||
</div>
|
||
<h2>创作记录</h2>
|
||
<div class="finish-job-list">
|
||
<ItemList :items="finishedJobs" v-if="finishedJobs.length > 0">
|
||
<template #default="scope">
|
||
<div class="job-item" @click="showTask(scope.item)">
|
||
<el-image
|
||
:src="scope.item['img_url']+'?imageView2/1/w/240/h/240/q/75'"
|
||
fit="cover"
|
||
loading="lazy">
|
||
<template #placeholder>
|
||
<div class="image-slot">
|
||
正在加载图片
|
||
</div>
|
||
</template>
|
||
|
||
<template #error>
|
||
<div class="image-slot">
|
||
<el-icon>
|
||
<Picture/>
|
||
</el-icon>
|
||
</div>
|
||
</template>
|
||
</el-image>
|
||
</div>
|
||
</template>
|
||
</ItemList>
|
||
</div> <!-- end finish job list-->
|
||
</div>
|
||
|
||
</div><!-- end task list box -->
|
||
</div>
|
||
|
||
<!-- 任务详情弹框 -->
|
||
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
|
||
<el-row :gutter="20">
|
||
<el-col :span="16">
|
||
<div class="img-container" :style="{maxHeight: fullImgHeight+'px'}">
|
||
<el-image :src="item['img_url']" fit="contain"/>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="task-info">
|
||
<div class="info-line">
|
||
<el-divider>
|
||
正向提示词
|
||
</el-divider>
|
||
<div class="prompt">
|
||
<span>{{ item.prompt }}</span>
|
||
<el-icon class="copy-prompt" :data-clipboard-text="item.prompt">
|
||
<DocumentCopy/>
|
||
</el-icon>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="info-line">
|
||
<el-divider>
|
||
反向提示词
|
||
</el-divider>
|
||
<div class="prompt">
|
||
<span>{{ item.params.negative_prompt }}</span>
|
||
<el-icon class="copy-prompt" :data-clipboard-text="item.params.negative_prompt">
|
||
<DocumentCopy/>
|
||
</el-icon>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-line">
|
||
<div class="wrapper">
|
||
<label>采样方法:</label>
|
||
<div class="item-value">{{ item.params.sampler }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-line">
|
||
<div class="wrapper">
|
||
<label>图片尺寸:</label>
|
||
<div class="item-value">{{ item.params.width }} x {{ item.params.height }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-line">
|
||
<div class="wrapper">
|
||
<label>迭代步数:</label>
|
||
<div class="item-value">{{ item.params.steps }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-line">
|
||
<div class="wrapper">
|
||
<label>引导系数:</label>
|
||
<div class="item-value">{{ item.params.cfg_scale }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-line">
|
||
<div class="wrapper">
|
||
<label>随机因子:</label>
|
||
<div class="item-value">{{ item.params.seed }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="item.params.hd_fix">
|
||
<el-divider>
|
||
高清修复
|
||
</el-divider>
|
||
<div class="info-line">
|
||
<div class="wrapper">
|
||
<label>重绘幅度:</label>
|
||
<div class="item-value">{{ item.params.hd_redraw_rate }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-line">
|
||
<div class="wrapper">
|
||
<label>放大算法:</label>
|
||
<div class="item-value">{{ item.params.hd_scale_alg }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-line">
|
||
<div class="wrapper">
|
||
<label>放大倍数:</label>
|
||
<div class="item-value">{{ item.params.hd_scale }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-line">
|
||
<div class="wrapper">
|
||
<label>迭代步数:</label>
|
||
<div class="item-value">{{ item.params.hd_steps }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="copy-params">
|
||
<el-button type="primary" round @click="copyParams(item)">画一张同款的</el-button>
|
||
</div>
|
||
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {onMounted, ref} from "vue"
|
||
import {DocumentCopy, InfoFilled, Picture} from "@element-plus/icons-vue";
|
||
import {httpGet, httpPost} from "@/utils/http";
|
||
import {ElMessage} from "element-plus";
|
||
import ItemList from "@/components/ItemList.vue";
|
||
import Clipboard from "clipboard";
|
||
import {checkSession} from "@/action/session";
|
||
import {useRouter} from "vue-router";
|
||
import {getSessionId, getUserToken} from "@/store/session";
|
||
|
||
const listBoxHeight = ref(window.innerHeight - 40)
|
||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||
const fullImgHeight = ref(window.innerHeight - 60)
|
||
const showTaskDialog = ref(false)
|
||
const item = ref({})
|
||
|
||
window.onresize = () => {
|
||
listBoxHeight.value = window.innerHeight - 40
|
||
mjBoxHeight.value = window.innerHeight - 150
|
||
}
|
||
const samplers = ["Euler a", "Euler", "DPM2 a Karras", "DPM++ 2S a Karras", "DPM++ 2M Karras", "DPM++ SDE Karras", "DPM2", "DPM2 a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM fast", "DPM adaptive",
|
||
"LMS Karras", "DPM2 Karras", "DDIM", "PLMS", "UniPC", "LMS", "Heun",]
|
||
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
|
||
const params = ref({
|
||
width: 1024,
|
||
height: 1024,
|
||
sampler: samplers[0],
|
||
seed: -1,
|
||
steps: 20,
|
||
cfg_scale: 7,
|
||
face_fix: false,
|
||
hd_fix: false,
|
||
hd_redraw_rate: 0.3,
|
||
hd_scale: 2,
|
||
hd_scale_alg: scaleAlg[0],
|
||
hd_steps: 0,
|
||
prompt: "A beautiful Chinese girl riding on a tiger",
|
||
negative_prompt: "nsfw, paintings, cartoon, anime, sketches, low quality,easynegative,ng_deepnegative _v1 75t,(worst quality:2),(low quality:2),(normalquality:2),lowres,bad anatomy,bad hands,normal quality,((monochrome)),((grayscale)),((watermark))",
|
||
})
|
||
|
||
const runningJobs = ref([])
|
||
const finishedJobs = ref([])
|
||
const previewImgList = ref([])
|
||
const router = useRouter()
|
||
|
||
const socket = ref(null)
|
||
const imgCalls = ref(0)
|
||
|
||
const connect = () => {
|
||
let host = process.env.VUE_APP_WS_HOST
|
||
if (host === '') {
|
||
if (location.protocol === 'https:') {
|
||
host = 'wss://' + location.host;
|
||
} else {
|
||
host = 'ws://' + location.host;
|
||
}
|
||
}
|
||
const _socket = new WebSocket(host + `/api/sd/client?session_id=${getSessionId()}&token=${getUserToken()}`);
|
||
_socket.addEventListener('open', () => {
|
||
socket.value = _socket;
|
||
});
|
||
|
||
_socket.addEventListener('message', event => {
|
||
if (event.data instanceof Blob) {
|
||
const reader = new FileReader();
|
||
reader.readAsText(event.data, "UTF-8");
|
||
reader.onload = () => {
|
||
const data = JSON.parse(String(reader.result));
|
||
let append = true
|
||
if (data.progress === 100) { // 任务已完成
|
||
for (let i = 0; i < finishedJobs.value.length; i++) {
|
||
if (finishedJobs.value[i].id === data.id) {
|
||
append = false
|
||
break
|
||
}
|
||
}
|
||
for (let i = 0; i < runningJobs.value.length; i++) {
|
||
if (runningJobs.value[i].id === data.id) {
|
||
runningJobs.value.splice(i, 1)
|
||
break
|
||
}
|
||
}
|
||
if (append) {
|
||
finishedJobs.value.unshift(data)
|
||
}
|
||
previewImgList.value.unshift(data["img_url"])
|
||
} else { // 启动新的任务
|
||
for (let i = 0; i < runningJobs.value.length; i++) {
|
||
if (runningJobs.value[i].id === data.id) {
|
||
append = false
|
||
runningJobs.value[i] = data
|
||
break
|
||
}
|
||
}
|
||
if (append) {
|
||
runningJobs.value.push(data)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
_socket.addEventListener('close', () => {
|
||
connect()
|
||
});
|
||
}
|
||
|
||
onMounted(() => {
|
||
checkSession().then(user => {
|
||
imgCalls.value = user['img_calls']
|
||
// 获取运行中的任务
|
||
httpGet("/api/sd/jobs?status=0").then(res => {
|
||
runningJobs.value = res.data
|
||
}).catch(e => {
|
||
ElMessage.error("获取任务失败:" + e.message)
|
||
})
|
||
|
||
// 获取运行中的任务
|
||
httpGet("/api/sd/jobs?status=1").then(res => {
|
||
finishedJobs.value = res.data
|
||
previewImgList.value = []
|
||
for (let index in finishedJobs.value) {
|
||
previewImgList.value.push(finishedJobs.value[index]["img_url"])
|
||
}
|
||
}).catch(e => {
|
||
ElMessage.error("获取任务失败:" + e.message)
|
||
})
|
||
|
||
// 连接 socket
|
||
connect();
|
||
}).catch(() => {
|
||
router.push('/login')
|
||
});
|
||
|
||
const clipboard = new Clipboard('.copy-prompt');
|
||
clipboard.on('success', () => {
|
||
ElMessage.success({message: "复制成功!", duration: 500});
|
||
})
|
||
|
||
clipboard.on('error', () => {
|
||
ElMessage.error('复制失败!');
|
||
})
|
||
})
|
||
|
||
|
||
// 创建绘图任务
|
||
const promptRef = ref(null)
|
||
const generate = () => {
|
||
if (params.value.prompt === '') {
|
||
promptRef.value.focus()
|
||
return ElMessage.error("请输入绘画提示词!")
|
||
}
|
||
params.value.session_id = getSessionId()
|
||
httpPost("/api/sd/image", params.value).then(() => {
|
||
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...")
|
||
imgCalls.value -= 1
|
||
}).catch(e => {
|
||
ElMessage.error("任务推送失败:" + e.message)
|
||
})
|
||
}
|
||
|
||
const showTask = (row) => {
|
||
item.value = row
|
||
showTaskDialog.value = true
|
||
}
|
||
|
||
const copyParams = (row) => {
|
||
params.value = row.params
|
||
showTaskDialog.value = false
|
||
}
|
||
|
||
</script>
|
||
|
||
<style lang="stylus">
|
||
@import "@/assets/css/image-sd.styl"
|
||
</style>
|