V 3.0.1 LaiTool V3.0.1-preview.3

This commit is contained in:
lq1405 2024-08-08 16:24:47 +08:00
parent f3a25e9474
commit 36178fbe5d
40 changed files with 1545 additions and 581 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "laitool", "name": "laitool",
"version": "3.0.1-preview.2", "version": "3.0.1-preview.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "laitool", "name": "laitool",
"version": "3.0.1-preview.2", "version": "3.0.1-preview.3",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@alicloud/alimt20181012": "^1.2.0", "@alicloud/alimt20181012": "^1.2.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "laitool", "name": "laitool",
"version": "3.0.1-preview.2", "version": "3.0.1-preview.3",
"description": "An AI tool for image processing, video processing, and other functions.", "description": "An AI tool for image processing, video processing, and other functions.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "laitool.cn", "author": "laitool.cn",

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

Binary file not shown.

View File

@ -171,13 +171,13 @@ export function CompressImageToSize(base64: string, maxSizeInBytes: number): Pro
* *
* @param inputPath * @param inputPath
* @param outputPath * @param outputPath
* @param region x, y, width, height属性 * @param regions x, y, width, height属性
* @param markColor { r: 255, g: 255, b: 255 } * @param markColor { r: 255, g: 255, b: 255 }
* @param backColor { r: 0, g: 0, b: 0 } * @param backColor { r: 0, g: 0, b: 0 }
*/ */
export async function ProcessImage(inputPath: string, export async function ProcessImage(inputPath: string,
outputPath: string, outputPath: string,
region: { width: any; height: any; x: any; y: any }, regions: { width: any; height: any; x: any; y: any, imageWidth?: any, imageHeight?: any }[],
markColor: { r: number; g: number; b: number } = { r: 255, g: 255, b: 255 }, markColor: { r: number; g: number; b: number } = { r: 255, g: 255, b: 255 },
backColor: { r: number; g: number; b: number } = { r: 0, g: 0, b: 0 }, backColor: { r: number; g: number; b: number } = { r: 0, g: 0, b: 0 },
): Promise<void> { ): Promise<void> {
@ -188,7 +188,7 @@ export async function ProcessImage(inputPath: string,
// 获取图片的元数据 // 获取图片的元数据
const { width, height } = await image.metadata(); const { width, height } = await image.metadata();
// 创建一个新的空白图片,背景为白色 // 创建一个新的黑色图片,背景为白色
const whiteBackground = await sharp({ const whiteBackground = await sharp({
create: { create: {
width, width,
@ -198,27 +198,42 @@ export async function ProcessImage(inputPath: string,
} }
}).png().toBuffer(); }).png().toBuffer();
// 创建一个黑色的矩形 // 创建多个白色的矩形,并进行合成
const blackRegion = await sharp({ const composites = await Promise.all(regions.map(async (region) => {
create: {
width: region.width, let rateW = undefined;
height: region.height, let rateY = undefined;
channels: 3, // RGB channels let rate = undefined;
background: markColor // Black color if (region.imageWidth != null && region.imageHeight != null) {
rateY = height / region.imageHeight;
rateW = width / region.imageWidth;
rate = rateY;
}
if (rate == null) {
rate = 1;
} }
}).png().toBuffer();
// 在白色背景上叠加黑色矩形 const regionBuffer = await sharp({
await sharp(whiteBackground) create: {
.composite([ width: Math.ceil(region.width * rate),
{ height: Math.ceil(region.height * rate),
input: blackRegion, channels: 3, // RGB channels
left: region.x, background: markColor // 标记颜色
top: Math.floor(region.y)
} }
]) }).png().toBuffer();
.toFile(outputPath);
return {
input: regionBuffer,
left: Math.ceil(region.x * rate),
top: Math.ceil(region.y * rate),
};
}));
// 在背景上叠加所有的矩形区域
await sharp(whiteBackground)
.composite(composites)
.toFile(outputPath);
} catch (err) { } catch (err) {
throw err; throw err;

View File

@ -2,17 +2,17 @@ let apiUrl = [
{ {
label: 'LAI API', label: 'LAI API',
value: 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65', value: 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65',
gpt_url: 'https://laitool.net/v1/chat/completions', gpt_url: 'https://api.laitool.cc/v1/chat/completions',
mj_url: { mj_url: {
imagine: 'https://laitool.net/mj/submit/imagine', imagine: 'https://api.laitool.cc/mj/submit/imagine',
describe: 'https://laitool.net/mj/submit/describe', describe: 'https://api.laitool.cc/mj/submit/describe',
update_file: 'https://laitool.net/mj/submit/upload-discord-images', update_file: 'https://api.laitool.cc/mj/submit/upload-discord-images',
once_get_task: 'https://laitool.net/mj/task/${id}/fetch' once_get_task: 'https://api.laitool.cc/mj/task/${id}/fetch'
}, },
d3_url: { d3_url: {
image: 'https://laitool.net/v1/images/generations' image: 'https://api.laitool.cc/v1/images/generations'
}, },
buy_url: 'https://laitool.net/register?aff=Zmdu' buy_url: 'https://api.laitool.cc/register?aff=Zmdu'
}, },
{ {
label: 'openai-hk', label: 'openai-hk',

View File

@ -24,7 +24,8 @@ export class BookModel extends Realm.Object<BookModel> {
videoConfig: string | null // 合成视频设置 videoConfig: string | null // 合成视频设置
prefixPrompt: string | null // 前缀 prefixPrompt: string | null // 前缀
suffixPrompt: string | null // 后缀 suffixPrompt: string | null // 后缀
subtitlePosition: string | null subtitlePosition: string | null // 字幕位置
watermarkPosition: string | null // 水印位置一个json数组字符串
static schema: Realm.ObjectSchema = { static schema: Realm.ObjectSchema = {
name: 'Book', name: 'Book',
@ -37,7 +38,7 @@ export class BookModel extends Realm.Object<BookModel> {
oldVideoPath: 'string?', oldVideoPath: 'string?',
srtPath: 'string?', srtPath: 'string?',
audioPath: 'string?', audioPath: 'string?',
draftSrtStyle : 'string?', draftSrtStyle: 'string?',
backgroundMusic: 'string?', backgroundMusic: 'string?',
friendlyReminder: 'string?', friendlyReminder: 'string?',
imageFolder: 'string?', imageFolder: 'string?',
@ -50,7 +51,8 @@ export class BookModel extends Realm.Object<BookModel> {
videoConfig: "string?", videoConfig: "string?",
prefixPrompt: "string?", prefixPrompt: "string?",
suffixPrompt: "string?", suffixPrompt: "string?",
subtitlePosition: 'string?' subtitlePosition: 'string?',
watermarkPosition: "string?"
}, },
// 主键为_id // 主键为_id
primaryKey: 'id' primaryKey: 'id'

View File

@ -155,6 +155,13 @@ const migration = (oldRealm: Realm, newRealm: Realm) => {
newBookTask[i].subValue = '[]' newBookTask[i].subValue = '[]'
} }
} }
if (oldRealm.schemaVersion < 22) {
const oldBookTask = oldRealm.objects('Book')
const newBookTask = newRealm.objects('Book')
for (let i = 0; i < oldBookTask.length; i++) {
newBookTask[i].watermarkPosition = '[]'
}
}
} }
export class BaseRealmService extends BaseService { export class BaseRealmService extends BaseService {
@ -196,7 +203,7 @@ export class BaseRealmService extends BaseService {
BookTaskDetailModel BookTaskDetailModel
], ],
path: this.dbpath, path: this.dbpath,
schemaVersion: 21, schemaVersion: 22,
migration: migration migration: migration
} }
this.realm = await Realm.open(config) this.realm = await Realm.open(config)

View File

@ -277,7 +277,7 @@ export class BookService extends BaseRealmService {
* @param bookId ID * @param bookId ID
* @param bookData * @param bookData
*/ */
async UpdateBookData(bookId: string, bookData) { async UpdateBookData(bookId: string, bookData: Book.SelectBook) {
try { try {
if (bookId == null) { if (bookId == null) {
throw new Error('修改小说数据失败缺少小说ID') throw new Error('修改小说数据失败缺少小说ID')

View File

@ -202,7 +202,8 @@ export const DEFINE_STRING = {
BASE64_TO_FILE: 'BASE64_TO_FILE', BASE64_TO_FILE: 'BASE64_TO_FILE',
PROCESS_IMAGE: 'PROCESS_IMAGE', PROCESS_IMAGE: 'PROCESS_IMAGE',
BATCH_PROCESS_IMAGE: 'BATCH_PROCESS_IMAGE', BATCH_PROCESS_IMAGE: 'BATCH_PROCESS_IMAGE',
BATCH_PROCESS_IMAGE_RESULT: 'BATCH_PROCESS_IMAGE_RESULT' BATCH_PROCESS_IMAGE_RESULT: 'BATCH_PROCESS_IMAGE_RESULT',
PROCESS_IMAGE_WATERMASK_CHECK: "PROCESS_IMAGE_WATERMASK_CHECK"
}, },
BOOK: { BOOK: {
MAIN_DATA_RETURN: 'MAIN_DATA_RETURN', // 监听任务返回 MAIN_DATA_RETURN: 'MAIN_DATA_RETURN', // 监听任务返回
@ -291,6 +292,7 @@ export const DEFINE_STRING = {
}, },
DB: { DB: {
UPDATE_BOOK_TASK_DATA: "UPDATE_BOOK_TASK_DATA", UPDATE_BOOK_TASK_DATA: "UPDATE_BOOK_TASK_DATA",
UPDATE_BOOK_TASK_DETAIL_DATA: "UPDATE_BOOK_TASK_DETAIL_DATA" UPDATE_BOOK_TASK_DETAIL_DATA: "UPDATE_BOOK_TASK_DETAIL_DATA",
UPDATE_BOOK_DATA: "UPDATE_BOOK_DATA"
} }
} }

View File

@ -64,21 +64,30 @@ export class TagDefine {
if (res.hasOwnProperty('character_tags')) { if (res.hasOwnProperty('character_tags')) {
res.character_tags.forEach((item) => { res.character_tags.forEach((item) => {
if (item.show_image && item.show_image != '') { if (item.show_image && item.show_image != '') {
item.show_image = path.join(define.image_path, item.show_image) item.show_image = path.join(
define.image_path,
item.show_image + '?t=' + new Date().getTime()
)
} }
}) })
} }
if (res.hasOwnProperty('scene_tags')) { if (res.hasOwnProperty('scene_tags')) {
res.scene_tags.forEach((item) => { res.scene_tags.forEach((item) => {
if (item.show_image && item.show_image != '') { if (item.show_image && item.show_image != '') {
item.show_image = path.join(define.image_path, item.show_image) item.show_image = path.join(
define.image_path,
item.show_image + '?t=' + new Date().getTime()
)
} }
}) })
} }
if (res.hasOwnProperty('style_tags')) { if (res.hasOwnProperty('style_tags')) {
res.style_tags.forEach((item) => { res.style_tags.forEach((item) => {
if (item.show_image && item.show_image != '') { if (item.show_image && item.show_image != '') {
item.show_image = path.join(define.image_path, item.show_image) item.show_image = path.join(
define.image_path,
item.show_image + '?t=' + new Date().getTime()
)
} }
}) })
} }

View File

@ -9,6 +9,7 @@ import { BookImage } from '../Service/Book/bookImage'
import { ImageStyle } from '../Service/Book/imageStyle' import { ImageStyle } from '../Service/Book/imageStyle'
import { BookTask } from '../Service/Book/bookTask' import { BookTask } from '../Service/Book/bookTask'
import { BookVideo } from '../Service/Book/bookVideo' import { BookVideo } from '../Service/Book/bookVideo'
import { Watermark } from '../Service/watermark'
let reverseBook = new ReverseBook() let reverseBook = new ReverseBook()
let basicReverse = new BasicReverse() let basicReverse = new BasicReverse()
let subtitle = new Subtitle() let subtitle = new Subtitle()
@ -18,6 +19,7 @@ let bookImage = new BookImage()
let imageStyle = new ImageStyle() let imageStyle = new ImageStyle()
let bookTask = new BookTask() let bookTask = new BookTask()
let bookVideo = new BookVideo() let bookVideo = new BookVideo()
let watermark = new Watermark()
export function BookIpc() { export function BookIpc() {
// 获取样式图片的子列表 // 获取样式图片的子列表
@ -98,13 +100,13 @@ export function BookIpc() {
// 开始执行分镜任务 // 开始执行分镜任务
ipcMain.handle( ipcMain.handle(
DEFINE_STRING.BOOK.GET_COPYWRITING, DEFINE_STRING.BOOK.GET_COPYWRITING,
async (event, bookId) => await reverseBook.GetCopywriting(bookId) async (event, bookId, bookTaskId) => await reverseBook.GetCopywriting(bookId, bookTaskId)
) )
// 执行去除水印 // 执行去除水印
ipcMain.handle( ipcMain.handle(
DEFINE_STRING.BOOK.REMOVE_WATERMARK, DEFINE_STRING.BOOK.REMOVE_WATERMARK,
async (event, bookId) => await reverseBook.RemoveWatermark(bookId) async (event, id,operateBookType) => await watermark.RemoveWatermark(id,operateBookType)
) )
// 添加反推任务到任务列表 // 添加反推任务到任务列表

View File

@ -4,11 +4,13 @@ import { errorMessage, successMessage } from '../Public/generalTools'
import { Book } from '../../model/book' import { Book } from '../../model/book'
import { BookTaskService } from '../../define/db/service/Book/bookTaskService' import { BookTaskService } from '../../define/db/service/Book/bookTaskService'
import { BookTaskDetailService } from '../../define/db/service/Book/bookTaskDetailService' import { BookTaskDetailService } from '../../define/db/service/Book/bookTaskDetailService'
import { BookService } from '../../define/db/service/Book/bookService'
async function DBIpc() { async function DBIpc() {
let bookTaskService = await BookTaskService.getInstance() let bookTaskService = await BookTaskService.getInstance()
let bookTaskDetailService = await BookTaskDetailService.getInstance() let bookTaskDetailService = await BookTaskDetailService.getInstance()
let bookService = await BookService.getInstance()
//#region 小说相关的修改 //#region 小说相关的修改
// 修改小说任务的数据 // 修改小说任务的数据
@ -31,6 +33,18 @@ async function DBIpc() {
} }
}) })
/**
*
*/
ipcMain.handle(DEFINE_STRING.DB.UPDATE_BOOK_DATA, async (event, bookId: string, data: Book.SelectBook) => {
try {
bookService.UpdateBookData(bookId, data)
return successMessage(null, "修改小说数据成功", "DBIpc_UpdateBookData")
} catch (error) {
return errorMessage("修改小说数据失败", "DBIpc_UpdateBookData")
}
})
//#endregion //#endregion
} }

View File

@ -3,7 +3,9 @@ import { DEFINE_STRING } from '../../define/define_string'
import { Image } from '../Public/Image' import { Image } from '../Public/Image'
import { LOGGER_DEFINE } from '../../define/logger_define' import { LOGGER_DEFINE } from '../../define/logger_define'
import { errorMessage } from '../Public/generalTools' import { errorMessage } from '../Public/generalTools'
import { Watermark } from '../Service/watermark'
let image = new Image(global) let image = new Image(global)
let watermark = new Watermark()
function ImageIpc() { function ImageIpc() {
// 一拆四 // 一拆四
@ -32,5 +34,11 @@ function ImageIpc() {
DEFINE_STRING.IMG.BATCH_PROCESS_IMAGE, DEFINE_STRING.IMG.BATCH_PROCESS_IMAGE,
async (event, value) => await image.BatchProcessImage(value) async (event, value) => await image.BatchProcessImage(value)
) )
ipcMain.handle(
DEFINE_STRING.IMG.PROCESS_IMAGE_WATERMASK_CHECK,
async (event, imageBase64, maskPosition, bookId) =>
await watermark.ProcessImageCheck(imageBase64, maskPosition, bookId)
)
} }
export { ImageIpc } export { ImageIpc }

View File

@ -1,374 +1,381 @@
import axios from "axios"; import axios from 'axios'
import path from "path"; import path from 'path'
import { DEFINE_STRING } from "../../define/define_string"; import { DEFINE_STRING } from '../../define/define_string'
import { define } from "../../define/define"; import { define } from '../../define/define'
import { ImageStyleDefine } from "../../define/iamgeStyleDefine"; import { ImageStyleDefine } from '../../define/iamgeStyleDefine'
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash'
let fspromises = require("fs").promises; let fspromises = require('fs').promises
const sharp = require('sharp'); const sharp = require('sharp')
import { SdSettingDefine } from "../../define/setting/sdSettingDefine"; import { SdSettingDefine } from '../../define/setting/sdSettingDefine'
import { PublicMethod } from "./publicMethod"; import { PublicMethod } from './publicMethod'
import { Tools } from "../tools"; import { Tools } from '../tools'
import { errorMessage, successMessage } from "../Public/generalTools"; import { errorMessage, successMessage } from '../Public/generalTools'
import { SdApi } from "../../api/sdApi"; import { SdApi } from '../../api/sdApi'
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid')
export class SD { export class SD {
constructor(global) { constructor(global) {
this.global = global; this.global = global
this.pm = new PublicMethod(global); this.pm = new PublicMethod(global)
this.tools = new Tools(); this.tools = new Tools()
this.sdApi = new SdApi(); this.sdApi = new SdApi()
} }
/** /**
* 获取当前SD服务器所有的lora信息 * 获取当前SD服务器所有的lora信息
*/ */
async GetAllLoras(baseURL = null) { async GetAllLoras(baseURL = null) {
try { try {
let data = await this.sdApi.getAllLoras(baseURL); let data = await this.sdApi.getAllLoras(baseURL)
return successMessage(data); return successMessage(data)
} catch (error) { } catch (error) {
return errorMessage(error.toString()); return errorMessage(error.toString())
}
}
/**
* 获取所有的checkpoint模型
* @param {*} baseURL
* @returns
*/
async GetAllSDModel(baseURL = null) {
try {
let data = await this.sdApi.getAllSDModel(baseURL)
return successMessage(data)
} catch (error) {
return errorMessage(error.toString())
}
}
/**
* 获取所有的采样器
* @param {*} baseURL
* @returns
*/
async GetAllSamplers(baseURL = null) {
try {
let data = await this.sdApi.getAllSamplers(baseURL)
return successMessage(data)
} catch (error) {
return errorMessage(error.toString())
}
}
/**
* 加载所有的SD数据
* @param {*} baseURL
* @returns
*/
async LoadSDServiceData(baseURL = null) {
try {
// 加载大模型
let sd_model = await this.GetAllSDModel(baseURL)
// 往sd_model中添加一个默认的选项
sd_model.data.data.unshift({
title: '无',
name: '无',
description: '无'
})
// 加载Lora
let lora = await this.GetAllLoras(baseURL)
lora.data.data.unshift({
Key: '无',
name: '无',
description: '无'
})
// 加载采样器
let sampler = await this.GetAllSamplers(baseURL)
sampler.data.data.unshift({
name: '无',
description: '无'
})
if (!(sd_model.code & lora.code & sampler.code)) {
throw new Error('获取SD数据错误请检查SD WEBUI链接')
}
for (let i = 0; i < lora.data.data.length; i++) {
delete lora.data.data[i].metadata
}
let data = {
sd_model: sd_model.data.data,
lora: lora.data.data,
sampler: sampler.data.data
}
// 处理当前获取的数据,保存到配置文件中
await SdSettingDefine.SavePropertyValue('sd_model', data.sd_model)
await SdSettingDefine.SavePropertyValue('lora', data.lora)
await SdSettingDefine.SavePropertyValue('sampler', data.sampler)
return successMessage(data)
} catch (error) {
return errorMessage('加载数据失败,错误信息如下:' + error.toString())
}
}
/**
* 获取图片风格菜单
* @returns 返回图片风格菜单
*
* */
async GetImageStyleMenu() {
try {
let style = ImageStyleDefine.getImageStyleMenu()
return {
code: 1,
data: style
}
} catch (error) {
return {
code: 0,
message: '不可能出现错误'
}
}
}
/**
* 获取指定的ID的风格信息传入的是一个数组
* @param {*} value id集合
*/
async GetImageStyleInfomation(value) {
try {
if (value) {
value = JSON.parse(value)
} else {
value = []
}
value = value ? value : []
let style = ImageStyleDefine.getAllSubStyle()
let tmp = []
for (let i = 0; i < value.length; i++) {
const element = value[i]
for (let j = 0; j < style.length; j++) {
const item = style[j]
if (item.id == element) {
tmp.push(item)
break
}
} }
}
let newSubStyle = cloneDeep(tmp)
for (let i = 0; i < newSubStyle.length; i++) {
const element = newSubStyle[i]
element.image = path.join(define.image_path, 'style/' + element.image)
}
return {
code: 1,
data: newSubStyle
}
} catch (error) {
return {
code: 0,
message: error.toString()
}
} }
}
/** /**
* 获取所有的checkpoint模型 * 获取指定ID的分类的子风格信息
* @param {*} baseURL * @param {*} value ID
* @returns * @returns 返回ID对应的子风格的详细信息
*/ */
async GetAllSDModel(baseURL = null) { async GetStyleImageSubList(value) {
try { try {
let data = await this.sdApi.getAllSDModel(baseURL); let subStyle = ImageStyleDefine.getImagePathById(value)
return successMessage(data); let newSubStyle = cloneDeep(subStyle)
} catch (error) { for (let i = 0; i < newSubStyle.length; i++) {
return errorMessage(error.toString()); const element = newSubStyle[i]
element.image = path.join(
define.image_path,
'style/' + element.image + '?t=' + new Date().getTime()
)
}
return {
code: 1,
data: newSubStyle
}
} catch (error) {
return {
code: 0,
message: error.toString()
}
}
}
/**
* 单张生图
* @param {*} value 0 生图的参数1 图片的表示用于保存 2 baseUrl
* @returns
*/
async txt2img(value) {
try {
value = JSON.parse(value)
let data = value[0]
let res = await this.sdApi.txt2img(data)
// 将base· 64的图片转换为图片
// 将当前的图片保存到指定的文件夹中然后返回文件路径并且可以复制到指定的文件删除exif信息
let image_paths = []
for (let i = 0; res.data.images && i < res.data.images.length; i++) {
const element = res.data.images[i]
let image_data = {
base64: element
} }
// 将保存图片添加到队列中
let image_name = `sd_${Date.now()}_${uuidv4()}.png`
let image_path = path.join(define.temp_sd_image, image_name)
image_path = await this.tools.saveBase64ToImage(element, image_path)
image_data['image_path'] = image_path
image_paths.push(image_data)
}
return successMessage(image_paths)
} catch (error) {
return errorMessage('生图失败,错误信息如下:' + error.toString())
} }
}
/** /**
* 获取所有的采样器 * 生成一次图片的方法可以区分模式
* @param {*} baseURL * @param {图片名称 } image
* @returns * @param {任务队列信息} task_list 301198499
*/ */
async GetAllSamplers(baseURL = null) { async OneImageGeneration(image, task_list, seed = -1) {
try { let taskPath = path.join(this.global.config.project_path, 'scripts/task_list.json')
let data = await this.sdApi.getAllSamplers(baseURL); try {
return successMessage(data); let imageJson = JSON.parse(await fspromises.readFile(image + '.json', 'utf-8'))
} catch (error) { let sd_setting = JSON.parse(await fspromises.readFile(define.sd_setting, 'utf-8'))
return errorMessage(error.toString()); let model = imageJson.model
let image_json = JSON.parse(await fspromises.readFile(image + '.json', 'utf-8'))
let image_path = ''
let target_image_path = ''
if (image_json.name) {
image_path = path.join(
this.global.config.project_path,
`tmp/${task_list.out_folder}/tmp_${image_json.name}`
)
target_image_path = path.join(
this.global.config.project_path,
`tmp/${task_list.out_folder}/${image_json.name}`
)
} else {
image_path =
image.replaceAll('input_crop', task_list.out_folder).split('.png')[0] + '_tmp.png'
target_image_path = image.replaceAll('input_crop', task_list.out_folder)
}
let image_styles = await ImageStyleDefine.getImageStyleStringByIds(
task_list.image_style_list ? task_list.image_style_list : []
)
let prompt = sd_setting.webui.prompt + image_styles
// 拼接提示词
if (task_list.image_style != null) {
prompt += `((${task_list.image_style})), `
}
if (task_list.lora != null) {
prompt += `${task_list.lora}, `
}
prompt += imageJson.webui_config.prompt
// 判断当前是不是有开修脸修手
let ADetailer = {
args: sd_setting.adetailer
}
if (model == 'img2img') {
let web_api = this.global.config.webui_api_url + 'sdapi/v1/img2img'
let sd_config = imageJson['webui_config']
sd_config.prompt = prompt
sd_config.seed = seed
let im = await fspromises.readFile(image, 'binary')
sd_config.init_images = [new Buffer.from(im, 'binary').toString('base64')]
if (imageJson.adetailer) {
let ta = {
ADetailer: ADetailer
}
sd_config.alwayson_scripts = ta
} }
} sd_config.height = sd_setting.webui.height
sd_config.width = sd_setting.webui.width
/** const response = await axios.post(web_api, sd_config)
* 加载所有的SD数据 let info = JSON.parse(response.data.info)
* @param {*} baseURL if (seed == -1) {
* @returns seed = info.seed
*/
async LoadSDServiceData(baseURL = null) {
try {
// 加载大模型
let sd_model = await this.GetAllSDModel(baseURL);
// 往sd_model中添加一个默认的选项
sd_model.data.data.unshift({
title: "无",
name: "无",
description: "无",
})
// 加载Lora
let lora = await this.GetAllLoras(baseURL);
lora.data.data.unshift({
Key: "无",
name: "无",
description: "无",
})
// 加载采样器
let sampler = await this.GetAllSamplers(baseURL);
sampler.data.data.unshift({
name: "无",
description: "无",
})
if (!(sd_model.code & lora.code & sampler.code)) {
throw new Error("获取SD数据错误请检查SD WEBUI链接");
}
for (let i = 0; i < lora.data.data.length; i++) {
delete lora.data.data[i].metadata;
}
let data = {
sd_model: sd_model.data.data,
lora: lora.data.data,
sampler: sampler.data.data
}
// 处理当前获取的数据,保存到配置文件中
await SdSettingDefine.SavePropertyValue("sd_model", data.sd_model);
await SdSettingDefine.SavePropertyValue("lora", data.lora);
await SdSettingDefine.SavePropertyValue("sampler", data.sampler);
return successMessage(data);
} catch (error) {
return errorMessage("加载数据失败,错误信息如下:" + error.toString());
} }
} // 目前是单图出图
let images = response.data.images
/** let imageData = Buffer.from(images[0].split(',', 1)[0], 'base64')
* 获取图片风格菜单 await sharp(imageData)
* @returns 返回图片风格菜单 .toFile(image_path)
* .then(async () => {
* */ // console.log("图生图成功" + image_path);
async GetImageStyleMenu() { await this.tools.deletePngAndDeleteExifData(image_path, target_image_path)
try { })
let style = ImageStyleDefine.getImageStyleMenu(); .catch((err) => {
return { throw new Error(err)
code: 1, })
data: style return seed
} } else if (model == 'txt2img') {
} catch (error) { let body = {
return { prompt: prompt,
code: 0, negative_prompt: imageJson.webui_config.negative_prompt,
message: "不可能出现错误" seed: seed,
} sampler_name: imageJson.webui_config.sampler_name,
// 提示词相关性
cfg_scale: imageJson.webui_config.cfg_scale,
width: sd_setting.webui.width,
height: sd_setting.webui.height,
batch_size: 1,
n_iter: 1,
steps: imageJson.webui_config.steps,
save_images: false
} }
} let web_api = this.global.config.webui_api_url + 'sdapi/v1/txt2img'
/** if (imageJson.adetailer) {
* 获取指定的ID的风格信息传入的是一个数组 let ta = {
* @param {*} value id集合 ADetailer: ADetailer
*/ }
async GetImageStyleInfomation(value) { body.alwayson_scripts = ta
try {
if (value) {
value = JSON.parse(value);
} else {
value = [];
}
value = value ? value : [];
let style = ImageStyleDefine.getAllSubStyle();
let tmp = [];
for (let i = 0; i < value.length; i++) {
const element = value[i];
for (let j = 0; j < style.length; j++) {
const item = style[j];
if (item.id == element) {
tmp.push(item);
break;
}
}
}
let newSubStyle = cloneDeep(tmp);
for (let i = 0; i < newSubStyle.length; i++) {
const element = newSubStyle[i];
element.image = path.join(define.image_path, "style/" + element.image);
}
return {
code: 1,
data: newSubStyle
}
} catch (error) {
return {
code: 0,
message: error.toString()
}
} }
} const response = await axios.post(web_api, body)
let info = JSON.parse(response.data.info)
/** if (seed == -1) {
* 获取指定ID的分类的子风格信息 seed = info.seed
* @param {*} value ID
* @returns 返回ID对应的子风格的详细信息
*/
async GetStyleImageSubList(value) {
try {
let subStyle = ImageStyleDefine.getImagePathById(value);
let newSubStyle = cloneDeep(subStyle);
for (let i = 0; i < newSubStyle.length; i++) {
const element = newSubStyle[i];
element.image = path.join(define.image_path, "style/" + element.image);
}
return {
code: 1,
data: newSubStyle
}
} catch (error) {
return {
code: 0,
message: error.toString()
}
} }
// 目前是单图出图
let images = response.data.images
let imageData = Buffer.from(images[0].split(',', 1)[0], 'base64')
await sharp(imageData)
.toFile(image_path)
.then(async () => {
// console.log("文生图成功" + image_path);
await this.tools.deletePngAndDeleteExifData(image_path, target_image_path)
})
.catch((err) => {
// console.log(err)
throw new Error(err)
})
return seed
} else {
throw new Error('SD 模式错误')
}
} catch (error) {
// 当前队列执行失败移除整个批次的任务
this.global.requestQuene.removeTask(task_list.out_folder, null)
this.global.fileQueue.enqueue(async () => {
// 记录失败状态
let task_list_json = JSON.parse(await fspromises.readFile(taskPath, 'utf-8'))
// 修改指定的列表的数据
task_list_json.task_list.map((a) => {
if (a.id == task_list.id) {
a.status = 'error'
a.errorMessage = error.toString()
}
})
// 写入
await fspromises.writeFile(taskPath, JSON.stringify(task_list_json))
this.global.newWindow[0].win.webContents.send(DEFINE_STRING.IMAGE_TASK_STATUS_REFRESH, {
out_folder: task_list.out_folder,
status: 'error'
})
})
throw error
} }
}
/** }
* 单张生图
* @param {*} value 0 生图的参数1 图片的表示用于保存 2 baseUrl
* @returns
*/
async txt2img(value) {
try {
value = JSON.parse(value);
let data = value[0];
let res = await this.sdApi.txt2img(data);
// 将base· 64的图片转换为图片
// 将当前的图片保存到指定的文件夹中然后返回文件路径并且可以复制到指定的文件删除exif信息
let image_paths = [];
for (let i = 0; res.data.images && i < res.data.images.length; i++) {
const element = res.data.images[i];
let image_data = {
base64: element
}
// 将保存图片添加到队列中
let image_name = `sd_${Date.now()}_${uuidv4()}.png`;
let image_path = path.join(define.temp_sd_image, image_name);
image_path = await this.tools.saveBase64ToImage(element, image_path);
image_data["image_path"] = image_path;
image_paths.push(image_data);
}
return successMessage(image_paths);
} catch (error) {
return errorMessage("生图失败,错误信息如下:" + error.toString());
}
}
/**
* 生成一次图片的方法可以区分模式
* @param {图片名称 } image
* @param {任务队列信息} task_list 301198499
*/
async OneImageGeneration(image, task_list, seed = -1) {
let taskPath = path.join(this.global.config.project_path, "scripts/task_list.json")
try {
let imageJson = JSON.parse(await fspromises.readFile(image + '.json', 'utf-8'));
let sd_setting = JSON.parse(await fspromises.readFile(define.sd_setting, 'utf-8'));
let model = imageJson.model;
let image_json = JSON.parse(await fspromises.readFile(image + '.json', 'utf-8'));
let image_path = "";
let target_image_path = "";
if (image_json.name) {
image_path = path.join(this.global.config.project_path, `tmp/${task_list.out_folder}/tmp_${image_json.name}`)
target_image_path = path.join(this.global.config.project_path, `tmp/${task_list.out_folder}/${image_json.name}`)
} else {
image_path = image.replaceAll("input_crop", task_list.out_folder).split(".png")[0] + "_tmp.png";
target_image_path = image.replaceAll("input_crop", task_list.out_folder);
}
let image_styles = await ImageStyleDefine.getImageStyleStringByIds(task_list.image_style_list ? task_list.image_style_list : []);
let prompt = sd_setting.webui.prompt + image_styles;
// 拼接提示词
if (task_list.image_style != null) {
prompt += `((${task_list.image_style})), `;
}
if (task_list.lora != null) {
prompt += `${task_list.lora}, `;
}
prompt += imageJson.webui_config.prompt;
// 判断当前是不是有开修脸修手
let ADetailer = {
args: sd_setting.adetailer
};
if (model == "img2img") {
let web_api = this.global.config.webui_api_url + 'sdapi/v1/img2img'
let sd_config = imageJson["webui_config"];
sd_config.prompt = prompt;
sd_config.seed = seed;
let im = await fspromises.readFile(image, 'binary');
sd_config.init_images = [new Buffer.from(im, 'binary').toString('base64')];
if (imageJson.adetailer) {
let ta = {
ADetailer: ADetailer
}
sd_config.alwayson_scripts = ta;
}
sd_config.height = sd_setting.webui.height;
sd_config.width = sd_setting.webui.width;
const response = await axios.post(web_api, sd_config);
let info = JSON.parse(response.data.info);
if (seed == -1) {
seed = info.seed;
}
// 目前是单图出图
let images = response.data.images;
let imageData = Buffer.from(images[0].split(",", 1)[0], 'base64');
await sharp(imageData)
.toFile(image_path)
.then(async () => {
// console.log("图生图成功" + image_path);
await this.tools.deletePngAndDeleteExifData(image_path, target_image_path);
})
.catch(err => {
throw new Error(err);
});
return seed;
} else if (model == "txt2img") {
let body = {
"prompt": prompt,
"negative_prompt": imageJson.webui_config.negative_prompt,
"seed": seed,
"sampler_name": imageJson.webui_config.sampler_name,
// 提示词相关性
"cfg_scale": imageJson.webui_config.cfg_scale,
"width": sd_setting.webui.width,
"height": sd_setting.webui.height,
"batch_size": 1,
"n_iter": 1,
"steps": imageJson.webui_config.steps,
"save_images": false,
}
let web_api = this.global.config.webui_api_url + 'sdapi/v1/txt2img';
if (imageJson.adetailer) {
let ta = {
ADetailer: ADetailer
}
body.alwayson_scripts = ta;
}
const response = await axios.post(web_api, body);
let info = JSON.parse(response.data.info);
if (seed == -1) {
seed = info.seed;
}
// 目前是单图出图
let images = response.data.images;
let imageData = Buffer.from(images[0].split(",", 1)[0], 'base64');
await sharp(imageData)
.toFile(image_path)
.then(async () => {
// console.log("文生图成功" + image_path);
await this.tools.deletePngAndDeleteExifData(image_path, target_image_path);
})
.catch(err => {
// console.log(err)
throw new Error(err);
});
return seed;
} else {
throw new Error("SD 模式错误");
}
} catch (error) {
// 当前队列执行失败移除整个批次的任务
this.global.requestQuene.removeTask(task_list.out_folder, null)
this.global.fileQueue.enqueue(async () => {
// 记录失败状态
let task_list_json = JSON.parse(await fspromises.readFile(taskPath, 'utf-8'));
// 修改指定的列表的数据
task_list_json.task_list.map(a => {
if (a.id == task_list.id) {
a.status = "error";
a.errorMessage = error.toString();
}
})
// 写入
await fspromises.writeFile(taskPath, JSON.stringify(task_list_json));
this.global.newWindow[0].win.webContents.send(DEFINE_STRING.IMAGE_TASK_STATUS_REFRESH, {
out_folder: task_list.out_folder,
status: "error"
});
})
throw error;
}
}
}

View File

@ -229,16 +229,27 @@ export class ReverseBook extends BookBasic {
} }
} }
/**
* ID和小说批次任务ID获取小说和小说批次任务数据
* @param bookId ID
* @param bookTaskName ID
* @returns
*/
async GetBookAndTask(bookId: string, bookTaskName: string) { async GetBookAndTask(bookId: string, bookTaskName: string) {
let book = this.bookService.GetBookDataById(bookId) let book = this.bookService.GetBookDataById(bookId)
if (book == null) { if (book == null) {
throw new Error("查找小说数据失败"); throw new Error("查找小说数据失败");
} }
// 获取小说对应的批次任务数据,默认初始化为第一个 // 获取小说对应的批次任务数据,默认初始化为第一个
let bookTaskRes = await this.bookTaskService.GetBookTaskData({ let condition = {
bookId: bookId, bookId: bookId
name: bookTaskName } as Book.QueryBookBackTaskCondition
}) if (bookTaskName == "output_00001") {
condition["name"] = bookTaskName
} else {
condition["id"] = bookTaskName
}
let bookTaskRes = await this.bookTaskService.GetBookTaskData(condition)
if (bookTaskRes.data.bookTasks.length <= 0 || bookTaskRes.data.total <= 0) { if (bookTaskRes.data.bookTasks.length <= 0 || bookTaskRes.data.total <= 0) {
let msg = "没有找到对应的小说批次任务数据" let msg = "没有找到对应的小说批次任务数据"
this.taskScheduler.AddLogToDB(bookId, book.type, msg, OtherData.DEFAULT, LoggerStatus.FAIL) this.taskScheduler.AddLogToDB(bookId, book.type, msg, OtherData.DEFAULT, LoggerStatus.FAIL)
@ -322,12 +333,12 @@ export class ReverseBook extends BookBasic {
} }
/** /**
* *
*/ */
async GetCopywriting(bookId: string): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> { async GetCopywriting(bookId: string, bookTaskId: string = null): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try { try {
await this.InitService() await this.InitService()
let { book, bookTask } = await this.GetBookAndTask(bookId, 'output_00001') let { book, bookTask } = await this.GetBookAndTask(bookId, bookTaskId ? bookTaskId : 'output_00001')
if (isEmpty(book.subtitlePosition)) { if (isEmpty(book.subtitlePosition)) {
throw new Error("请先设置小说的字幕位置") throw new Error("请先设置小说的字幕位置")
} }
@ -390,95 +401,6 @@ export class ReverseBook extends BookBasic {
return errorMessage("获取分镜数据失败,失败信息如下:" + error.message, 'ReverseBook_GetCopywriting') return errorMessage("获取分镜数据失败,失败信息如下:" + error.message, 'ReverseBook_GetCopywriting')
} }
} }
/**
*
* @param bookId ID
*/
async RemoveWatermark(bookId) {
try {
await this.InitService()
let { book, bookTask } = await this.GetBookAndTask(bookId, 'output_00001')
let subtitlePosition = book.subtitlePosition;
if (isEmpty(subtitlePosition)) {
throw new Error("当前没有文案位置的内容,请先进行 ‘开始提取文案 -> 提取文案设置 -> 框选大概位置 -> 保存位置’ 操作保存文案的位置")
}
if (!ValidateJson(subtitlePosition)) {
throw new Error("文案位置格式有误,请先进行 ‘开始提取文案 -> 提取文案设置 -> 框选大概位置 -> 保存位置’ 操作保存文案的位置")
}
let subtitlePositionParse = JSON.parse(subtitlePosition)
if (subtitlePositionParse.length <= 0) {
throw new Error("文案位置格式有误,请先进行 ‘开始提取文案 -> 提取文案设置 -> 框选大概位置 -> 保存位置’ 操作保存文案的位置")
}
let inputImageFolder = path.resolve(define.project_path, `${book.id}/tmp/input`)
if (!(await CheckFileOrDirExist(inputImageFolder))) {
throw new Error("输出文件夹不存在,请先进行抽帧等操作")
}
let iamgePaths = await GetFilesWithExtensions(inputImageFolder, ['.png'])
if (iamgePaths.length <= 0) {
throw new Error("没有检查到抽帧图片,请先进行抽帧等操作")
}
// 处理位置生成蒙板
let inputImage = iamgePaths[0]
let outImagePath = path.resolve(define.project_path, `${book.id}/data/mask/mask_temp_${new Date().getTime()}.png`)
await CheckFolderExistsOrCreate(path.dirname(outImagePath));
await ProcessImage(inputImage, outImagePath, {
x: subtitlePositionParse[0].startX,
y: subtitlePositionParse[0].startY,
width: subtitlePositionParse[0].width,
height: subtitlePositionParse[0].height
})
if (!(await CheckFileOrDirExist(outImagePath))) {
throw new Error("生成蒙板失败")
}
// 开始去除水印
let bookTaskDetails = this.bookTaskDetailService.GetBookTaskData({
bookId: bookId,
bookTaskId: bookTask.id
}).data as Book.SelectBookTaskDetail[]
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i];
let bookInputImage = element.oldImage;
let name = path.basename(bookInputImage)
let bak = path.resolve(define.project_path, `${book.id}/tmp/input/bak/${name}`);
// 做个备份吧
await CopyFileOrFolder(bookInputImage, bak)
await fs.promises.unlink(bookInputImage) // 删除原来的图片
// 开始调用去除水印的方法
let imageBase64 = await GetImageBase64(bak)
let maskBase64 = await GetImageBase64(outImagePath)
let res = await this.watermark.ProcessImage({
imageBase64: imageBase64,
maskBase64: maskBase64,
type: 'file',
inputFilePath: bak,
maskPath: outImagePath,
outFilePath: bookInputImage
})
// 去水印执行完毕
this.sendReturnMessage({
code: 1,
id: element.id,
type: ResponseMessageType.REMOVE_WATERMARK,
data: res
}, DEFINE_STRING.BOOK.REMOVE_WATERMARK_RETURN)
this.taskScheduler.AddLogToDB(bookId, book.type, `${element.name} 去除水印完成`, element.bookTaskId, LoggerStatus.SUCCESS)
}
// 全部完毕
return successMessage(null, "全部图片去除水印完成", "ReverseBook_RemoveWatermark")
} catch (error) {
return errorMessage("去除水印失败,错误信息如下:" + error.message, "ReverseBook_RemoveWatermark")
}
}
//#endregion //#endregion
//#region 反推相关任务 //#region 反推相关任务

View File

@ -3,7 +3,7 @@ import fs from 'fs'
import util from 'util' import util from 'util'
import { exec } from 'child_process' import { exec } from 'child_process'
const execAsync = util.promisify(exec); const execAsync = util.promisify(exec);
import { CheckFileOrDirExist, CheckFolderExistsOrCreate } from '../../define/Tools/file' import { CheckFileOrDirExist, CheckFolderExistsOrCreate, CopyFileOrFolder, GetFilesWithExtensions } from '../../define/Tools/file'
import { errorMessage, successMessage } from '../Public/generalTools' import { errorMessage, successMessage } from '../Public/generalTools'
import { SoftwareService } from '../../define/db/service/SoftWare/softwareService' import { SoftwareService } from '../../define/db/service/SoftWare/softwareService'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
@ -12,15 +12,28 @@ import { ValidateJson } from '../../define/Tools/validate'
import { define } from '../../define/define' import { define } from '../../define/define'
import { LOGGER_DEFINE } from '../../define/logger_define' import { LOGGER_DEFINE } from '../../define/logger_define'
import axios from 'axios' import axios from 'axios'
import { Base64ToFile } from '../../define/Tools/image' import { Base64ToFile, GetImageBase64 } from '../../define/Tools/image'
import { TaskScheduler } from './taskScheduler'; import { TaskScheduler } from './taskScheduler';
import { LoggerStatus, OtherData } from '../../define/enum/softwareEnum'; import { LoggerStatus, OtherData, ResponseMessageType } from '../../define/enum/softwareEnum';
import { basicApi } from '../../api/apiBasic'; import { basicApi } from '../../api/apiBasic';
import { FfmpegOptions } from './ffmpegOptions';
import { ProcessImage } from '../../define/Tools/image';
import { BookService } from '../../define/db/service/Book/bookService';
import { OperateBookType } from '../../define/enum/bookEnum';
import { GeneralResponse } from '../../model/generalResponse';
import { BookTaskDetailService } from '../../define/db/service/Book/bookTaskDetailService';
import { Book } from '../../model/book';
import { DEFINE_STRING } from '../../define/define_string';
import { BookTaskService } from '../../define/db/service/Book/bookTaskService';
export class Watermark { export class Watermark {
softwareService: SoftwareService softwareService: SoftwareService
taskScheduler: TaskScheduler; taskScheduler: TaskScheduler;
constructor() { } bookService: BookService
bookTaskDetailService: BookTaskDetailService
bookTaskService: BookTaskService
constructor() {
}
async InitService() { async InitService() {
if (!this.softwareService) { if (!this.softwareService) {
@ -29,6 +42,20 @@ export class Watermark {
if (!this.taskScheduler) { if (!this.taskScheduler) {
this.taskScheduler = new TaskScheduler() this.taskScheduler = new TaskScheduler()
} }
if (!this.bookService) {
this.bookService = await BookService.getInstance()
}
if (!this.bookTaskDetailService) {
this.bookTaskDetailService = await BookTaskDetailService.getInstance()
}
if (!this.bookTaskService) {
this.bookTaskService = await BookTaskService.getInstance()
}
}
// 主动返回前端的消息
sendReturnMessage(data: GeneralResponse.MessageResponse, message_name = DEFINE_STRING.BOOK.GET_COPYWRITING_RETURN) {
global.newWindow[0].win.webContents.send(message_name, data)
} }
//#region 设置 //#region 设置
@ -114,7 +141,7 @@ export class Watermark {
//#endregion //#endregion
//#region 去除水印 //#region 去除水印相关操作
/** /**
* lama * lama
@ -234,7 +261,7 @@ export class Watermark {
* iopaint * iopaint
* @param value * @param value
*/ */
async ProcessImage(value: ImageModel.ProcessImageParams) { async ProcessImage(value: ImageModel.ProcessImageParams): Promise<string | Buffer> {
let outDir = path.dirname(value.outFilePath) let outDir = path.dirname(value.outFilePath)
await CheckFolderExistsOrCreate(outDir); await CheckFolderExistsOrCreate(outDir);
@ -253,6 +280,182 @@ export class Watermark {
throw new Error('未知的去除水印模式') throw new Error('未知的去除水印模式')
} }
} }
//#endregion
//#region 开始去除水印
/**
*
* @param imageBase64 base64
* @param maskPosition
*/
async ProcessImageCheck(imageBase64: string, maskPosition: [], bookId: string) {
try {
await this.InitService()
// 开始做处理
console.log(imageBase64, maskPosition);
let book = this.bookService.GetBookDataById(bookId)
if (book == null) {
throw new Error('指定的小说数据不存在')
}
let inputFilePath = path.resolve(book.bookFolderPath, `data/mask/temp/${new Date().getTime()}.png`)
await CheckFolderExistsOrCreate(path.dirname(inputFilePath))
const base64Image = imageBase64.split(';base64,').pop();
await fs.promises.writeFile(inputFilePath, base64Image, { encoding: 'base64' })
let outputPath = path.resolve(book.bookFolderPath, `data/mask/mask_temp_${new Date().getTime()}.png`)
let outImagePath = path.resolve(book.bookFolderPath, `data/mask/temp/out_${new Date().getTime()}.png`)
await ProcessImage(inputFilePath, outputPath, maskPosition.map((item: any) => { return { x: item.startX, y: item.startY, width: item.width, height: item.height } }))
let markBase64 = await GetImageBase64(outputPath)
// 开始去谁赢
let res = await this.ProcessImage({
imageBase64: imageBase64,
maskBase64: markBase64, // 处理蒙板的base64
type: 'arrayBuffer', // 返回数据的类型是直接写道输出文件地址还是返回Buffer数组
inputFilePath: inputFilePath, // 输入文件的地址(待处理的)
maskPath: outputPath, // 蒙板文件的地址
outFilePath: outImagePath // 输出文件的地址
})
// 将得到的buffer返回前端进行渲染
return successMessage(res, "去除水印成功", "Image_ProcessImageCheck")
} catch (error) {
return errorMessage('去除图片指定位置的水印失败,失败信息如下:' + error.toString(), 'Image_ProcessImageCheck')
}
}
/**
*
* @param bookId ID
*/
async RemoveWatermark(id: string, operateBookType: OperateBookType): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
await this.InitService()
let book = undefined as Book.SelectBook
let bookTask = undefined as Book.SelectBookTask
let bookTaskDetails = undefined as Book.SelectBookTaskDetail[]
if (operateBookType == OperateBookType.BOOKTASK) {
bookTask = this.bookTaskService.GetBookTaskDataById(id);
if (bookTask == null) {
throw new Error('指定的小说任务数据不存在')
}
book = this.bookService.GetBookDataById(bookTask.bookId)
if (book == null) {
throw new Error("小说数据不存在,请检查")
}
bookTaskDetails = this.bookTaskDetailService.GetBookTaskData({
bookTaskId: bookTask.id,
bookId: bookTask.bookId
}).data as Book.SelectBookTaskDetail[]
} else if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
bookTask = this.bookTaskService.GetBookTaskDataById(id);
if (bookTask == null) {
throw new Error('指定的小说任务数据不存在')
}
book = this.bookService.GetBookDataById(bookTask.bookId)
if (book == null) {
throw new Error("小说数据不存在,请检查")
}
let bookTaskDetail = this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (bookTaskDetail == null) {
throw new Error("指定的小说任务分镜信息不存在,请检查")
}
bookTaskDetails = [bookTaskDetail];
} else {
throw new Error("未知的操作类型")
}
let watermarkPosition = book.watermarkPosition;
if (isEmpty(watermarkPosition)) {
throw new Error("当前没有文案位置的内容,请先进行 ‘开始去除水印 -> 选择水印位置 -> 框选大概位置 -> 保存蒙板位置’ 操作保存蒙板的位置")
}
if (!ValidateJson(watermarkPosition)) {
throw new Error("文案位置格式有误,请先进行 ‘开始去除水印 -> 选择水印位置 -> 框选大概位置 -> 保存蒙板位置’ 操作保存蒙板的位置")
}
let watermarkPositionParse = JSON.parse(watermarkPosition) as any[]
if (watermarkPositionParse.length <= 0) {
throw new Error("没有检测到去除水印的蒙板位置,请先进行 ‘开始去除水印 -> 选择水印位置 -> 框选大概位置 -> 保存蒙板位置’ 操作保存蒙板的位置")
}
let inputImageFolder = path.resolve(define.project_path, `${book.id}/tmp/input`)
if (!(await CheckFileOrDirExist(inputImageFolder))) {
throw new Error("输出文件夹不存在,请先进行抽帧等操作")
}
let iamgePaths = await GetFilesWithExtensions(inputImageFolder, ['.png'])
if (iamgePaths.length <= 0) {
throw new Error("没有检查到抽帧图片,请先进行抽帧等操作")
}
// 处理位置生成蒙板
let inputImage = iamgePaths[0]
let outImagePath = path.resolve(define.project_path, `${book.id}/data/mask/mask_temp_${new Date().getTime()}.png`)
await CheckFolderExistsOrCreate(path.dirname(outImagePath));
// 这边要计算倍率
let watermarkPositionArray = watermarkPositionParse.map(item => {
return {
x: item.startX,
y: item.startY,
width: item.width,
height: item.height,
imageWidth: item.imageWidth,
imageHeight: item.imageHeight
}
})
await ProcessImage(inputImage, outImagePath, watermarkPositionArray)
if (!(await CheckFileOrDirExist(outImagePath))) {
throw new Error("生成蒙板失败")
}
// 开始去除水印
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i];
let bookInputImage = element.oldImage;
let name = path.basename(bookInputImage)
let bak = path.resolve(define.project_path, `${book.id}/tmp/input/bak/${name}`);
// 做个备份吧
await CopyFileOrFolder(bookInputImage, bak)
// 新的输出地址
let tempOutImagePath = path.join(path.dirname(bookInputImage), "temp_" + path.basename(outImagePath))
// await fs.promises.unlink(bookInputImage) // 删除原来的图片
// 开始调用去除水印的方法
let imageBase64 = await GetImageBase64(bak)
let maskBase64 = await GetImageBase64(outImagePath)
let res = await this.ProcessImage({
imageBase64: imageBase64,
maskBase64: maskBase64,
type: 'file',
inputFilePath: bak,
maskPath: outImagePath,
outFilePath: tempOutImagePath
})
// 执行完成,将缓存的图片删除,复制新的图片到原来的位置
await fs.promises.unlink(bookInputImage)
await CopyFileOrFolder(tempOutImagePath, bookInputImage)
// 删除临时文件
await fs.promises.unlink(tempOutImagePath)
// 去水印执行完毕
this.sendReturnMessage({
code: 1,
id: element.id,
type: ResponseMessageType.REMOVE_WATERMARK,
data: bookInputImage
}, DEFINE_STRING.BOOK.REMOVE_WATERMARK_RETURN)
this.taskScheduler.AddLogToDB(book.id, book.type, `${element.name} 去除水印完成`, element.bookTaskId, LoggerStatus.SUCCESS)
}
// 全部完毕
return successMessage(null, "全部图片去除水印完成", "ReverseBook_RemoveWatermark")
} catch (error) {
return errorMessage("去除水印失败,错误信息如下:" + error.message, "ReverseBook_RemoveWatermark")
}
}
//#endregion //#endregion
} }

View File

@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'
import { version } from '../../package.json' import { version } from '../../package.json'
import { graphics } from 'systeminformation' import { graphics } from 'systeminformation'
import { app, shell, BrowserWindow, ipcMain, dialog, nativeTheme } from 'electron' import { app, shell, BrowserWindow, ipcMain, dialog, nativeTheme, session } from 'electron'
import path, { join } from 'path' import path, { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils' import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.ico?asset' import icon from '../../resources/icon.ico?asset'
@ -52,7 +52,7 @@ async function createWindow(hash = 'ShowMessage', data, url = null) {
// 判断当前是不是有设置的宽高,用的话记忆 // 判断当前是不是有设置的宽高,用的话记忆
let isRe = let isRe =
global.config.window_wh_bm_remember && hash == 'ShowMessage' && global.config.window_wh_bm global.config.window_wh_bm_remember && hash == 'ShowMessage' && global.config.window_wh_bm
const ses = session.fromPartition('persist:my-session')
let mainWindow = new BrowserWindow({ let mainWindow = new BrowserWindow({
width: isRe ? global.config.window_wh_bm.width : 900, width: isRe ? global.config.window_wh_bm.width : 900,
height: isRe ? global.config.window_wh_bm.height : 675, height: isRe ? global.config.window_wh_bm.height : 675,
@ -69,7 +69,9 @@ async function createWindow(hash = 'ShowMessage', data, url = null) {
nodeIntegration: hash == 'discord' ? false : true, // 在网页中集成Node nodeIntegration: hash == 'discord' ? false : true, // 在网页中集成Node
nodeIntegrationInWorker: true, nodeIntegrationInWorker: true,
webSecurity: false, webSecurity: false,
partition: 'persist:my-partition' partition: 'persist:my-partition',
session: ses,
webviewTag : true,
} }
}) })

1
src/model/book.d.ts vendored
View File

@ -18,6 +18,7 @@ declare namespace Book {
backgroundMusic?: string | null // 背景音乐ID backgroundMusic?: string | null // 背景音乐ID
friendlyReminder?: string | null // 友情提示 friendlyReminder?: string | null // 友情提示
subtitlePosition?: string, subtitlePosition?: string,
watermarkPosition?: string
updateTime?: Date, updateTime?: Date,
createTime?: Date, createTime?: Date,
version?: string, version?: string,

View File

@ -61,12 +61,12 @@ const book = {
Framing: async (bookId) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.FRAMING, bookId), Framing: async (bookId) => await ipcRenderer.invoke(DEFINE_STRING.BOOK.FRAMING, bookId),
// 获取文案信息 // 获取文案信息
GetCopywriting: async (bookId) => GetCopywriting: async (bookId, bookTaskId) =>
await ipcRenderer.invoke(DEFINE_STRING.BOOK.GET_COPYWRITING, bookId), await ipcRenderer.invoke(DEFINE_STRING.BOOK.GET_COPYWRITING, bookId, bookTaskId),
// 去除所有水印 // 去除所有水印
RemoveWatermark: async (bookId) => RemoveWatermark: async (id,operateBookType) =>
await ipcRenderer.invoke(DEFINE_STRING.BOOK.REMOVE_WATERMARK, bookId), await ipcRenderer.invoke(DEFINE_STRING.BOOK.REMOVE_WATERMARK, id,operateBookType),
// 添加单句推理的 // 添加单句推理的
AddReversePrompt: async (bookTaskDetailIds, type) => AddReversePrompt: async (bookTaskDetailIds, type) =>

View File

@ -5,7 +5,12 @@ import { Book } from '../model/book'
const db = { const db = {
//#region 小说相关的修改 //#region 小说相关的修改
// 修改小说人物的数据 // 修改小说数据
UpdateBookData: async (bookId: string, data: Book.SelectBook) => {
return await ipcRenderer.invoke(DEFINE_STRING.DB.UPDATE_BOOK_DATA, bookId, data)
},
// 修改小说任务的数据
UpdateBookTaskData: async (bookTaskId: string, data: Book.SelectBookTask) => { UpdateBookTaskData: async (bookTaskId: string, data: Book.SelectBookTask) => {
return await ipcRenderer.invoke(DEFINE_STRING.DB.UPDATE_BOOK_TASK_DATA, bookTaskId, data) return await ipcRenderer.invoke(DEFINE_STRING.DB.UPDATE_BOOK_TASK_DATA, bookTaskId, data)
}, },

View File

@ -1,20 +1,30 @@
import { ipcRenderer } from "electron" import { ipcRenderer } from 'electron'
import { DEFINE_STRING } from "../define/define_string" import { DEFINE_STRING } from '../define/define_string'
const img = { const img = {
// 加载当前链接的SD服务数据 // 加载当前链接的SD服务数据
OneSplitFour: async (value, callback) => callback(await ipcRenderer.invoke(DEFINE_STRING.IMG.ONE_SPLIT_FOUR, value)), OneSplitFour: async (value, callback) =>
callback(await ipcRenderer.invoke(DEFINE_STRING.IMG.ONE_SPLIT_FOUR, value)),
// 将base64的图片转换为文件 // 将base64的图片转换为文件
Base64ToFile: async (value, callback) => callback(await ipcRenderer.invoke(DEFINE_STRING.IMG.BASE64_TO_FILE, value)), Base64ToFile: async (value, callback) =>
callback(await ipcRenderer.invoke(DEFINE_STRING.IMG.BASE64_TO_FILE, value)),
// 请求图片处理,去除水印 // 请求图片处理,去除水印
ProcessImage: async (value, callback) => callback(await ipcRenderer.invoke(DEFINE_STRING.IMG.PROCESS_IMAGE, value)), ProcessImage: async (value, callback) =>
callback(await ipcRenderer.invoke(DEFINE_STRING.IMG.PROCESS_IMAGE, value)),
//批量处理图片,去除水印 //批量处理图片,去除水印
BatchProcessImage: async (value, callback) => callback(await ipcRenderer.invoke(DEFINE_STRING.IMG.BATCH_PROCESS_IMAGE, value)), BatchProcessImage: async (value, callback) =>
callback(await ipcRenderer.invoke(DEFINE_STRING.IMG.BATCH_PROCESS_IMAGE, value)),
// 检查水印处理
ProcessImageCheck: async (imageBase64, maskPosition, bookId) =>
await ipcRenderer.invoke(
DEFINE_STRING.IMG.PROCESS_IMAGE_WATERMASK_CHECK,
imageBase64,
maskPosition,
bookId
)
} }
export { export { img }
img
}

View File

@ -6,7 +6,7 @@
<n-message-provider> <n-message-provider>
<n-dialog-provider> <n-dialog-provider>
<n-notification-provider> <n-notification-provider>
<n-spin style="z-index: 100;" :show="softwareStore.spin.spinning"> <n-spin style="z-index: 1000;" :show="softwareStore.spin.spinning">
<RouterView></RouterView> <RouterView></RouterView>
<template #description> {{ softwareStore.spin.tip }} </template> <template #description> {{ softwareStore.spin.tip }} </template>
</n-spin> </n-spin>

View File

@ -0,0 +1,32 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 32 32"
>
<path
d="M8 9H4a2 2 0 0 0-2 2v12h2v-5h4v5h2V11a2 2 0 0 0-2-2zm-4 7v-5h4v5z"
fill="currentColor"
></path>
<path d="M22 11h3v10h-3v2h8v-2h-3V11h3V9h-8v2z" fill="currentColor"></path>
<path
d="M14 23h-2V9h6a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-4zm0-7h4v-5h-4z"
fill="currentColor"
></path>
</svg>
</template>
<script>
import { ref, onMounted, defineComponent, onUnmounted, toRaw, watch } from 'vue'
import { useMessage } from 'naive-ui'
export default defineComponent({
components: {},
setup() {
onMounted(async () => {})
return {}
}
})
</script>

View File

@ -0,0 +1,16 @@
<template>
<div style="width: 100%; height: 100%">
<webview style="width: 100%; height: 100%" src="https://api.laitool.cc" partition="persist:your-session-name"></webview>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
components: {},
setup() {
return {}
}
})
</script>

View File

@ -74,7 +74,12 @@
> >
</n-form-item> </n-form-item>
<n-form-item label="背景音乐" path="srtPath"> <n-form-item label="背景音乐" path="srtPath">
<n-select v-model:value="reverseManageStore.selectBook.backgroundMusic" :options="backgroundMusicOptions" placeholder="请选择背景音乐"> </n-select> <n-select
v-model:value="reverseManageStore.selectBook.backgroundMusic"
:options="backgroundMusicOptions"
placeholder="请选择背景音乐"
>
</n-select>
</n-form-item> </n-form-item>
<n-form-item label="小说项目文件夹" path="bookFolderPath"> <n-form-item label="小说项目文件夹" path="bookFolderPath">
<n-input <n-input
@ -99,7 +104,7 @@
> >
</n-form-item> </n-form-item>
<div style="margin-left: 10px; display: flex; justify-content: flex-end"> <div style="margin-left: 10px; display: flex; justify-content: flex-end">
<n-button type="success" @click="AddOrModifyBook">{{ <n-button :loading="loading" type="success" @click="AddOrModifyBook">{{
type == 'add' ? '添加' : '保存' type == 'add' ? '添加' : '保存'
}}</n-button> }}</n-button>
</div> </div>
@ -113,6 +118,7 @@ import { useMessage, NButton, NForm, NFormItem, NInput, NSelect, NIcon } from 'n
import { useReverseManageStore } from '../../../../../stores/reverseManage.ts' import { useReverseManageStore } from '../../../../../stores/reverseManage.ts'
import { CloseSharp } from '@vicons/ionicons5' import { CloseSharp } from '@vicons/ionicons5'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { useSoftwareStore } from '../../../../../stores/software'
export default defineComponent({ export default defineComponent({
components: { components: {
@ -128,7 +134,9 @@ export default defineComponent({
setup(props) { setup(props) {
let message = useMessage() let message = useMessage()
let reverseManageStore = useReverseManageStore() let reverseManageStore = useReverseManageStore()
let softwareStore = useSoftwareStore()
let bookRef = ref(null) let bookRef = ref(null)
let loading = ref(false)
let backgroundMusicOptions = ref([]) let backgroundMusicOptions = ref([])
// let data = ref(reverseManageStore.selectBook) // let data = ref(reverseManageStore.selectBook)
let type = ref(props.type ? props.type : 'add') let type = ref(props.type ? props.type : 'add')
@ -220,7 +228,9 @@ export default defineComponent({
return return
} }
debugger debugger
loading.value = true
let res = await reverseManageStore.SaveSelectBook(cloneDeep(reverseManageStore.selectBook)) let res = await reverseManageStore.SaveSelectBook(cloneDeep(reverseManageStore.selectBook))
loading.value = false
// ID // ID
if (res.code == 0) { if (res.code == 0) {
message.error(res.message) message.error(res.message)
@ -279,8 +289,9 @@ export default defineComponent({
SelectMP4File, SelectMP4File,
AddOrModifyBook, AddOrModifyBook,
bookRef, bookRef,
softwareStore,
OpenFolder, OpenFolder,
backgroundMusicOptions backgroundMusicOptions,loading
} }
} }
}) })

View File

@ -9,7 +9,7 @@
</n-dropdown> </n-dropdown>
<n-divider vertical style="height: 30px" /> <n-divider vertical style="height: 30px" />
<n-dropdown trigger="hover" :options="frameOptions" @select="handleSelect"> <n-dropdown trigger="hover" :options="frameOptions" @select="handleSelect">
<n-button :color="softwareStore.SoftColor.ORANGE" @click=""> 开始分镜 </n-button> <n-button :color="softwareStore.SoftColor.ORANGE" @click="ActionFrame"> 开始分镜 </n-button>
</n-dropdown> </n-dropdown>
<n-dropdown trigger="hover" :options="copywritingOptions" @select="handleSelect"> <n-dropdown trigger="hover" :options="copywritingOptions" @select="handleSelect">
<n-button <n-button
@ -102,18 +102,20 @@ import VideoCanvas from '../../VideoSubtitle/VideoCanvas.vue'
import { SubtitleSavePositionType } from '../../../../../define/enum/waterMarkAndSubtitle' import { SubtitleSavePositionType } from '../../../../../define/enum/waterMarkAndSubtitle'
import { DEFINE_STRING } from '../../../../../define/define_string' import { DEFINE_STRING } from '../../../../../define/define_string'
import Setting from './Setting.vue' import Setting from './Setting.vue'
import { BookType } from '../../../../../define/enum/bookEnum' import { BookType, OperateBookType } from '../../../../../define/enum/bookEnum'
import TranslateSetting from '../../Setting/TranslateSetting.vue' import TranslateSetting from '../../Setting/TranslateSetting.vue'
import { TranslateType } from '../../../../../define/enum/translate' import { TranslateType } from '../../../../../define/enum/translate'
import { ContainsChineseOrPunctuation } from '../../../../../define/Tools/common' import { ContainsChineseOrPunctuation } from '../../../../../define/Tools/common'
import ImportWordAndSrt from '../../Original/Components/ImportWordAndSrt.vue' import ImportWordAndSrt from '../../Original/Components/ImportWordAndSrt.vue'
import GetWaterMaskRectangle from '../../Watermark/GetWaterMaskRectangle.vue'
export default defineComponent({ export default defineComponent({
components: { components: {
NButton, NButton,
NCheckbox, NCheckbox,
NDropdown, NDropdown,
NDivider NDivider,
GetWaterMaskRectangle
}, },
setup() { setup() {
@ -268,7 +270,10 @@ export default defineComponent({
}) })
return return
} }
let res_frame = await window.book.RemoveWatermark(reverseManageStore.selectBook.id) let res_frame = await window.book.RemoveWatermark(
reverseManageStore.selectBookTask.id,
OperateBookType.BOOKTASK
)
softwareStore.spin.spinning = false softwareStore.spin.spinning = false
window.api.showGlobalMessageDialog(res_frame) window.api.showGlobalMessageDialog(res_frame)
} }
@ -292,21 +297,45 @@ export default defineComponent({
}) })
} }
/**
* 获取水印位置
*/
async function GetWatermarkPosition() {
//
//
//
let dialogWidth = window.innerWidth * 0.7
let dialogHeight = window.innerHeight * 0.9
// ImportWordAndSrt
dialog.create({
showIcon: false,
closeOnEsc: false,
content: () => h(GetWaterMaskRectangle, { width: dialogWidth, height: dialogHeight }),
style: `width : ${dialogWidth}px; height : ${dialogHeight}px`,
maskClosable: false,
onClose: () => {}
})
}
//
async function handleSelect(key) { async function handleSelect(key) {
switch (key) { switch (key) {
case 'compute_frame': case 'compute_frame': //
await ComputeStoryboard() await ComputeStoryboard()
break break
case 'framing': case 'framing': //
await Framing() await Framing()
break break
case 'recognizing_setting': case 'recognizing_setting': //
await GetCopywritingSetting() await GetCopywritingSetting()
break break
case 'mj_reverse': case 'watermark_position': //
await GetWatermarkPosition()
break
case 'mj_reverse': // MJ
await ImageReversePrompt(BookType.MJ_REVERSE) await ImageReversePrompt(BookType.MJ_REVERSE)
break break
case 'sd_reverse': case 'sd_reverse': // SD
await ImageReversePrompt(BookType.SD_REVERSE) await ImageReversePrompt(BookType.SD_REVERSE)
break break
case 'remove_reverse': case 'remove_reverse':
@ -570,12 +599,17 @@ export default defineComponent({
}) })
} }
async function ActionFrame() {
message.info('请使用下面菜单中的按钮进行对应的操作')
}
return { return {
RetuenMain, RetuenMain,
AutoAction, AutoAction,
ImportWordAndSrtFunc, ImportWordAndSrtFunc,
reverseManageStore, reverseManageStore,
softwareStore, softwareStore,
ActionFrame,
switchLogger, switchLogger,
ComputeStoryboard, ComputeStoryboard,
OpenSettingDialog, OpenSettingDialog,
@ -606,7 +640,7 @@ export default defineComponent({
], ],
copywritingOptions: [ copywritingOptions: [
{ {
label: '提取文案置', label: '提取文案置',
key: 'recognizing_setting' key: 'recognizing_setting'
}, },
{ {
@ -616,8 +650,8 @@ export default defineComponent({
], ],
watermarkOptions: [ watermarkOptions: [
{ {
label: '去除水印设置', label: '选择水印位置',
key: 'watermark_setting' key: 'watermark_position'
}, },
{ {
label: '停止去除', label: '停止去除',

View File

@ -52,14 +52,14 @@ import {
DuplicateOutline, DuplicateOutline,
GridOutline, GridOutline,
RadioOutline, RadioOutline,
BookOutline BookOutline,
} from '@vicons/ionicons5' } from '@vicons/ionicons5'
import CheckMachineId from '../Components/CheckMachineId.vue' import CheckMachineId from '../Components/CheckMachineId.vue'
import axios from 'axios'
import { DEFINE_STRING } from '../../../../define/define_string' import { DEFINE_STRING } from '../../../../define/define_string'
import ShowMessage from './ShowMessage.vue' import ShowMessage from './ShowMessage.vue'
import { MD5 } from 'crypto-js' import { MD5 } from 'crypto-js'
import InputDialogContent from '../Original/Components/InputDialogContent.vue' import InputDialogContent from '../Original/Components/InputDialogContent.vue'
import APIIcon from '../APIService/APIIcon.vue'
export default defineComponent({ export default defineComponent({
components: { components: {
@ -71,7 +71,8 @@ export default defineComponent({
NIcon, NIcon,
ShowMessage, ShowMessage,
NSwitch, NSwitch,
NButton NButton,
APIIcon
}, },
setup() { setup() {
let collapsed = ref(false) let collapsed = ref(false)
@ -89,6 +90,7 @@ export default defineComponent({
if (option.key == 'setting') return h(NIcon, null, { default: () => h(SettingsOutline) }) if (option.key == 'setting') return h(NIcon, null, { default: () => h(SettingsOutline) })
if (option.key == 'gptCopywriting') return h(NIcon, null, { default: () => h(BookOutline) }) if (option.key == 'gptCopywriting') return h(NIcon, null, { default: () => h(BookOutline) })
if (option.key == 'book_management') return h(NIcon, null, { default: () => h(GridOutline) }) if (option.key == 'book_management') return h(NIcon, null, { default: () => h(GridOutline) })
if (option.key == 'lai_api') return h(NIcon, null, { default: () => h(APIIcon) })
if (option.key == 'backward_matrix') if (option.key == 'backward_matrix')
return h(NIcon, null, { default: () => h(DuplicateOutline) }) return h(NIcon, null, { default: () => h(DuplicateOutline) })
if (option.key == 'TTS_Services') return h(NIcon, null, { default: () => h(RadioOutline) }) if (option.key == 'TTS_Services') return h(NIcon, null, { default: () => h(RadioOutline) })
@ -452,21 +454,21 @@ export default defineComponent({
} }
] ]
}, },
// { {
// label: () => label: () =>
// h( h(
// RouterLink, RouterLink,
// { {
// to: { to: {
// name: 'test_options' name: 'lai_api'
// } }
// }, },
// { {
// default: () => '' default: () => 'API服务'
// } }
// ), ),
// key: 'test_options' key: 'lai_api'
// }, },
{ {
label: () => label: () =>
h( h(

View File

@ -3,16 +3,26 @@
<div style="width: 500px; position: relative"> <div style="width: 500px; position: relative">
<n-form ref="formRef" label-placement="top" :model="characterData" :rules="rules"> <n-form ref="formRef" label-placement="top" :model="characterData" :rules="rules">
<n-form-item label="人物名称" path="label"> <n-form-item label="人物名称" path="label">
<n-input style="width: 400px;" v-model:value="characterData.label" placeholder="请输入人物名称" /> <n-input
style="width: 400px"
v-model:value="characterData.label"
placeholder="请输入人物名称"
/>
<n-checkbox style="margin-left: 20px" v-model:checked="characterData.isShow" <n-checkbox style="margin-left: 20px" v-model:checked="characterData.isShow"
>显示</n-checkbox >显示</n-checkbox
> >
<n-image <div
style="position: absolute; left: 520px; top: 0" style="position: absolute; left: 520px; top: 0; display: flex; flex-direction: column"
width="120" >
height="120" <n-image
:src="png_base64 ? png_base64 : characterData.show_image" width="120"
/> height="120"
:src="png_base64 ? png_base64 : characterData.show_image"
/>
<n-button color="#e18a3b" style="margin-top: 5px" size="tiny" @click="SelecImage"
>本地上传图片</n-button
>
</div>
</n-form-item> </n-form-item>
<n-form-item label="人物别名(可多个)"> <n-form-item label="人物别名(可多个)">
<n-dynamic-tags type="success" v-model:value="alias_tags" /> <n-dynamic-tags type="success" v-model:value="alias_tags" />
@ -36,7 +46,7 @@
style="margin-top: 10px" style="margin-top: 10px"
@click="GenerateCharacterImage" @click="GenerateCharacterImage"
:loading="imageLoading" :loading="imageLoading"
>生成图片</n-button >SD生成图片</n-button
> >
</div> </div>
</n-form-item> </n-form-item>
@ -188,7 +198,6 @@ export default defineComponent({
await window.mj.SaveTagPropertyData( await window.mj.SaveTagPropertyData(
[JSON.stringify(characterData.value), 'character_tags'], [JSON.stringify(characterData.value), 'character_tags'],
(value) => { (value) => {
console.log(value) console.log(value)
if (value.code == 0) { if (value.code == 0) {
message.error(value.message) message.error(value.message)
@ -280,7 +289,6 @@ export default defineComponent({
} }
imageLoading.value = true imageLoading.value = true
await window.sd.txt2img(JSON.stringify([d]), async (value) => { await window.sd.txt2img(JSON.stringify([d]), async (value) => {
if (value.code == 0) { if (value.code == 0) {
message.error(value.message) message.error(value.message)
imageLoading.value = false imageLoading.value = false
@ -298,8 +306,40 @@ export default defineComponent({
}) })
} }
/**
* 选择本地图片
*/
async function SelecImage() {
await window.api.SelectFile(
[
'jpeg',
'jpg',
'png',
'gif',
'bmp',
'tiff',
'tif',
'webp',
'svg',
'raw',
'cr2',
'nef',
'arw'
],
(value) => {
if (value.code == 0) {
message.error(value.message)
return
}
png_base64.value = null
characterData.value.show_image = value.value
}
)
}
return { return {
characterData, characterData,
SelecImage,
alias_tags, alias_tags,
SaveCharacterTag, SaveCharacterTag,
TranslatePrompt, TranslatePrompt,

View File

@ -3,12 +3,12 @@
<n-form label-placement="top" ref="formRef" :rules="rules" :model="styleData"> <n-form label-placement="top" ref="formRef" :rules="rules" :model="styleData">
<n-form-item label="风格名称" path="label"> <n-form-item label="风格名称" path="label">
<n-input v-model:value="styleData.label" placeholder="请输入风格名称" /> <n-input v-model:value="styleData.label" placeholder="请输入风格名称" />
<n-image <div style="position: absolute; left: 520px; top: 0; display: flex; flex-direction: column">
style="position: absolute; left: 520px; top: 0" <n-image width="120" height="120" :src="png_base64 ? png_base64 : styleData.show_image" />
width="120" <n-button color="#e18a3b" style="margin-top: 5px" size="tiny" @click="SelecImage"
height="120" >本地上传图片</n-button
:src="png_base64 ? png_base64 : styleData.show_image" >
/> </div>
</n-form-item> </n-form-item>
<n-form-item label="风格提示词描述(中文)"> <n-form-item label="风格提示词描述(中文)">
<n-input <n-input
@ -29,7 +29,7 @@
style="margin-top: 10px" style="margin-top: 10px"
@click="GenerateStyleImage" @click="GenerateStyleImage"
:loading="imageLoading" :loading="imageLoading"
>生成图片</n-button >SD生成图片</n-button
> >
</template> </template>
使用SD出图模式生成风格图片提示词为1gril加上风格提示词 使用SD出图模式生成风格图片提示词为1gril加上风格提示词
@ -149,6 +149,9 @@ export default defineComponent({
return return
} }
styleData.value['type'] = 'style_main' styleData.value['type'] = 'style_main'
styleData.value['show_image'] = styleData.value.show_image
? styleData.value.show_image.split('?t')[0]
: null
// //
// //
await window.mj.SaveTagPropertyData( await window.mj.SaveTagPropertyData(
@ -244,6 +247,7 @@ export default defineComponent({
} }
imageLoading.value = true imageLoading.value = true
await window.sd.txt2img(JSON.stringify([d]), async (value) => { await window.sd.txt2img(JSON.stringify([d]), async (value) => {
debugger
if (value.code == 0) { if (value.code == 0) {
message.error(value.message) message.error(value.message)
imageLoading.value = false imageLoading.value = false
@ -251,7 +255,8 @@ export default defineComponent({
} }
if (value.data && value.data.length > 0) { if (value.data && value.data.length > 0) {
let d = value.data[0] let d = value.data[0]
styleData.value.show_image = d.image_path.replaceAll('\\', '/') styleData.value.show_image =
d.image_path.replaceAll('\\', '/') + '?t=' + new Date().getTime()
png_base64.value = 'data:image/png;base64,' + d.base64 png_base64.value = 'data:image/png;base64,' + d.base64
console.log(styleData.value.show_image) console.log(styleData.value.show_image)
imageLoading.value = false imageLoading.value = false
@ -261,8 +266,52 @@ export default defineComponent({
}) })
} }
/**
* 选择本地的图片应用到风格图片
*/
async function SelecImage() {
await window.api.SelectFile(
[
'jpeg',
'jpg',
'png',
'gif',
'bmp',
'tiff',
'tif',
'webp',
'svg',
'raw',
'cr2',
'nef',
'arw'
],
(value) => {
if (value.code == 0) {
message.error(value.message)
return
}
// Base64
fetch(value.value)
.then((response) => response.blob())
.then((blob) => {
const reader = new FileReader()
reader.onloadend = () => {
png_base64.value = reader.result
}
reader.readAsDataURL(blob)
})
//
png_base64.value = null
styleData.value.show_image = value.value + '?t=' + new Date().getTime()
}
)
}
return { return {
styleData, styleData,
SelecImage,
SaveStyleTag, SaveStyleTag,
loading, loading,
imageLoading, imageLoading,

View File

@ -83,6 +83,7 @@
fit="cover" fit="cover"
width="120" width="120"
height="120" height="120"
lazy
/> />
<n-button <n-button
text text

View File

@ -760,7 +760,7 @@ export default defineComponent({
} }
// //
tempData = tempData.filter((item) => item.imageLock == false) tempData = tempData.filter((item) => item.imageLock == null || item.imageLock == false)
if (tempData.length <= 0) { if (tempData.length <= 0) {
message.error('下面的数据都被锁定,无法生图,请检查') message.error('下面的数据都被锁定,无法生图,请检查')

View File

@ -220,10 +220,10 @@ export default defineComponent({
* 生成所有的图片 * 生成所有的图片
*/ */
async function GenerateImageAll() { async function GenerateImageAll() {
debugger
let tmpData = cloneDeep(toRaw(data.value)) let tmpData = cloneDeep(toRaw(data.value))
tmpData = tmpData.filter((item) => { tmpData = tmpData.filter((item) => {
// return item.outImagePath == null || item.outImagePath == '' return item.imageLock == null || item.imageLock == false
return !item.imageLoak
}) })
if (tmpData.length == 0) { if (tmpData.length == 0) {
@ -280,12 +280,24 @@ export default defineComponent({
} }
} }
// GPT
async function ResetGPTPrompt() {
for (let i = 0; i < data.value.length; i++) {
data.value[i].gpt_prompt = ''
}
}
// //
async function ResetImage() { async function ResetImage() {
// data // data
for (let i = 0; i < data.value.length; i++) { for (let i = 0; i < data.value.length; i++) {
data.value[i].outImagePath = null data.value[i].outImagePath = null
data.value[i].subImagePath = [] data.value[i].subImagePath = []
data.value[i].imageLock = false
// mj_message
if (data.value[i].mj_message) {
data.value[i].mj_message = undefined
}
} }
} }
@ -333,6 +345,9 @@ export default defineComponent({
await ResetPrompt() await ResetPrompt()
} else if (value == 'resetImage') { } else if (value == 'resetImage') {
await ResetImage() await ResetImage()
} else if (value == 'resetGPTPrompt') {
// GPT
await ResetGPTPrompt()
} }
} }
@ -340,6 +355,7 @@ export default defineComponent({
async function resetALLData() { async function resetALLData() {
await ResetPrompt() await ResetPrompt()
await ResetImage() await ResetImage()
await ResetGPTPrompt()
} }
// //
@ -408,6 +424,7 @@ export default defineComponent({
GptButtonOptions: [{ label: '停止推理任务', key: 'stopGptPrompt' }], GptButtonOptions: [{ label: '停止推理任务', key: 'stopGptPrompt' }],
GenerateImageOptions: [{ label: '停止生成图片任务', key: 'stopGenerateImage' }], GenerateImageOptions: [{ label: '停止生成图片任务', key: 'stopGenerateImage' }],
ResetDataOptions: [ ResetDataOptions: [
{ label: '重置GPT提示词', key: 'resetGPTPrompt' },
{ label: '重置提示词', key: 'resetPrompt' }, { label: '重置提示词', key: 'resetPrompt' },
{ label: '重置图片', key: 'resetImage' } { label: '重置图片', key: 'resetImage' }
], ],

View File

@ -0,0 +1,531 @@
<template>
<div style="margin-top: 30px">
<div
style="
margin-top: 10px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
"
id="cnvas_ss"
>
<canvas ref="imageCanvas" />
<canvas
ref="tempImageCanvas"
@mousemove="draw"
@mouseup="stopDrawing"
@mousedown="startDrawing"
style="position: absolute; top: 1; left: 0"
/>
</div>
<div style="margin-top: 10px; display: flex; justify-content: center; align-items: center">
<n-button style="margin-left: 10px" @click="undo" type="success">回撤</n-button>
<n-button style="margin-left: 10px" @click="Check" type="success">查看去水印效果</n-button>
<n-button style="margin-left: 10px" @click="SaveMask" type="success">保存蒙板位置</n-button>
</div>
</div>
</template>
<script>
import { defineComponent, ref, onMounted, h, watch, toRaw } from 'vue'
import {
NButton,
useMessage,
NInput,
NTag,
NImage,
NCheckbox,
NSlider,
NPopover,
NProgress,
useDialog
} from 'naive-ui'
import ImageFileSelect from './ImageFileSelect.vue'
import ProgressDialog from '../Components/ProgressDialog.vue'
import { DEFINE_STRING } from '../../../../define/define_string'
import { cloneDeep, isEmpty } from 'lodash'
import { useReverseManageStore } from '../../../../stores/reverseManage'
export default defineComponent({
components: {
NButton,
NInput,
NTag,
ImageFileSelect,
NImage,
NCheckbox,
NSlider,
NPopover,
NProgress
},
props: ['width', 'height'],
setup(props) {
let message = useMessage()
let dialog = useDialog()
const imageCanvas = ref(null)
let tempImageCanvas = ref(null)
let imageContext = ref(null)
let reverseManageStore = useReverseManageStore()
let height = ref(props.height)
let width = ref(props.width)
let rectPos = ref(null)
let drawing = ref(false)
let moving = ref(false)
let startPos = ref(null)
let offset = ref({ x: 0, y: 0 })
let isDrawing = false
let radius = ref(20) //
let canvasHistory = [] //
let step = 0
let mask_setting = ref({
isRemote: true,
localUrl: null,
mask_path: null
})
let progressDialogRef = ref(null)
let total = ref(0)
let current = ref(0)
let dp
let image = ref(null)
let initImage = null
onMounted(async () => {
if (imageCanvas.value) {
const ctx = imageCanvas.value.getContext('2d', { willReadFrequently: true })
if (ctx) {
imageContext.value = ctx
}
}
let div = document.getElementById('cnvas_ss')
div.style.height = height.value - 120 + 'px'
await window.api.GetDefineConfigJsonByProperty(
JSON.stringify(['img_base', 'mask_setting', false, null]),
(value) => {
if (value.code == 0) {
message.error(value.message)
return
}
//
if (value.data) {
mask_setting.value = Object.assign(mask_setting.value, value.data)
}
}
)
window.api.setEventListen([DEFINE_STRING.IMG.BATCH_PROCESS_IMAGE_RESULT], (value) => {
if (value.code == 0) {
message.error(value.message)
return
}
total.value = value.data.total
current.value = value.data.current
if (progressDialogRef.value == null) {
return
}
progressDialogRef.value?.modifyTotal(total.value)
progressDialogRef.value?.modifyCurrent(current.value)
if (value.data.total == value.data.current) {
//
setTimeout(() => {
if (dp) {
dp.destroy()
}
}, 1000)
}
})
//
await loadFile(reverseManageStore.selectBookTaskDetail[0].oldImage)
})
let hasImage = false
/**
* 获取图片的高度并且计算适应的宽高
* @param image
*/
function getCurrentWidthHeight(image) {
let image_width = 512
let image_height = 512
//
if (image) {
image_width = image.width
image_height = image.height
}
// window
// let min_size = Math.min(width.value,height.value);
let image_height_scale = (height.value - 120) / image_height
let image_width_scale = (width.value - 120) / image_width
let scale = Math.min(image_height_scale, image_width_scale)
return [image_width * scale, image_height * scale]
}
function getImage(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = reject
img.src = reader.result
}
reader.onerror = (error) => reject(error)
reader.readAsDataURL(file)
})
}
async function loadFile(fileUrl) {
try {
const response = await fetch(fileUrl)
const blob = await response.blob()
const file = new File([blob], 'file.jpg', { type: blob.type })
const img = await getImage(file)
image.value = img
modifyImage(img)
} catch (error) {
console.error('Error loading file:', error)
}
}
/**
* 选择图片初始化
* @param image_d
*/
function modifyImage(image_d) {
let [image_width, image_height] = getCurrentWidthHeight(image_d)
imageContext.value.canvas.width = image_width
imageContext.value.canvas.height = image_height
imageContext.value.drawImage(image_d, 0, 0, image_width, image_height)
// canvasHistory.push(imageCanvas.value.toDataURL('image/png'))
hasImage = true
let w = (width.value - image_width - 56) / 2
tempImageCanvas.value.style.left = w + 'px'
tempImageCanvas.value.width = imageCanvas.value.width
tempImageCanvas.value.height = imageCanvas.value.height
tempImageCanvas.value.style.zIndex = 100
if (!initImage) {
initImage = imageCanvas.value.toDataURL('image/png')
}
}
/**
* canvas绘制矩形
* @param rect
*/
const drawRectangle = (canvas, rect, clear = true) => {
const ctx = canvas.value.getContext('2d')
if (clear) {
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height) //
}
ctx.strokeStyle = 'red' //
ctx.lineWidth = 2 // 线
ctx.strokeRect(rect.startX, rect.startY, rect.width, rect.height)
}
const clearCanvas = (canvas) => {
const c = canvas.value
const context = c.getContext('2d')
context.clearRect(0, 0, c.width, c.height)
}
/**
* 获取当前鼠标在canvas中的相对位置
* @param e
*/
function getCanvasRelativePosition(e) {
const rect = tempImageCanvas.value.getBoundingClientRect()
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
/**
* 停止画
*/
function stopDrawing(e) {
//
if (drawing.value) {
const pos = getCanvasRelativePosition(e)
rectPos.value = {
startX: Math.min(startPos.value.x, pos.x),
startY: Math.min(startPos.value.y, pos.y),
width: Math.abs(startPos.value.x - pos.x),
height: Math.abs(startPos.value.y - pos.y),
imageWidth: tempImageCanvas.value.width,
imageHeight: tempImageCanvas.value.height
}
drawRectangle(tempImageCanvas, rectPos.value)
// imageCanvas
drawRectangle(imageCanvas, rectPos.value, false)
isDrawing = false
// canvasHistory.push(imageCanvas.value.toDataURL('image/png'))
canvasHistory.push(rectPos.value)
console.log(canvasHistory)
step++
startPos.value = null
drawing.value = false
}
if (moving.value) {
moving.value = false
// imageCanvas
drawRectangle(imageCanvas, rectPos.value, false)
// canvasHistory.push(imageCanvas.value.toDataURL('image/png'))
canvasHistory.push(rectPos.value)
}
clearCanvas(tempImageCanvas)
}
/**
* 填充颜色
* @param e
*/
function draw(e) {
if (drawing.value && startPos.value) {
const currentPos = getCanvasRelativePosition(e)
const rect = {
startX: Math.min(startPos.value.x, currentPos.x),
startY: Math.min(startPos.value.y, currentPos.y),
width: Math.abs(startPos.value.x - currentPos.x),
height: Math.abs(startPos.value.y - currentPos.y)
}
drawRectangle(tempImageCanvas, rect)
} else if (moving.value && rectPos.value) {
const currentPos = getCanvasRelativePosition(e)
const newRectPos = {
startX: currentPos.x - offset.value.x,
startY: currentPos.y - offset.value.y,
width: rectPos.value.width,
height: rectPos.value.height,
imageWidth: tempImageCanvas.value.width,
imageHeight: tempImageCanvas.value.height
}
rectPos.value = newRectPos
drawRectangle(tempImageCanvas, newRectPos)
}
}
/**
* 开始话
* @param event
*/
function startDrawing(e) {
//
const pos = getCanvasRelativePosition(e)
if (
rectPos.value &&
pos.x >= rectPos.value.startX &&
pos.x <= rectPos.value.startX + rectPos.value.width &&
pos.y >= rectPos.value.startY &&
pos.y <= rectPos.value.startY + rectPos.value.height
) {
moving.value = true
offset.value = { x: pos.x - rectPos.value.startX, y: pos.y - rectPos.value.startY }
} else {
startPos.value = pos
drawing.value = true
}
}
/**
* 回撤
*/
async function undo() {
debugger
if (step <= 0) {
//
imageContext.value.clearRect(
0,
0,
imageContext.value.canvas.width,
imageContext.value.canvas.height
)
canvasHistory = []
} else {
step--
clearCanvas(imageCanvas)
modifyImage(image.value)
canvasHistory.pop()
//
for (let i = 0; i < canvasHistory.length; i++) {
const element = canvasHistory[i]
drawRectangle(imageCanvas, element, false)
}
}
}
/**
* 获取当前的蒙板
*/
function getCanvasMaskBase64() {
//
const imageData = imageContext.value.getImageData(
0,
0,
imageContext.value.canvas.width,
imageContext.value.canvas.height
)
const newImageData = imageContext.value.createImageData(imageData.width, imageData.height)
for (let i = 0; i < imageData.data.length; i += 4) {
if (
imageData.data[i] === 255 &&
imageData.data[i + 1] === 255 &&
imageData.data[i + 2] === 0
) {
// rgba(255, 255, 0, 255)
newImageData.data[i] = newImageData.data[i + 1] = newImageData.data[i + 2] = 255
newImageData.data[i + 3] = 255
} else {
//
newImageData.data[i] = newImageData.data[i + 1] = newImageData.data[i + 2] = 0
newImageData.data[i + 3] = 255
}
}
// // Canvas
const newCanvas = document.createElement('canvas')
newCanvas.width = imageData.width
newCanvas.height = imageData.height
const newContext = newCanvas.getContext('2d')
// ImageData Canvas
newContext.putImageData(newImageData, 0, 0)
// Canvas base64 URL
const base64 = newCanvas.toDataURL('image/png')
return base64
}
/**
* 查看效果
*/
async function Check() {
debugger
//
if (canvasHistory.length == 0) {
message.error('请先选择去水印的区域')
return
}
let res = await window.img.ProcessImageCheck(
initImage,
cloneDeep(canvasHistory),
reverseManageStore.selectBook.id
)
const arrayBuffer = res.data
const blob = new Blob([arrayBuffer], { type: 'image/png' })
const reader = new FileReader()
reader.onloadend = function () {
let img = new Image()
img.src = reader.result
img.onload = function () {
imageContext.value.drawImage(img, 0, 0)
canvasHistory.push({
startX: 0,
startY: 0,
width: 0,
height: 0,
imageWidth: tempImageCanvas.value.width,
imageHeight: tempImageCanvas.value.height
})
step++
}
}
reader.readAsDataURL(blob)
}
async function SaveMask() {
debugger
//
if (canvasHistory.length <= 1) {
message.error('请先选择去水印的区域')
return
}
let maskPos = canvasHistory.filter((item) => item.width > 0 && item.height > 0)
if (maskPos.length == 0) {
message.error('请先选择去水印的区域,或者选择的水印区域无效')
return
}
//
let res = await window.db.UpdateBookData(reverseManageStore.selectBook.id, {
watermarkPosition: JSON.stringify(maskPos)
})
if (res.code == 0) {
message.error(res.message)
return
}
message.success('水印位置保存成功')
}
/**
*查看蒙板
*/
async function ViewMaskImage() {
window.system.OpenFile(mask_setting.value.mask_path)
}
function OpenTeach(type) {
switch (type) {
case 'lama':
window.api.OpenUrl(
'https://rvgyir5wk1c.feishu.cn/docx/RZYCdG7ZpoKsIzxBEzccNEIFn8f#QOIRdJQvEouJB7xv0Xqcx3zInyb'
)
break
case 'iopaint':
window.api.OpenUrl(
'https://rvgyir5wk1c.feishu.cn/docx/RZYCdG7ZpoKsIzxBEzccNEIFn8f#MLoUdn5cfo6NJTxL0xTcUGyLn43'
)
default:
break
}
}
return {
modifyImage,
imageCanvas,
tempImageCanvas,
height,
width,
stopDrawing,
startDrawing,
startPos,
draw,
undo,
Check,
offset,
SaveMask,
mask_setting,
ViewMaskImage,
radius,
progressDialogRef,
total,
current,
OpenTeach,
reverseManageStore,
image
}
}
})
</script>
<style>
.url_class {
color: #e18a3b;
}
.url_class:hover {
color: brown;
cursor: pointer;
}
</style>

View File

@ -43,6 +43,7 @@ export default defineComponent({
} }
async function selectImage(value) { async function selectImage(value) {
debugger
let img = await getBase64(value.file.file) let img = await getBase64(value.file.file)
props.modifyImage(img) props.modifyImage(img)
} }

View File

@ -6,6 +6,9 @@ const app = createApp(App)
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
const pinia = createPinia() const pinia = createPinia()
// 注册 webview 为自定义元素
app.config.compilerOptions.isCustomElement = (tag) => tag === 'webview'
const routes = [ const routes = [
{ {
path: '/', path: '/',
@ -101,6 +104,11 @@ const routes = [
name: 'test_options', name: 'test_options',
component: () => import('./components/VideoSubtitle/VideoCanvas.vue') component: () => import('./components/VideoSubtitle/VideoCanvas.vue')
}, },
{
path: '/lai_api',
name: 'lai_api',
component: () => import('./components/APIService/ApiService.vue')
},
{ {
path: '/TTS_Services', path: '/TTS_Services',
name: 'TTS_Services', name: 'TTS_Services',

13
vue.config.js Normal file
View File

@ -0,0 +1,13 @@
export default {
chainWebpack: (config) => {
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
options.compilerOptions = {
isCustomElement: (tag) => tag === 'webview'
}
return options
})
}
}