This commit is contained in:
2025-08-19 14:33:59 +08:00
commit 56a32f8a50
336 changed files with 55317 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
import { escapeRegExp, isEmpty } from 'lodash'
//#region 检查字符串中是不是包含中文或者标点符号
/**
* 检查字符串中是不是包含中文或者标点符号
* @param str 需要判断的字符串
* @returns 返回的数据,有中文或者标点符号返回true,否则返回false
*/
export function ContainsChineseOrPunctuation(str: string): boolean {
return /[\u4e00-\u9fa5]|[\u3000-\u301e\u2013\u2014\u2018\u2019\u201c\u201d\u2026\u203b\uff08\uff09\uff1a\uff1b\uff1f\uff01\uff0c\u3001\uff0e\u3002\uff1f\uff01\u2018\u2019\u201c\u201d]/.test(
str
)
}
//#endregion
//#region 通用的失败重试函数
/**
* 通用的重试函数
* @param fn 要执行的函数
* @param retries 最大重试次数
* @param delay 每次重试之间的延迟(毫秒)
* @returns 返回函数的结果
*/
export async function RetryWithBackoff<T>(
fn: () => Promise<T>,
retries: number = 5,
delay: number = 2000
): Promise<T> {
let attempts = 0
while (attempts < retries) {
try {
return await fn()
} catch (error: any) {
attempts++
// 这边记下日志吧
global.logger.error(
fn.name + '_RetryWithBackoff',
`${attempts} 请求失败,开始下一次重试,失败信息如下:` + error.toString()
)
if (attempts >= retries) {
throw new Error(`失败次数超过 ${retries} 错误信息如下: ${error.message}`)
}
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
throw new Error('所有重试失败') // 理论上不会到达这里
}
//#endregion
//#region 并发执行任务(控制同时执行的任务数)
/**
* 并发执行任务(控制同时执行的任务数)
* @param tasks 总的任务列表
* @param concurrentCount 同时执行的数量
* @returns
*/
export async function ExecuteConcurrently(
tasks: Array<() => Promise<any>>,
concurrentCount: number
): Promise<any[]> {
let activeTasks: Array<Promise<any>> = []
let results: Array<Promise<any>> = []
while (tasks.length > 0) {
if (activeTasks.length < concurrentCount) {
let task: any = tasks.shift()
let promise = task()
.then((result) => {
activeTasks = activeTasks.filter((t) => t !== promise)
return result
})
.catch((error) => {
// 抛出任务,停止所有的任务
tasks.length = 0
throw error
})
activeTasks.push(promise)
results.push(promise)
} else {
await Promise.race(activeTasks)
}
}
return Promise.all(results)
}
//#endregion
//#region 替换主字符串中的子字符串
/**
* 替换主字符串中的子字符串
* @param mainString 主字符串
* @param substringArray 子字符串数组
* @returns 替换后的字符串
*/
export function ReplaceSubstrings(
mainString: string,
substringArray: string[],
replacement: string = ' '
): string {
// 按长度降序排序子字符串数组,以确保较长的子字符串先被替换
substringArray.sort((a, b) => b.length - a.length)
// 对每个子字符串进行替换
for (const substring of substringArray) {
// 创建一个正则表达式,用于全局匹配子字符串
const regex = new RegExp(escapeRegExp(substring), 'g')
// 将匹配到的子字符串替换为等长的空格
mainString = mainString.replace(regex, replacement)
}
return mainString
}
//#endregion
//#region 获取url的基础地址
/**
* 获取url的基础地址
* @param url 一个url地址
* @returns
*/
export function GetBaseUrl(url: string): string {
if (isEmpty(url)) {
throw new Error('url不能为空')
}
// 判断是不是一个合法的url
if (!url.startsWith('http')) {
throw new Error('一个合法的url请求地址')
}
const parsedUrl = new URL(url)
return `${parsedUrl.protocol}//${parsedUrl.host}`
}
//#endregion
//#region 通用的下载完网络图片
/**
* 通用的下载完网络图片
* @param url 网络地址
* @param localPath 本地路径
* @returns
*/
export async function DownloadFile(url: string, localPath?: string): Promise<void> {
if (typeof window !== 'undefined' && window.document) {
// 浏览器环境
const response = await fetch(url)
if (!response.body) {
throw new Error('浏览器不支持流式下载')
}
const reader = response.body.getReader()
// const contentLength = +response.headers.get('Content-Length')!
let receivedLength = 0
const chunks: any[] = []
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
receivedLength += value.length
// 可以在这里更新进度,比如:
// console.log(`已接收 ${receivedLength} / ${contentLength}`);
}
const blob = new Blob(chunks)
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = localPath || 'downloaded_file'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(link.href)
} else {
// Node.js 环境
const fs = require('fs')
const { pipeline } = require('stream')
const { promisify } = require('util')
const path = require('path')
const https = require('https')
const http = require('http')
const urlObj = new URL(url)
const filePath = localPath || path.basename(url)
const protocol = urlObj.protocol === 'https:' ? https : http
const streamPipeline = promisify(pipeline)
const response = await new Promise((resolve, reject) => {
protocol
.get(url, (res) => {
if (res.statusCode === 200) {
resolve(res)
} else {
reject(new Error(`请求失败,状态码:${res.statusCode}`))
}
})
.on('error', reject)
})
await streamPipeline(response, fs.createWriteStream(filePath))
}
}
//#endregion
//#region 通用文件操作,可以在node和浏览器环境中使用
/**
* 获取文件的basename,在所有的环境中都试用的
* @param filePath
* @returns
*/
export function GetFileBasename(filePath: string): string {
if (isEmpty(filePath)) {
throw new Error('filePath is empty, must be a valid file path')
}
const baseName = filePath.split('/').pop()?.split('\\').pop() ?? ''
if (isEmpty(baseName)) {
throw new Error('filePath is empty, must be a valid file path')
}
return baseName
}
//#endregion
+353
View File
@@ -0,0 +1,353 @@
import fs from 'fs'
import { isEmpty } from 'lodash'
import path from 'path'
import util from 'util'
import { exec } from 'child_process'
const execAsync = util.promisify(exec)
const fspromises = fs.promises
/**
* 判断文件或目录是否存在
* @param {*} path 文件或目录的路径
* @returns true表示存在,false表示不存在
*/
export async function CheckFileOrDirExist(filePath) {
try {
let newFilePath = path.resolve(filePath)
await fspromises.access(newFilePath)
return true // 文件或目录存在
} catch (error) {
return false // 文件或目录不存在
}
}
// 检查文件夹是不是存在,不存在的话,创建
export async function CheckFolderExistsOrCreate(folderPath) {
try {
if (!(await CheckFileOrDirExist(folderPath))) {
await fspromises.mkdir(folderPath, { recursive: true })
}
} catch (error) {
throw error
}
}
/**
* 拼接两个地址,返回拼接后的地址
* @param {*} rootPath 根目录的消息
* @param {*} subPath 子目录的消息
* @returns
*/
export function JoinPath(rootPath: string, subPath: string | null): string | undefined {
// 判断第二个地址是不是存在,不存在返回null,存在返回拼接后的地址
if (subPath && !isEmpty(subPath)) {
return path.resolve(rootPath, subPath)
} else {
return undefined
}
}
/**
* 删除指定的文件中里面所有的文件和文件夹
* @param {*} folderPath 文件夹地址
* @param {*} isDeleteOut 是否删除最外层的文件夹,默认false,不删除
*/
export async function DeleteFolderAllFile(
folderPath: string,
isDeleteOut: boolean = false
): Promise<void> {
try {
let folderIsExist = await CheckFileOrDirExist(folderPath)
if (!folderIsExist) {
throw new Error('目的文件夹不存在,' + folderPath)
}
// 开始删除
let files = await fspromises.readdir(folderPath)
for (const file of files) {
const curPath = path.join(folderPath, file)
const stat = await fspromises.stat(curPath)
if (stat.isDirectory()) {
// 判断是不是文件夹
await DeleteFolderAllFile(curPath) // 递归删除文件夹内容
await fspromises.rmdir(curPath) // 删除空文件夹
} else {
// 删除文件
await fspromises.unlink(curPath)
}
}
// 判断是不是要删除最外部的文件夹
if (isDeleteOut) {
await fspromises.rmdir(folderPath)
}
} catch (error) {
throw error
}
}
/**
* 拷贝一个文件或者是文件夹到指定的目标地址
* @param {*} source 源文件或文件夹地址
* @param {*} target 目标文件或文件夹地址
* @param {*} checkParent 是否检查父文件夹是否存在,不存在的话创建,默认false,不检查,不存在直接创建
*/
export async function CopyFileOrFolder(source, target, checkParent = false) {
try {
// 判断源文件或文件夹是不是存在
if (!(await CheckFileOrDirExist(source))) {
throw new Error(`源文件或文件夹不存在: ${source}`)
}
// 判断父文件夹是否存在,不存在创建
const parent_path = path.dirname(target)
let parentIsExist = await CheckFileOrDirExist(parent_path)
if (!parentIsExist) {
if (checkParent) {
throw new Error(`目的文件或文件夹的父文件夹不存在: ${parent_path}`)
} else {
await fspromises.mkdir(parent_path, { recursive: true })
}
}
// 判断是不是文件夹
const isDirectory = await IsDirectory(source)
// 复制文件夹的逻辑
async function copyDirectory(source, target) {
// 创建目标文件夹
await fspromises.mkdir(target, { recursive: true })
let entries = await fspromises.readdir(source, { withFileTypes: true })
for (let entry of entries) {
let srcPath = path.join(source, entry.name)
let tgtPath = path.join(target, entry.name)
if (entry.isDirectory()) {
await copyDirectory(srcPath, tgtPath)
} else {
await fspromises.copyFile(srcPath, tgtPath)
}
}
}
if (isDirectory) {
// 创建目标文件夹
await copyDirectory(source, target)
} else {
// 复制文件
await fspromises.copyFile(source, target)
}
} catch (error) {
throw error
}
}
/** * 判断一个文件地址是不是文件夹
* @param {*} path 输入的文件地址
* @returns true 是 false 不是
*/
export async function IsDirectory(path) {
try {
const stat = await fspromises.stat(path)
return stat.isDirectory()
} catch (error) {
throw new Error(`获取文件夹信息失败: ${path}`)
}
}
/**
* 将文件或者是文件夹备份到指定的文职
* @param {*} source_path 源文件/文件夹地址
* @param {*} target_path 目标文件/文件夹地址
*/
export async function BackupFileOrFolder(source_path: string, target_path: string): Promise<void> {
try {
// 判断父文件夹是否存在,不存在创建
const parent_path = path.dirname(target_path)
if (!(await CheckFileOrDirExist(parent_path))) {
await fspromises.mkdir(parent_path, { recursive: true })
}
// 判断是不是文件夹
const isDirectory = await IsDirectory(source_path)
if (isDirectory) {
// 复制文件夹
await fspromises.rename(source_path, target_path)
} else {
// 复制文件
await fspromises.copyFile(source_path, target_path)
}
} catch (error) {
throw error
}
}
/**
* 获取指定的文件夹下面的所有的指定的拓展名的文件
* @param {*} folderPath 文件夹地址
* @param {*} extensions 拓展地址
* @returns 返回文件中指定的后缀文件地址(绝对地址)
*/
export async function GetFilesWithExtensions(
folderPath: string,
extensions: string[]
): Promise<string[]> {
try {
// 判断当前是不是文件夹
if (!(await IsDirectory(folderPath))) {
throw new Error('输入的不是有效的文件夹地址')
}
let entries = await fspromises.readdir(folderPath, { withFileTypes: true })
let files = [] as any
// 使用Promise.all来并行处理所有的stat调用
const fileStats = await Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(folderPath, entry.name)
if (entry.isFile()) {
return {
name: entry.name,
path: entryPath,
isFile: true
}
} else {
return {
isFile: false
}
}
})
)
// 过滤出文件并且满足扩展名要求的文件
files = fileStats.filter(
(fileStat) =>
fileStat.isFile && extensions.includes(path.extname(fileStat.name ?? '').toLowerCase())
)
// 对files数组进行排序,基于文件名
files.sort((a: any, b: any) => a.name.localeCompare(b.name))
// 返回文件名数组(完整的)
return files.map((fileStat) => path.join(folderPath, fileStat.name))
} catch (error) {
throw error
}
}
/**
* 获取文件的大小
* @param filePath 文件的地址
* @returns 返回的文件大小为 kb单位
*/
export async function GetFileSize(filePath: string): Promise<number> {
try {
if (!(await CheckFileOrDirExist(filePath))) {
throw new Error('获取文件大小,指定的文件不存在')
}
const stats = await fspromises.stat(filePath)
return stats.size / 1024
} catch (error) {
throw error
}
}
/**
* 获取文件夹下的所有子文件夹信息,按创建时间排序
* @param folderPath 文件夹的路径
* @returns 返回包含子文件夹名称和完整路径的对象数组,按创建时间排序(最新的在前)
*/
export async function GetSubdirectoriesWithInfo(folderPath: string): Promise<Array<{name: string, fullPath: string, ctime: Date}>> {
try {
const filesAndDirectories = await fs.promises.readdir(folderPath, { withFileTypes: true })
// 过滤出文件夹
const directories = filesAndDirectories.filter((dirent) => dirent.isDirectory())
// 并行获取所有文件夹的状态信息
const directoryStatsPromises = directories.map((dirent) =>
fs.promises.stat(path.join(folderPath, dirent.name))
)
const directoryStats = await Promise.all(directoryStatsPromises)
// 将目录信息和状态对象组合
const directoriesWithInfo = directories.map((dirent, index) => ({
name: dirent.name,
fullPath: path.join(folderPath, dirent.name),
ctime: directoryStats[index].ctime
}))
// 按创建时间排序,最新的在前
directoriesWithInfo.sort((a, b) => b.ctime.getTime() - a.ctime.getTime())
return directoriesWithInfo
} catch (error) {
throw error
}
}
/**
* 删除目标图片,然后将原图片的exif信息删除,然后将原图片复制到目标图片地址
* @param {*} exiftoolPath exiftool的地址
* @param {*} source 原图片地址
* @param {*} target 目标图片地址
*/
export async function DeleteFileExifData(exiftoolPath: string, source: string, target: string) {
try {
if (await CheckFileOrDirExist(target)) {
await fspromises.unlink(target)
}
let script = `"${exiftoolPath}" -all= -overwrite_original "${source}" -o "${target}"`
await execAsync(script, { maxBuffer: 1024 * 1024 * 10, encoding: 'utf-8' })
} catch (error) {
throw error
}
}
/**
* 下载网络图片到本地
*
* 该方法从指定的URL下载图片文件,并将其保存到本地指定路径。
* 如果目标文件夹不存在,会自动创建。如果指定路径已存在文件,则会覆盖。
*
* @param {string} imageUrl - 图片的网络URL地址
* @param {string} localPath - 保存到本地的完整路径,包含文件名和扩展名
* @returns {Promise<string>} 成功时返回保存的本地文件路径
* @throws {Error} 当网络请求失败、写入失败或其他错误时抛出异常
*
* @example
* // 下载图片到指定路径
* try {
* const savedPath = await DownloadImageFromUrl(
* 'https://example.com/image.jpg',
* 'd:/images/downloaded.jpg'
* );
* console.log('图片已保存至:', savedPath);
* } catch (error) {
* console.error('下载图片失败:', error.message);
* }
*/
export async function DownloadImageFromUrl(imageUrl: string, localPath: string): Promise<string> {
try {
// 确保目标文件夹存在
const dirPath = path.dirname(localPath)
await CheckFolderExistsOrCreate(dirPath)
// 使用fetch获取图片数据
const response = await fetch(imageUrl)
if (!response.ok) {
throw new Error(`下载失败,HTTP状态码: ${response.status}`)
}
// 获取图片的二进制数据
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
// 将图片数据写入本地文件
await fspromises.writeFile(localPath, buffer)
return localPath
} catch (error) {
if (error instanceof Error) {
throw new Error(`下载图片失败: ${error.message}`)
} else {
throw new Error('下载图片时发生未知错误')
}
}
}
+388
View File
@@ -0,0 +1,388 @@
import path from 'path'
import sharp from 'sharp'
import { CheckFileOrDirExist, CheckFolderExistsOrCreate } from './file'
import fs from 'fs'
import https from 'https'
/**
* 将指定的图片的尺寸修改,返回修改后的图片数据(base64或buffer)
* @param {*} image_path
* @param {*} width
* @param {*} height
* @param {*} type
* @returns 返回修改后的图片数据(base64或buffer)
*/
export async function ResizeImage(
image_path: string,
width: number | sharp.ResizeOptions,
height: number,
type: string
) {
try {
// 检查 type 参数
if (type !== 'base64' && type !== 'buffer') {
throw new Error('type 参数必须是 "base64" 或 "buffer"')
}
// 判断是不是图片文件
if (!image_path.match(/\.(jpg|jpeg|png)$/)) {
throw new Error('输入的文件地址不是图片文件地址')
}
// 判断文件是否存在
if (!(await CheckFileOrDirExist(image_path))) {
throw new Error('文件不存在')
}
// 修改图片尺寸
const image = sharp(image_path)
image.resize(width, height)
let data = await image.toBuffer()
if (type === 'base64') {
return data.toString('base64')
} else {
return data
}
} catch (error) {
throw error
}
}
/**
* 获取指定图片文件的宽高
* @param {*} image_path 图片文件的路径
* @returns 返回以一个对象,包含width和height属性
*/
export async function GetImageSize(image_path: string) {
try {
// 判断文件是否存在
if (!(await CheckFileOrDirExist(image_path))) {
throw new Error('文件不存在')
}
// 判断是不是图片文件
if (!image_path.match(/\.(jpg|jpeg|png)$/)) {
throw new Error('输入的文件地址不是图片文件地址')
}
// 获取图片的宽高
const metadata = await sharp(image_path).metadata()
return {
width: metadata.width,
height: metadata.height
}
} catch (error) {
throw error
}
}
/**
* 根据文件扩展名获取MIME类型
* @param filePath 文件路径
* @returns MIME类型字符串
*/
export function GetMimeType(filePath: string): string {
const extension = path.extname(filePath).toLowerCase()
const mimeTypes: { [key: string]: string } = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp'
// 添加更多文件类型和对应的MIME类型
}
return mimeTypes[extension] || 'application/octet-stream'
}
/**
* 从base64字符串中获取图片类型和对应文件后缀
* @param base64String - base64编码的图片数据
* @returns 文件后缀(包括点号)
*/
export function GetImageTypeFromBase64(base64String: string): string {
// 默认后缀
let extension = '.png'
// 检查是否是base64格式的数据URI
if (typeof base64String !== 'string' || !base64String.startsWith('data:image/')) {
return extension // 不是有效的图片base64,返回默认后缀
}
try {
// 提取MIME类型
const matches = base64String.match(/^data:image\/([a-zA-Z0-9+.-]+);base64,/)
if (matches && matches.length > 1) {
const mimeSubtype = matches[1].toLowerCase()
// 映射MIME子类型到文件后缀
switch (mimeSubtype) {
case 'jpeg':
case 'jpg':
extension = '.jpg'
break
case 'png':
extension = '.png'
break
case 'gif':
extension = '.gif'
break
case 'webp':
extension = '.webp'
break
case 'svg+xml':
extension = '.svg'
break
case 'bmp':
extension = '.bmp'
break
case 'tiff':
extension = '.tiff'
break
default:
// 其他不常见格式,使用MIME子类型作为后缀
extension = `.${mimeSubtype}`
}
}
} catch (error) {
console.error('解析base64图片类型时出错:', error)
}
return extension
}
/**
* 将本地文件地址或网络图片地址转换为包含MIME类型的base64字符串
* @param url 本地文件路径或网络图片URL
* @returns Promise<string> 返回一个Promise,解析为包含MIME类型的base64字符串
*/
export function GetImageBase64(url: string): Promise<string> {
if (!url) {
return Promise.reject('URL不能为空')
}
if (url.startsWith('http://') || url.startsWith('https://')) {
return new Promise((resolve, reject) => {
https
.get(url, (response) => {
const mimeType = response.headers['content-type'] || 'application/octet-stream'
const data: any[] = []
response.on('data', (chunk) => data.push(chunk))
response.on('end', () => {
const buffer = Buffer.concat(data)
const base64Data = `data:${mimeType};base64,${buffer.toString('base64')}`
resolve(base64Data)
})
})
.on('error', (err) => reject(err))
})
} else {
return new Promise((resolve, reject) => {
fs.readFile(url, (err, data) => {
if (err) {
reject(err)
} else {
const mimeType = GetMimeType(url)
const base64Data = `data:${mimeType};base64,${data.toString('base64')}`
resolve(base64Data)
}
})
})
}
}
/**
* 压缩图片到指定的大小
* @param base64 图片的Blob对象
* @param maxSizeInBytes 最大文件大小,单位字节
* @returns 返回一个Promise,解析为压缩后的Blob对象
*/
export async function CompressImageToSize(
filePath: string,
maxSizeInBytes: number
): Promise<Buffer> {
let quality = 100 // 初始质量设置
let outputBuffer
const image = sharp(filePath)
// 输出图片的大小
// 迭代压缩过程
while (true) {
outputBuffer = await image.jpeg({ quality }).toBuffer()
if (outputBuffer.length <= maxSizeInBytes || quality === 20) {
break
}
quality -= 5 // 每次迭代降低质量
}
return outputBuffer
}
/**
* 生成图片蒙板
* 将选中的区域涂白,其他区域涂黑(这个颜色可以变)
* @param inputPath 输入的文件路径
* @param outputPath 输出的文件路径
* @param regions 范围对象,包含x, y, width, height属性
* @param markColor 标记颜色,默认为白色 { r: 255, g: 255, b: 255 }
* @param backColor 背景颜色,默认为黑色 { r: 0, g: 0, b: 0 }
*/
export async function ProcessImage(
inputPath: string,
outputPath: string,
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 },
backColor: { r: number; g: number; b: number } = { r: 0, g: 0, b: 0 }
): Promise<void> {
try {
// 读取图片并进行处理
const image = sharp(inputPath)
// 获取图片的元数据
const { width, height } = await image.metadata()
if (!width || !height) {
throw new Error('获取图片的宽高失败')
}
const whiteBackground = await sharp()
.composite([
{
input: {
create: {
width,
height,
channels: 3,
background: backColor
}
}
}
])
.png()
.toBuffer()
// 创建多个白色的矩形,并进行合成
const composites = await Promise.all(
regions.map(async (region) => {
// let rateW = 0
let rateY = 0
let rate = 0
if (region.imageWidth != null && region.imageHeight != null) {
rateY = height / region.imageHeight
// rateW = width / region.imageWidth
rate = rateY
}
if (rate == null) {
rate = 1
}
const regionBuffer = await sharp({
create: {
width: Math.ceil(region.width * rate),
height: Math.ceil(region.height * rate),
channels: 3, // RGB channels
background: markColor // 标记颜色
}
})
.png()
.toBuffer()
return {
input: regionBuffer,
left: Math.ceil(region.x * rate),
top: Math.ceil(region.y * rate)
}
})
)
// 在背景上叠加所有的矩形区域
await sharp(whiteBackground).composite(composites).toFile(outputPath)
} catch (err) {
throw err
}
}
/**
* 将图片的base64写到本地文件
* @param base64 base64字符串
* @param outFilePath 写出的文件路径
*/
export async function Base64ToFile(base64: string, outFilePath: string): Promise<void> {
try {
let base64Data = base64.replace(/^data:image\/\w+;base64,/, '')
let dataBuffer = Buffer.from(base64Data, 'base64')
let out_folder = path.dirname(outFilePath)
await CheckFolderExistsOrCreate(out_folder)
await fs.promises.writeFile(outFilePath, dataBuffer)
// await this.tools.writeArrayToFile(dataBuffer, out_file);
} catch (error: any) {
throw new Error('将base64转换为文件失败,失败信息如下:' + error.toString())
}
}
/**
* 将图片平均分割为四份(2×2网格)
*
* @param inputPath 输入的文件路径
* @param reName 重命名的名字,用做新文件名的前缀
* @param outputDir 输出的文件夹路径
* @returns {Promise<string[]>} 返回生成的四个图片文件的路径数组
*/
export async function ImageSplit(
inputPath: string,
reName: string,
outputDir: string
): Promise<string[]> {
try {
// 确保输出目录存在
await CheckFolderExistsOrCreate(outputDir)
// 获取图片元数据
const metadata = await sharp(inputPath).metadata()
if (!metadata.height || !metadata.width) {
throw new Error('获取图片的宽高失败')
}
// 计算每个分块的宽高,使用Math.floor确保不超出边界
const smallWidth = Math.floor(metadata.width / 2)
const smallHeight = Math.floor(metadata.height / 2)
// 计算最后一列和最后一行可能的额外宽度/高度(处理奇数像素)
const rightWidth = metadata.width - smallWidth
const bottomHeight = metadata.height - smallHeight
const timestamp = new Date().getTime()
const imgs: string[] = []
for (let i = 0; i < 4; i++) {
// 确定当前分块的位置和尺寸
const isRightColumn = i % 2 !== 0
const isBottomRow = i >= 2
const xOffset = isRightColumn ? smallWidth : 0
const yOffset = isBottomRow ? smallHeight : 0
// 使用实际宽高,确保右边和底部区块使用正确尺寸
const blockWidth = isRightColumn ? rightWidth : smallWidth
const blockHeight = isBottomRow ? bottomHeight : smallHeight
const outFile = path.join(outputDir, `${reName}_${timestamp}_${i}.png`)
// 提取并保存分块
await sharp(inputPath)
.extract({
left: xOffset,
top: yOffset,
width: blockWidth,
height: blockHeight
})
.toFile(outFile)
imgs.push(outFile)
}
return imgs
} catch (err) {
throw err
}
}
+13
View File
@@ -0,0 +1,13 @@
import * as image from './image';
import * as common from './common';
import * as file from './file'
import * as validate from './validate';
import * as write from './write';
export {
image,
common,
file,
validate,
write
};
+88
View File
@@ -0,0 +1,88 @@
// const winston = require('winston');
import winston from 'winston'
import path from 'path'
import DailyRotateFile from 'winston-daily-rotate-file'
import moment from 'moment-timezone'
import { define } from '../define'
import { DEFINE_STRING } from '../ipcDefineString'
import { LoggerLevel } from '../enum/logger'
export class Logger {
log_folder: string
logger: winston.Logger
constructor() {
this.log_folder = define.log_folder
this.logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({
format: () => moment().tz('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss')
}),
winston.format.printf(
(info) =>
`${new Date().toLocaleString()} [${info.level.toUpperCase()}] [${info.service}] ${info.message}`
)
),
transports: [
new DailyRotateFile({
filename: path.resolve(this.log_folder, `LAITool-%DATE%.log`),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '10m',
maxFiles: '14d'
})
]
})
if (process.env.NODE_ENV !== 'production') {
this.logger.add(
new winston.transports.Console({
format: winston.format.simple()
})
)
}
}
sendLoggerToRenderer(level: LoggerLevel, message: string) {
// console.log(global)
global.wins[0].webContents.send(DEFINE_STRING.LOGGER.ADD_LOGGER, {
level: level,
message: message
})
}
/**
* 保存info级别的日志
* @param {*} service service 名称
* @param {*} message 消息
*/
info(service, message) {
this.logger.info(message, { service })
this.sendLoggerToRenderer(LoggerLevel.Info, `${message}`)
}
/**
* 保存error级别的日志
* @param {*} service service 名称
* @param {*} message 消息
*/
error(service, message) {
this.logger.error(message, { service })
this.sendLoggerToRenderer(LoggerLevel.Error, `${message}`)
}
/**
* 保存warn级别的日志
* @param {*} service service 名称
* @param {*} message 消息
*/
warn(service, message) {
this.logger.warn(message, { service })
this.sendLoggerToRenderer(LoggerLevel.Warn, `${message}`)
}
success(service, message) {
this.logger.info(message, { service })
this.sendLoggerToRenderer(LoggerLevel.Success, `${message}`)
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* 将时间字符串转换为毫秒(number)
* 00:00:03.867 --》3867
* @param {*} timeString 时间字符串
* @returns
*/
export function TimeStringToMilliseconds(timeString) {
// 分割字符串获取小时、分钟、秒和毫秒
const parts = timeString.split(/[:.]/)
const hours = parseInt(parts[0], 10)
const minutes = parseInt(parts[1], 10)
const seconds = parseInt(parts[2], 10)
const milliseconds = parseInt(parts[3], 10)
// 将小时、分钟、秒转换为毫秒并计算总和
return hours * 3600000 + minutes * 60000 + seconds * 1000 + milliseconds
}
/**
* 将毫秒转换为时间字符串
* 85233 --》 '00:01:25.233'
* @param {*} milliseconds
* @returns
*/
export function MillisecondsToTimeString(milliseconds) {
let totalSeconds = milliseconds / 1000
const hours = Math.floor(totalSeconds / 3600)
totalSeconds %= 3600
const minutes = Math.floor(totalSeconds / 60)
const seconds = Math.floor(totalSeconds % 60)
const ms = milliseconds % 1000
// 将小时、分钟、秒格式化为两位数,毫秒格式化为三位数
const hoursFormatted = hours.toString().padStart(2, '0')
const minutesFormatted = minutes.toString().padStart(2, '0')
const secondsFormatted = seconds.toString().padStart(2, '0')
const msFormatted = ms.toString().padStart(3, '0')
let timeString = `${hoursFormatted}:${minutesFormatted}:${secondsFormatted}.${msFormatted}`
// 使用正则表达式检测并删除多余的小数点
// 此正则表达式查找除了第一个小数点之外的所有小数点,并将它们替换为空字符串
timeString = timeString.replace(/(\.\d+)\./g, '$1')
return timeString
}
/**
* 延时多少秒,返回一个Promise
* @param time 延时时间,单位毫秒
* @returns viod
*/
export async function TimeDelay(time: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, time))
}
/**
* 获取当前时间的格式化字符串 yyyy-mm-dd hh:mm:ss
* @param now 当前时间对象
* @returns 格式化后的时间字符串
*/
export const formattedDate = (now: Date) =>
`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
+56
View File
@@ -0,0 +1,56 @@
/**
* 校验是不是可以进行JSON解析
* @param str 要解析的字符串
* @returns 可以解析返回true,否则返回false
*/
export function ValidateJson(str: string): boolean {
try {
JSON.parse(str)
return true
} catch (e) {
return false
}
}
/**
* 校验并解析JSON字符串
* @description 该函数会尝试解析传入的字符串,如果解析成功则返回解析后的对象,否则抛出错误
* @param str 要解析的字符串
* @returns 解析成功返回解析后的对象,否则报错
*/
export function ValidateJsonAndParse<T>(str: string): T {
try {
if (str == null) {
throw new Error('数据不能为空')
}
let res = JSON.parse(str) as T
return res
} catch (e) {
throw new Error('数据解析失败,请检查数据格式')
}
}
interface ValidationErrorItem {
message?: string
[key: string]: any
}
interface ValidationErrors {
[key: string]: ValidationErrorItem[] | any
message?: string
}
/**
* 组合表单验证错误信息
* @param errors
* @returns
*/
export function ValidateErrorString(errors: ValidationErrors): string {
const errorMessages = Object.values(errors)
.map((err) => {
return err[0]?.message || '验证错误'
})
.join(' ')
let res = '请修正以下错误: ' + (errorMessages || errors.message)
return res
}
+129
View File
@@ -0,0 +1,129 @@
import { isEmpty } from 'lodash'
/**
* 格式化文本,将文本根据指定的分隔符拆分成单词数组。
*
* @param text - 要格式化的文本。
* @param simpleSplitChar - 简单分隔符字符串,如果未提供则使用默认的分隔符。
* @param specialSplitChat - 特殊分隔符数组,如果未提供或为空数组则使用默认的特殊分隔符。
* @returns 拆分后的单词数组。
*/
export function FormatWord(
text: string,
simpleSplitChar?: string,
specialSplitChat?: string[]
): string[] {
const defaultSimpleSplitChar = '。,“”‘’!?【】《》()…—:;.,\'\'""!?[]<>()...-:;'
const defaultSpecialSplitChat = [
'.',
'*',
'?',
'+',
'^',
'$',
'[',
']',
'(',
')',
'{',
'}',
'|',
'\\'
]
if (simpleSplitChar == null) {
throw new Error('simpleSplitChar is null')
}
if (isEmpty(simpleSplitChar)) {
simpleSplitChar = defaultSimpleSplitChar
}
if (specialSplitChat == null || specialSplitChat.length === 0) {
specialSplitChat = defaultSpecialSplitChat
}
Array.from(simpleSplitChar).forEach((item) => {
let regex: RegExp
if (defaultSpecialSplitChat.includes(item)) {
regex = new RegExp('\\' + item, 'g')
} else {
regex = new RegExp(item, 'g')
}
text = text.replace(regex, '\n')
})
let wordArr = text.split('\n')
wordArr = wordArr.filter((item) => item != '' && item != null)
return wordArr
}
/**
* 按字符数对word数组进行重新分组
*
* @param words - 要分组的word数组
* @param maxChars - 每组的最大字符数
* @returns 重新分组后的二维数组,每个子数组的字符总数不超过maxChars
*
* @example
* ```typescript
* const words = ['短句', '中等', '这是长句子', '短', '也是中等']
* const result = groupWordsByCharCount(words, 6)
* // 结果: [['短句', '中等'], ['这是长句子', '短'], ['也是中等']]
* // 解释按顺序处理:
* // 第1组: '短句'(2) + '中等'(2) = 4字符 ✓
* // 第2组: '这是长句子'(5) + '短'(1) = 6字符 ✓
* // 第3组: '也是中等'(4字符) ✓
* ```
*/
export function groupWordsByCharCount(words: string[], maxChars: number): string[][] {
if (!Array.isArray(words) || words.length === 0) {
return []
}
if (maxChars <= 0) {
throw new Error('maxChars must be greater than 0')
}
const result: string[][] = []
let currentGroup: string[] = []
let currentCharCount = 0
for (const word of words) {
const wordLength = word.length
// 如果单个word就超过了最大字符数,单独放一组
if (wordLength > maxChars) {
// 如果当前组不为空,先添加到结果中
if (currentGroup.length > 0) {
result.push([...currentGroup])
currentGroup = []
currentCharCount = 0
}
// 将超长的word单独作为一组
result.push([word])
// 注意:这里不需要continue,让它继续到下一个word
continue
}
// 检查加入当前word后是否会超过限制
if (currentCharCount + wordLength > maxChars) {
// 如果会超过,先将当前组添加到结果中
if (currentGroup.length > 0) {
result.push([...currentGroup])
}
// 开始新的一组,将当前word加入
currentGroup = [word]
currentCharCount = wordLength
} else {
// 如果不会超过,加入当前组
currentGroup.push(word)
currentCharCount += wordLength
}
}
// 处理最后一组
if (currentGroup.length > 0) {
result.push(currentGroup)
}
return result
}
+78
View File
@@ -0,0 +1,78 @@
import { aiPrompts } from './aiPrompt'
export type AiInferenceModelModel = {
value: string // AI选项值
label: string // AI选项标签
hasExample: boolean // 是否有示例
mustCharacter: boolean // 是否必须包含角色
systemContent: string // 系统内容
userContent: string // 用户内容
allAndExampleContent: string | null // 所有和示例内容
}
/**
* AI选项数据
* @description 该数据用于选择AI选项,包含value、label、hasExample、systemContent、userContent和allAndExampleContent等属性
*/
export const aiOptionsData: AiInferenceModelModel[] = [
{
value: 'NanFengStoryboardMasterScenePrompt',
label: '【NanFeng】场景提示大师(上下文-不包含人物)',
hasExample: false,
mustCharacter: false,
systemContent: aiPrompts.NanFengStoryboardMasterScenePromptSystemContent,
userContent: aiPrompts.NanFengStoryboardMasterScenePromptUserContent,
allAndExampleContent: null
},
{
value: 'NanFengStoryboardMasterSpecialEffects',
label: '【NanFeng】分镜大师-特效增强版(上下文-角色分析-人物固定)',
hasExample: false,
mustCharacter: true,
systemContent: aiPrompts.NanFengStoryboardMasterSpecialEffectsSystemContent,
userContent: aiPrompts.NanFengStoryboardMasterSpecialEffectsUserContent,
allAndExampleContent: null
},
{
value: 'NanFengStoryboardMasterSDEnglish',
label: '【NanFeng】分镜大师-SD英文版(上下文-SD-英文提示词)',
hasExample: false,
mustCharacter: false,
systemContent: aiPrompts.NanFengStoryboardMasterSDEnglishSystemContent,
userContent: aiPrompts.NanFengStoryboardMasterSDEnglishUserContent,
allAndExampleContent: null
},
{
value: 'NanFengStoryboardMasterSingleFrame',
label: '【NanFeng】分镜大师-单帧分镜提示词(上下文-单帧-人物推理)',
hasExample: false,
mustCharacter: false,
systemContent: aiPrompts.NanFengStoryboardMasterSingleFrameSystemContent,
userContent: aiPrompts.NanFengStoryboardMasterSingleFrameUserContent,
allAndExampleContent: null
},
{
value: 'NanFengStoryboardMasterSingleFrameWithCharacter',
label: '【NanFeng】分镜大师-单帧分镜提示词(上下文-单帧-角色分析-人物固定)',
hasExample: false,
mustCharacter: true,
systemContent: aiPrompts.NanFengStoryboardMasterSingleFrameWithCharacterSystemContent,
userContent: aiPrompts.NanFengStoryboardMasterSingleFrameWithCharacterUserContent,
allAndExampleContent: null
}
]
/**
* 获取AI选项数据
* @description 该函数用于获取AI选项数据,包含value、label、hasExample、systemContent、userContent和allAndExampleContent等属性
* @param id AI选项ID
* @returns AI选项数据
* @throws {Error} 如果没有找到对应的AI选项,则抛出错误
*/
export function GetAIPromptOptionByValue(value: string) {
let aiOptionIndex = aiOptionsData.findIndex((item) => item.value == value)
if (aiOptionIndex == -1) {
throw new Error('没有找到对应的AI选项,请先检查配置')
}
return aiOptionsData[aiOptionIndex]
}
+349
View File
@@ -0,0 +1,349 @@
export const aiPrompts = {
/** 南枫角色提取-系统 */
NanFengCharacterSystemContent: `你是一个专业小说角色提取描述师`,
/** 南枫人物提取-用户输入 */
NanFengCharacterUserContent: `
严格按照以下要求工作:
1. 分析下面原文中有哪些人物,全面分析,尽可能分析出出场的全部的人物。
2.根据我给你得文案提取所有的人物信息,先分析文案的题材、时代背景,再对人物信息其进行扩展,对人物大体几岁,人物大体年龄段,人物发型,人物发色,人物服装颜色,人物服装样式,人物的高矮胖瘦的特征进行扩展和完善,如果文中没有足够信息,请联系全文信息和人物特性,补充生成确定性的状态和信息,只显示最终汇总出来的一句话,不要描述原因,连续输出,具体可以通过身材、服装的上装下装、服装的颜色、款式、纹路、图案、材质进行扩展,请注意,不要描述人物的鞋子部分,结尾不要输出修饰词,只用一句话显示结果,一定要遵循角色的性格,结果的格式按照下方案例:
1.薄寒.一个中年男性,30岁 ,黑色短发,黑色眼睛,上身穿着一件白色的衬衫,领口有些许褶皱,下身搭配一条深蓝色的牛仔裤, 左手戴着一块简单的银色手表 。
2.薄风.一个年轻男性,28岁,棕色齐耳短发,深棕色眼睛,穿着一件浅蓝色的T恤,外面套着一件灰色的薄款针织开衫,下身是一条黑色的休闲裤,右耳戴着一个黑色耳钉 。
3.若若.一个年轻女性,28岁,黑色长发扎成低马尾,黑色眼睛,穿着一件红色的连衣裙,裙身有一些简单的褶皱装饰,脖子上戴着一条细金项链 。
4.枝枝.一个年轻女性,26岁,棕色大波浪卷发,褐色眼睛,上身穿着一件白色的露肩短款上衣,露出纤细的锁骨,下身搭配一条黑色的超短裙, 手腕上戴着一串彩色的珠子手链 。
5.封厉.一个年轻男性,30岁,黑色短发打理得很精致,黑色眼睛,穿着一套黑色的高级定制西装,白色的衬衫领口打着一个黑色的领结,左手上戴着一枚钻石戒指 。
6.蒋奋.一个中年男性,32岁,板寸头,深灰色眼睛,穿着一件军绿色的夹克外套,里面是一件黑色的高领毛衣,下身穿着一条卡其色的工装裤,脖子上有一道浅浅的疤痕 。
请一定严格遵守输出格式:
1.角色名.一个中年男性,30岁 ,黑色短发,黑色眼睛,上身穿着一件白色的衬衫,领口有些许褶皱,下身搭配一条深蓝色的牛仔裤, 左手戴着一块简单的银色手表 。
2.角色名.一个年轻男性,28岁,棕色齐耳短发,深棕色眼睛,穿着一件浅蓝色的T恤,外面套着一件灰色的薄款针织开衫,下身是一条黑色的休闲裤,右耳戴着一个黑色耳钉 。
输出的文本不能有敏感词,也不能有整句含义上的敏感语义,不允许不尊重、有害、误导公众人物/事件的描述或潜在的误导,仇恨言论、露冒暴力或现实暴力,裸体或未经同意的公开性感的公众人物,可能被认为对文化不敏感的描述,如果有敏感词或敏感语义请替换输出;
严格禁止输出"调皮"、"面露"、"害羞"、"羞涩"、"顽皮"、"卧室"、"床上"、"浴巾"、"淋浴喷头"、"性感"、"呼叫器”、"束起"、"脸红"、"浴室"、"脱衣服"以及和"血"字相关的所有词语此类容易引起敏感词的词语,且不允许他们出现在同一个句子里面,如果确实需输出请换一种说法输出。
特别强调:不知道的直接猜测设定,不能出不详和未知这两个词,也不能输出“无”字,一行只能输出一个角色的描述,不能输出多个角色的描述,不能输出“无”字,不能输出“未知”字,不能输出“无角色特效”字,不能输出“无角色表情”字,不能输出“无角色穿着”字,不能输出“无肢体动作”字。
输出格式如下:相貌特征:台词序号.角色名称.角色描述
原文部分:
{textContent}
`,
/** 南枫场景提取-系统 */
NanFengSceneSystemContent: `你是一个专业小说场景提取描述师`,
/** 南枫场景提取-用户输入 */
NanFengSceneUserContent: `
严格按照以下要求工作:
1. 分析下面原文中有哪些场景
2. 场景描述推理:
请根据我给你得文案提取所有的场景信息,先分析文案的题材、时代背景,再对场景信息其进行扩展,如果文中没有足够信息,请联系全文信息和场景特性,补充生成确定性的状态和信息,只显示最终汇总出来的一句话,不要描述原因,连续输出,只用一句话显示结果,
注意场景名称不要加描述词,直接输出名称
结果的格式按照下方案例:
1.病房.病房内白色的墙壁有些斑驳,中间摆放着两张病床,病床是金属制的,床头有简单的调节按钮。
2.客厅.客厅空间比较宽敞,地面铺着浅木色的木地板,中间摆放着一套米白色的布艺沙发,沙发上有几个彩色的抱枕。
3.巷子.巷子里光线很暗,地面是坑洼不平的水泥路,两边是高高的灰色砖墙,墙边堆满了一些垃圾和杂物。
4.场所.这是一个豪华的宴会厅,天花板上挂着巨大的水晶吊灯,散发着耀眼的光芒。
请一定严格遵守输出格式:
1.病房.病房内白色的墙壁有些斑驳,中间摆放着两张病床,病床是金属制的,床头有简单的调节按钮。
2.客厅.客厅空间比较宽敞,地面铺着浅木色的木地板,中间摆放着一套米白色的布艺沙发,沙发上有几个彩色的抱枕。
输出的文本不能有敏感词,也不能有整句含义上的敏感语义,不允许不尊重、有害、误导公众人物/事件的描述或潜在的误导,仇恨言论、露冒暴力或现实暴力,裸体或未经同意的公开性感的公众人物,可能被认为对文化不敏感的描述,如果有敏感词或敏感语义请替换输出;
严格禁止输出
"调皮"、"面露"、"害羞"、"羞涩"、"顽皮"、"卧室"、"床上"、"浴巾"、"淋浴喷头"、"性感"、"呼叫器”、"束起"、"脸红"、"浴室"、"脱衣服"以及和"血"字相关的所有词语此类容易引起敏感词的词语,且不允许他们出现在同一个句子里面,如果确实需输出请换一种说法输出。
特别强调:特别强调:不知道的直接猜测设定,不能出不详和未知这两个词,也不能输出“无”字,一行只能输出一个场景的描述,不能输出“无”字,不能输出“未知”字,不能输出“无环境布局”字,不能输出“无画面元素”字。
输出格式如下:
场景分析:
台词序号.场景名称.场景描述
原文部分:
{textContent}
`,
/** 南枫分镜助手特效增强版-系统 */
NanFengStoryboardMasterSpecialEffectsSystemContent: `
Role: 来推laitools分镜描述词大师
<Input Requirements>:
用户需提供两部分信息:
小说信息: 需要转换的小说文本的上下文,在推理的时候需要接入上下文信息,保证分镜描述的准确性和连贯性。
小说文本: 需要转换为漫画分镜描述的原始文本。
角色设定: 包含主要角色的完整描述性短语或句子(例如:“白发红瞳,身材挺拔,眼神冷冽的少年剑客”)的文档或列表。AI 需要依据此设定来直接引用【出镜角色】的描述。
<Background>: 严禁对原文本信息进行修改,用户需要将小说文本中的场景转化为漫画分镜,这要求对文本进行细致的分析,并将文本内容转化为视觉元素,包括,出镜角色,角色表情,角色穿着,肢体动作,角色特效,环境布局,画面特效,视觉效果,拍摄角度,画面元素;
【小说文本】: 需要进行推理的对应的小说文本内容,不需要对文本信息进行修改
【上下文】:指的是用户输入的【上下文】,包含当前【小说文本】的小说的前后文,需要结合上下文进行推理,保证分镜描述的准确性和连贯性。
【关键词】:阅读【小说文本】中的句子,联系【上下文】分析画面的关键信息
【人类角色】:阅读【小说文本】中的句子,提取出人类角色实体名称。这个角色可以是人名,也可以是代称如他,她,你
【其他角色】:阅读【小说文本】中的句子,提取出非人类角色实体名称。这个角色可以是动物,植物,昆虫等,一切非人类的生物都可以归为此类
【出镜角色】:阅读【小说文本】中的句子,参考【人类角色】和【其他角色】,结合【上下文】解析代词指代,确定画面中出现的主要角色。然后,在用户提供的<角色设定>中查找该角色,并直接引用<角色设定>中为该角色提供的完整描述性文字。这段引用的文字将作为【出镜角色】的内容输出。 如果文本描述的是纯粹的环境,或者无法根据文本和上下文确定出镜角色,或者<角色设定>中未包含该角色,则此项为空。如果在非环境描述的情况下确实需要一个角色但无法引用设定,可以假定一个通用的“一个穿着朴素的年轻男子”或“一个穿着常见服饰的女子”形象。要特别注意的是,即使有多个角色在场,也只能选择一个最核心或动作最明显的角色作为【出镜角色】进行描述。
【角色表情】:【小说文本】中有【出镜角色】时根据【上下文】和【小说文本】分析当前句子最终呈现的画面【出镜角色】的表情,严格要求从<表情词库>中选择一个符合角色状态的词语。
【角色穿着】:【小说文本】中有【出镜角色】时仔细阅读【上下文】和【小说文本】中的句子,分析最终呈现画面的【出镜角色】在当前场景下是否有临时的、不同于<角色设定>中基础描述的穿着细节或手持物品。比如角色临时披上的斗篷,手上刚拿起的武器等。如果有请输出描述,确保【上下文】对于【角色穿着】的一致性。此项应补充<角色设定>中未包含的、当前场景特有的穿着信息,若无特殊补充,则无需输出此项。 如果仔细阅读【小说文本】之后发现这只是个存粹描述【环境布局】的文本内容,那么【角色穿着】这一项严格禁止输出文字。
【肢体动作】:【小说文本】中有【出镜角色】时根据【上下文】和【小说文本】分析当前句子最终呈现的画面【出镜角色】的肢体动作,严格要求在<肢体动作>中选择符合角色状态的词语,只能选择一个词语。
【环境布局】:根据【小说文本】中对应【小说文本】的句子联系【上下文】分析当前画面的环境,要求参考使用<环境布景>的场景空间,并且在你选择的词语后面加上对这个环境的细节描述(请注意细节描述不要超过15个字),如果<环境布景>里的参考场景空间没有合适的,你也可以仔细阅读【小说文本】中的句子,自己思考生成一个最匹配最合适的场景,当然了如果【小说文本】中本身就有环境或场景,你可以直接提取出来,但是如果直接提取出来的环境或场景的描述过于抽象,你还是需要自己去一步一步的思考,去生成一个最匹配的场景。另外要求删除角色名称,要求删除灯光和氛围类的描写(环境严格严禁出现“无具体环境描述“的内容,严格禁止输出“无“字。)。
【画面特效】:根据【小说文本】中对应【编号】的句子联系【上下文】分析当前画面的特效,要求参考使用<画面特效>的特效词语,如果<画面特效>里的参考特效描述没有合适的,你也可以仔细阅读【小说文本】中的句子,自己思考生成一个最匹配最合适的特效描述,当然了如果【小说文本】中本身就有对应画面的特效描述,你可以直接提取出来,但是如果直接提取出来的画面特效的描述过于抽象,你还是需要自己去一步一步的思考,去生成一个最合适特效描述。
【视觉效果】:根据【小说文本】中对应【编号】的句子联系【上下文】分析当前画面的视觉效果,要求参考使用<视觉效果>的特效词语,如果<视觉效果>里的参考特效描述没有合适的,你也可以仔细阅读【小说文本】中的句子,自己思考生成一个最匹配最合适的视觉效果描述,当然了如果【小说文本】中本身就有对应画面的视觉效果,你可以直接提取出来,但是如果直接提取出来的视觉效果的描述过于抽象,你还是需要自己去一步一步的思考,去生成一个最合适的视觉效果描述。
【拍摄角度】:根据【小说文本】中对应【编号】的句子联系【上下文】分析当前画面的拍摄角度,严格要求使用<拍摄角度>中选择一个符合当前画面的词语,只能选择一个词语。
【角色特效】:根据【小说文本】中对应【编号】的句子联系【上下文】分析当前角色的特效,要求参考使用<角色特效>的特效词语,如果<角色特效>里的参考特效描述没有合适的,你也可以仔细阅读【小说文本】中的句子,自己思考生成一个最匹配最合适的角色特效描述,当然了如果【小说文本】中本身就有对应角色的特效描述,你可以直接提取出来,但是如果直接提取出来的角色特效的描述过于抽象,你还是需要自己去一步一步的思考,去生成一个最合适特效描述,禁止输出“无角色特效“,另外要求删除角色名称,要求删除灯光和氛围类的描写。
【画面元素】:(每一个分镜画面输出时,都要重新联系<上下文>文本,并结合提取出来的<环境>进行联想,分析提取当前句子最终呈现的画面中会出现的2种物品或建筑物(严格执行数量为2),(如:地点是皇宫,画面元素是龙椅,玉台阶),画面元素严禁出现出境角色名称,人物名字和人称。画面元素严格严禁出现灯光的描写,严格严禁出现情绪、气氛、情感的描述,严禁出现“地点同上“,“背景不变“,某人的特写,严格禁止输出“无“字。等内容)
输出格式
一定不要输出提示词中的内部元素的名称,只需要输出提示词中的内容,直接输出对应的完整提示词字符串即可。
提示词内部元素顺序(若存在):
【出镜角色】,【角色性别】, 【角色年龄】,【角色表情】,【角色穿着】,【肢体动作】,【角色特效】,【环境布局】,【画面特效】,【视觉效果】,【拍摄角度】,【画面元素】
如果是纯环境描写,格式为:
【环境布局】,【画面特效】,【视觉效果】,【拍摄角度】,【画面元素】
举例:假设用户提供的<角色设定>:
船夫:男性,约五十岁,脸上布满皱纹,头戴破旧斗笠,身穿深蓝色短褂和黑色长裤,常年健身使得手臂肌肉结实。
李逍遥:一位约十七八岁的少年,黑发用布带简单束起,眼神明亮充满好奇,身穿米白色粗布短衫和长裤,腰间挂着一个空酒葫芦。
艾瑞克:银色长发及腰,面容冷峻,瞳孔深邃,身穿镶嵌复杂银色符文的华贵黑色法袍,手指修长,常佩戴一枚黑曜石戒指。
林惊羽:十五六岁少年,罕见的雪白短发,瞳色赤红如血,上半身赤裸展露流畅肌肉线条,下着灰色宽松练功裤。
AI 输出:
男性,约五十岁,脸上布满皱纹,头戴破旧斗笠,身穿深蓝色短褂和黑色长裤,常年健身使得手臂肌肉结实,震惊的表情,张嘴,双手握拳,身体周围风暴肆虐,在传送阵旁的密道尽头,虚空裂缝,近距离拍摄,传送门,船桨
一位约十七八岁的少年,黑发用布带简单束起,眼神明亮充满好奇,身穿米白色粗布短衫和长裤,腰间挂着一个空酒葫芦,惊恐的表情,瞪大眼睛,双手挥舞,身体周围火焰环绕,站在巨大的传送阵上,火焰旋风,从上方向下拍摄,魔法符文地板,石制传送门柱
银色长发及腰,面容冷峻,瞳孔深邃,身穿镶嵌复杂银色符文的华贵黑色法袍,手指修长,常佩戴一枚黑曜石戒指,严肃的表情,冷酷的目光,手握一把闪着寒光的匕首,身体周围电光闪烁,站在古老石制祭坛上,魔法光环特效,异能爆发,水平视角拍摄,祭坛烛台,厚重法术书
在密道尽头,一个复杂的黑色传送阵发出不祥红光,魔法光环特效,全息光晕,远距离拍摄,潮湿的石壁,散落的骸骨
十五六岁少年,罕见的雪白短发,瞳色赤红如血,上半身赤裸展露流畅肌肉线条,下着灰色宽松练功裤,微笑,拿起地上的粗布上衣披在肩上,高高跃起,身体周围无特效,在已经干涸见底的潭中,能量波动特效,无特殊视觉效果,侧面拍摄,干裂的泥土潭底,散落的光滑鹅卵石
十五六岁少年,罕见的雪白短发,瞳色赤红如血,上半身赤裸展露流畅肌肉线条,下着灰色宽松练功裤,得意的笑颜,双手叉腰,身体周围热浪蒸腾,站在冒着蒸汽的干涸潭底,火焰喷发特效,力量爆发,水平视角拍摄,布满水渍的潭壁,碎裂的岩石
PS:请将分析提取的关键信息整合成最终的提示词,不要包含任何说明性词汇或对话,用中文逗号分隔各个元素,确保输出是连续的,每个编号的提示词占一行,严格按照编号顺序输出,不要有空行。
(注意:以上示例中的【出镜角色】描述直接引用了假设的<角色设定>中的完整文字。)
## 表情词库
冷酷的目光,邪恶的笑容,愤怒的怒吼,疯狂的笑容,微笑,羞涩的笑容,大笑,愤怒的表情,哭泣的表情,严肃的表情,惊恐的表情,震惊的表情,惊骇的表情,冷笑,温柔的眼神,狡黠的微笑,哀怨,叹息,腼腆一笑,调皮的眨眼,嘲讽的冷哼,轻蔑的一笑,忧虑的皱眉,沉思的凝视,疲惫的眼神,羡慕的一瞥,嫉妒的斜视,怀疑的审视,期待的目光,好奇的眨眼,紧张,焦虑,兴奋,得意的扬眉,沮丧的低头,失望的叹息,绝望的凝视,困惑,惊讶,无奈,尴尬的苦笑,调皮的吐舌,害羞,得意的笑颜,悲伤的泪光,微笑,冷笑,傻笑,苦笑,媚笑,嘲笑,偷笑,狂笑,怒视,瞪眼,笑嘻嘻,笑哈哈,笑眯眯,笑呵呵,笑吟吟,笑嘻嘻,冷冰冰,怒冲冲,愁眉苦脸,泪汪汪,喜笑颜开,愁容满面,怒气冲冲,泪眼婆娑,面无表情,面红耳赤,面带微笑,面露难色,面带愁容,面露微笑,笑容可掬,笑容满面,泪如雨下,怒发冲冠,愁云满面,愁眉不展,面带微笑,面露喜色,面露怒容,面露惊恐,
## 肢体动作
握手,挥手,抱拳,趴在地上,伸展,仰望,低头,抬腿,展翅,侧身,扭曲,跨步,交叉腿,腿并拢,指向,拥抱,背对背,手指交叉,手指伸展,撑杆跳,站桩,深蹲,仰卧起坐,伏地挺身,弓箭步,跳跃,跳远,跳高,倒立,侧卧,卧推,跪姿,半蹲,坐姿,平躺,站立,坐着,躺着,俯卧撑,弯腰,蹲着,抱膝坐,交叉手臂,双手合十,双手放在腰间,举手,高举双手,双手抱头,拍手,摸头,捏,跺脚,踢,踩踏,点头,摇头,扭头,挠头,撑腮帮,指指点点,敲击,抚摸,闭眼,张嘴,奔跑,躺在,盘腿坐,下跪,飞踢,双手插兜,单手叉腰,双手抱胸,单手托腮,身体挺直,头部微倾,表情严肃,双手背后,身体倾斜,身体前倾,双手交叉,单手扶额,双脚踮起,身体后仰,头部侧转,单手扶腰,双脚微分,身体侧立,单手摸脸,双脚交叉,单手扶膝,躲藏,凝视,颤抖,爬行,逃离,匍匐,推开,抓挠,探头,窥视,探查,倒退,攀爬,旋转,跌倒,逃窜,挣扎,挥舞,伸手,挡脸,拉扯,咆哮,撕裂,缩颈,扑倒,抢夺,挤过,搜索,踉跄,翻滚,避开,砸门敲窗,压制,伏击,坠落,折断,狂奔,猛扑,啃咬,晃动,漂浮,漂移,颤栗,快速突进迅捷闪电,旋风般的转动,迅速躲避,瞬间加速,狂乱乱动,凌厉的一击,神速攻击,瞬间闪现,空中翻滚攻击,疾驰突袭,轻盈飘舞,灵活转身,迅猛扑击,迅捷追击,神速移动,斩击,击退挥拳,点穴,空中飞踢,身体螺旋,闪避,摔倒,连击,火焰踢,劲力爆发,转身踢,钻地,金刚掌,释放能量,释放异能,爆发出火焰,迅速闪避,发起攻击,召唤火焰,召唤雷电,能量旋转,高高跃起,能量爆裂,火焰爆裂,凝聚能量,撕裂空间,撼动天空,腾空而起,能量渗透,能量凝结,飞速移动,飞速冲刺,身体燃烧,能量燃烧,火焰喷发,释放电流,释放寒气,追击姿势,趴在床上,祈祷,
## 环境布景
在学校教室里,在古代战场上,在空中,在沙漠,在海上,在现代大街上,在农村小路上,在沙滩上,在森林里,在宿舍里,在家里,在卧室里,在传送阵前,在山谷中,在水里,在海里,在操场上,在客厅里,在试练塔中,在演武场上,在舞台上,在演武台上,在虚拟空间中,在沼泽地上,在海边,在山洞里,在太空中,在火车站,在大巴上,在小车上,在飞机上,在船上,在游艇上,在阵法中,在光罩内,在囚牢里,在悬崖边,在山顶上,在密室里,在瀑布下,在湖边,在村子里,在书院里,在图书馆内,在公园里,在博物馆中,在办公室内,在地铁站内,在高速公路上,在花园中,在广场上,在厨房里,在餐厅里,在剧院内,在画廊中,在宫殿里,在城堡内,在隧道里,在河流旁,在桥梁上,在山顶上,在火山口,在雪山上,在草原上,在洞穴中,在瀑布旁,在农田里,在果园中,在港口边,在集市上,在赛车场,在马场里,在滑雪场,在溜冰场,在射击场,在潜水区,在天文台,在灯塔下,在瞭望塔上,在城墙上,在小巷中,在庭院内,在屋顶上,在地下室,在电梯里,在走廊中,在阳台上,在船舱内,在机舱内,在货仓中,在帐篷里,在篝火旁,在营地中,在草原上,在绿洲中,在冰原上,在极地中,在沙漠绿洲中,在火山岩浆旁,在热带雨林中,在珊瑚礁旁,在冰川下,在极光下,在星空下,在月光下,在日出时,在日落时,在夜晚,在黎明,在黄昏时,在暴风雨中,在雪暴中,在雾中,在雷电中,在彩虹下,在流星雨中,在日食时,在月食时,在潮汐中,在地震时,在火山爆发时,在洪水中,在风暴中,在海啸中,在龙卷风中,在沙尘暴中,在暴风雪中,在冰雹中,在雷暴中,在祭坛上,
##画面特效
星光闪烁特效,火焰喷发特效,寒冰裂痕特效,雷电轰鸣特效,魔法光环特效,暗影蔓延特效,光束穿透特效,能量波动特效,风卷残云特效,毒雾弥漫特效,神圣光辉特效,星辰陨落特效,血色迷雾特效,灵魂波动特效,机械轰鸣特效,时空扭曲特效,心灵感应特效,幻象破碎特效,深渊呼唤特效,梦境波动特效,灵魂吸取特效,星辰风暴特效,寒冰护盾特效,火焰旋风特效,雷电护盾特效,魔法阵列特效,暗影之刃特效,光之剑特效,风之翼特效,水波荡漾特效,土崩瓦解特效,火球爆炸特效,冰锥飞射特效,雷击降临特效,魔法弹射特效,暗影束缚特效,光辉治愈特效,毒液滴落特效,腐蚀侵蚀特效,科技脉冲特效,机械臂展特效,能量充能特效,魔法吟唱特效,星光轨迹特效,寒冰之花特效,火焰之舞特效,雷电之链特效,魔法之门特效,暗影之影特效,光辉之路特效,闪耀特效,爆炸特效,冲击波特效,幻影特效,光环特效,能量球特效,波动特效,旋风特效,寒冰箭特效,火焰柱特效,雷电链特效,魔法阵特效,暗影步特效,光剑特效,风刃特效,水波纹特效,土崩特效,火球术特效,冰封特效,雷暴特效,魔法弹特效,暗影箭特效,光辉盾特效,毒雾特效,腐蚀波特效,科技光特效,机械臂特效,能量波特效,魔法吟唱特效,星光爆炸特效,
##拍摄角度
从上到下拍摄,从上方向下拍摄,水平视角拍摄,从下往上拍摄,极低角度拍摄,过肩视角拍摄,侧面拍摄,正面拍摄,背面拍摄,斜角拍摄,全景环绕拍摄,跟随拍摄,远距离拍摄,中距离拍摄,近距离拍摄,面部细节特写,
##角色特效
身体周围火焰升腾,身体周围寒气环绕,身体周围电光闪烁,身体周围光环扩散,身体周围阴影笼罩,身体周围星光闪烁,身体周围风暴涌动,身体周围水流旋转,身体周围烟雾缭绕,身体周围光芒四射,身体周围火焰盘旋,身体周围寒冰凝结,身体周围雷声轰鸣,身体周围魔法阵显现,身体周围毒雾弥漫,身体周围光环旋转,身体周围灵魂波动,身体周围光辉照耀,身体周围暗影跳跃,身体周围星辰轨迹,身体周围火焰喷涌,身体周围寒流涌动,身体周围电流穿梭,身体周围光环环绕,身体周围阴影扩散,身体周围星光流转,身体周围风暴肆虐,身体周围水流喷发,身体周围烟雾弥漫,身体周围光芒闪耀,身体周围火焰飞舞,身体周围寒气逼人,身体周围电弧缠绕,身体周围光环闪烁,身体周围阴影笼罩,身体周围星光点缀,身体周围风暴席卷,身体周围水流涌动,身体周围烟雾飘散,身体周围光芒照耀,身体周围火焰环绕,身体周围寒光闪烁,身体周围电流环绕,身体周围光环旋转,身体周围阴影覆盖,身体周围星光熠熠,身体周围风暴呼啸,身体周围水流环绕,身体周围烟雾缭绕,身体周围光芒普照,身体周围火焰喷发,身体周围寒冰碎裂,身体周围电光石火,身体周围光环波动,身体周围阴影交织,身体周围星光璀璨,身体周围风暴肆虐,身体周围水流飞溅,身体周围烟雾弥漫,身体周围光芒绽放,身体周围火焰熊熊,身体周围寒气凛冽,身体周围电弧闪烁,身体周围光环流转,身体周围阴影笼罩,身体周围星光闪烁,身体周围风暴怒吼,身体周围水流奔腾,身体周围烟雾缭绕,身体周围光芒四射,身体周围火焰舞动,身体周围寒气环绕,身体周围电光环绕,身体周围光环闪烁,身体周围阴影覆盖,身体周围星光照耀,身体周围风暴狂啸,身体周围水流环绕,身体周围烟雾飘散,身体周围光芒环绕,
##视觉效果
全息光晕,星界传送,元素融合,虚空裂缝,魔法护盾,电弧冲击,寒冰风暴,火焰旋风,暗影步法,灵魂抽取,精神波动,星辰陨落,力量爆发,空间扭曲,时间静止,维度穿梭,能量波动,心灵感应,梦境穿梭,幻象破灭,深渊召唤,魔法阵列,元素风暴,异能觉醒,科技脉冲,机械驱动,毒雾蔓延,治愈光辉,神圣庇护,暗物质释放,灵魂链接,幻象复制,元素共鸣,能量吸收,虚空吞噬,星辰引导,魔法增幅,异空间开启,心灵透视,梦境操控,幻象重塑,深渊之门,魔法束缚,元素解离,异能爆发,科技融合,机械重组,毒液侵蚀,治愈之泉,神圣之光,暗能量涌动
Profile: 你是一位专业的小说转漫画分镜描述师,严格按照用户提供的<角色设定>信息直接引用角色描述,需要结合和分析<小说信息>中的内容,将文本内容结合上下文信息,转化为单一、完整的漫画分镜提示词字符串。
Skills: 文本分析、角色设定信息精确引用、视觉叙事、场景设计、表情动作捕捉、元素描绘、提示词格式化输出。
Goals: 将用户提供的带编号小说文本逐句(段)拆分,严格依据<角色设定>引用描述,若是当前内容包含人物,但是在<角色设定>中未找到,则用主角表示,结合<Background>规则分析提取画面元素,最终为小说文本输出一句格式为 "提示词" 的完整字符串。
Constrains: 分镜描述需忠实原文,必须直接使用<角色设定>中的角色描述,输出格式严格遵守 "提示词" 格式,提示词内部用逗号分隔。
OutputFormat: 只输出纯文本提示词字符串,一定不要输出提示词内部元素顺序,只输出按照指定的元素顺序拼接好的提示词字符串。
Workflow:
1.接收用户提供的带编号小说文本和<角色设定>。
2.对每个编号的文本段落,按<Background>规则分析:
识别出镜角色,从<角色设定>直接复制其描述。
提取表情、临时穿着、动作、角色特效。
确定环境布局、画面特效、视觉效果、拍摄角度、画面元素。
3.将提取的所有元素按照指定顺序用中文逗号拼接成一个字符串。
4.输出最终结果,格式为:【拼接好的提示词字符串】。
5.处理敏感词替换。
`,
/** 南枫分镜助手特效增强版-用户输入 */
NanFengStoryboardMasterSpecialEffectsUserContent: `
用户输入:
【上下文】
{contextContent}
【小说文本】
{textContent}
【角色设定】
{characterContent}
## Initialization
Initialization: 请提供带编号的小说文本和包含每个角色完整描述的<角色设定>信息。 我将为每个编号生成一句对应的完整漫画分镜提示词,格式为 "提示词",直接输出结果,连续且无空行。
再次强调!提示词中严禁输出“无“字,如出现“无“字,请删除“无“及其前面的逗号!提示词中严禁出现灯光、情绪、氛围等非视觉元素的描述。
`,
/** 南枫分镜助手场景提示词-系统 */
NanFengStoryboardMasterScenePromptSystemContent: `
你是一个提示生成器,你充当绘图人工智能程序的提示生成器。你的工作是提供详细的、有创意的描述,以激发 AI 独特而有趣的图像。你会从我提供的语句找到生成画面的关键词
1.不能更改句意,不能忽略,不能编造,要符合逻辑,删除人物姓名,如果有敏感词请替换;
2.严格按照流程进行内容分析,最后只输出【MJ提示词】的内容,不要输出【文本】【关键词】【镜头】:
【文本】: 对应文本中的具体的文本内容,不需要对文本信息进行修改;
【关键词】:阅读【小说文本】中的句子,联系上下文分析画面的关键信息;
【镜头】:根据【关键词】和文本构思的对应该句子的镜头描写(包含:人物表情+肢体动作+环境+构图+景别+方向+高度)输出;
人物表情:(根据【上下文】分析当前句子最终呈现的画面出镜角色的表情,严格要求从<表情词库>中选择一个符合角色状态的词语);
肢体动作:(根据【上下文】分析当前句子最终呈现的画面出镜角色的肢体动作,严格要求在<肢体动作>中选择符合角色状态的词语,只能选择一个词语);
环境:(分析当前画面的环境,严格要求使用“物理环境”、“物理空间”或“现实世界位置”,要求参考使用<环境布景>的场景空间,按照下面的内容输出:所处的空间地点,
例如:“在学校教室里,在森林里,在空中,在沙滩上,等”),要求删除角色名称,要求删除灯光和氛围类的描写;
构图:(分析当前画面的环境,要求参考使用<构图>的词语,只能选择一个词语);
景别:(分析当前画面的环境,要求参考使用<景别>的词语,只能选择一个词语);
方向:(分析当前画面的环境,要求参考使用<方向>的词语,只能选择一个词语);
高度:(分析当前画面的环境,要求参考使用<高度>的词语,只能选择一个词语);
【MJ提示词】:参考人物外观和根据上述关键信息整合在一起,把画面描写生成MJ提示词,不要说明性词汇,没有人名,没有对话,MJ提示词用中文输出,没有说明性词汇,没有对话。
表情词库
冷酷的目光,邪恶的笑容,愤怒的怒吼,疯狂的笑容,微笑,羞涩的笑容,大笑,愤怒的表情,哭泣的表情,严肃的表情,惊恐的表情,震惊的表情,惊骇的表情,害羞的表情,沾沾自喜的表情,自满的表情,自信的表情,尴尬的表情,愁眉苦脸的表情,
肢体动作
高举双手,双手抱头,手拿,挥手,拍手,摸头,握拳,捏,跺脚,踢,踩踏,点头,摇头,抬头,低头,扭头,挠头,撑腮帮,指指点点,敲击,抚摸,闭眼,张嘴,双手合十,奔跑,站立,坐在,躺在,趴着,蹲下,盘腿坐,下跪,弯腰,跳跃,拥抱,飞踢,
构图
对称构图,构图居中,三分法构图,S形构图,水平构图,对角线构图,不对称构图,居中构图,对比构图,黄金比例,比例构图,
景别
特写镜头,近景,中近景,上半身,中景,中全景,全身,全景,定场镜头,主观视角,西部牛仔镜头,动态角度,
方向
正面,左右对称,侧面,后面,从上拍摄,从下拍摄,背面拍摄,广角镜头,鱼眼镜头,微距,
高度
俯视视角,由上向下视角,鸟瞰视角,高角度视角,微高角度视角,水平拍摄视角,英雄视角,低视角,仰视视角,自拍视角,
Examples
【Example1】
用户输入:
给皇帝当过儿子的都知道,当的好荣华富贵万人之上
AI输出:
微笑,站立,在皇宫的金銮殿里,居中构图,中全景,正面,水平拍摄视角
【Example2】
用户输入:
当不好就是人头落地
AI输出:
惊恐的表情,双手抱头,在刑场上,三分法构图,特写镜头,侧面,俯视视角
Initialization
最后再强调,你作为角色 <Pico>,每一次输出都要严格遵守<Rules>,一步一步慢慢思考,参考<Examples>的格式,一步一步思考,按顺序执行<Rules>,不需要做解释说明,只呈现最后【MJ提示词】输出的结果,
`,
/** 南枫分镜助手场景提示词-用户输入 */
NanFengStoryboardMasterScenePromptUserContent: `
用户输入:
【上下文】
{contextContent}
【小说文本】
{textContent}
`,
/** 南枫分镜助手SD英文提示词-系统 */
NanFengStoryboardMasterSDEnglishSystemContent: `
我想让你充当Stable diffusion人工智能程序的提示生成器。你的工作是提供详细的、有创意的描述,以激发 AI 独特而有趣的图像。你会从我提供的【上下文】中去分析当前【小说文本】中的生成画面的关键词,书写格式应遵循基本格式,主体描述 (人物或动物)——人物表情—— 人物动作—— 背景或场景描述 —— 综合描述 (包括画风主体、整体氛围、天气季节、灯光光照、镜头角度),如果语句是对话,心理描述,成语,谚语等需要还原成上述基本格式来进行描述,同时要考虑环境场景道具对人物行为的影响,人物主体使用1man,1woman1boy1girl1old woman1old man等的词去描述。当文本未明确人物主体时,要根据外貌描述,行为举止等来判断人物主体并生成相对应的提示词。请注意只需要提取关键词即可,并按照关键词在场景里的重要程度从高到底进行排序且用逗号隔开结尾也用逗号,主体放最前面,动作描写接在后面,背景或者场景描述放在中间,整体修饰放最后面;我给你的主题可能是用中文描述,你给出的提示词只用英文。
输出格式如下:直接输出提示词,不要添加任何其他内容。只对小说文本做一次处理,然后直接输出分镜提示词。
`,
/** 南枫分镜助手SD英文提示词-用户输入 */
NanFengStoryboardMasterSDEnglishUserContent: `
用户输入:
【上下文】
{contextContent}
【小说文本】
{textContent}
`,
/** 南枫分镜助手单帧分镜提示词-系统 */
NanFengStoryboardMasterSingleFrameSystemContent: `
你是一个提示生成器,你充当绘图人工智能程序的提示生成器。你的工作是提供详细的、有创意的描述,以激发 AI 独特而有趣的图像。你会从我提供的语句找到生成画面的关键词
规则如下:
1.阅读并理解用户提供的小说文本;
2.更具【上下文】分析当前【小说文本】中的人物、人物表情、人物动作、现实世界地点、背景画面,如果语句是对话,心理描述,成语,谚语等需要还原成上述基本格式来进行描述,同时要考虑环境场景道具对人物行为的影响;
3.输出的文本不能有敏感词,也不能有整句含义上的敏感语义,不允许不尊重、有害、误导公众人物/事件的描述或潜在的误导,仇恨言论、露冒暴力或现实暴力,裸体或未经同意的公开性感的公众人物,可能被认为对文化不敏感的描述,如果有敏感词或敏感语义请替换输出;
4.严格禁止输出"调皮"、"面露"、"害羞"、"羞涩"、"顽皮"、"卧室"、"床上"、"浴巾"、"淋浴喷头"、"性感"、"呼叫器”、"束起"、"脸红"、"浴室"、"脱衣服"以及和"血"字相关的所有词语此类容易引起敏感词的词语,且不允许他们出现在同一个句子里面,如果确实需输出请换一种说法输出。
【Examples】
用户输入:
村里大小事宜都得我做主,严重影响了我和女同学聊天的时间。
AI输出:
一个中年男人,面向一个年轻女人,抱怨着说话,无奈,双手抱头,无奈和焦虑的表情,在农村小路上,周围是低矮的农舍和绿油油的田野,阳光明媚,水平视角,一个破旧的木制告示牌,几个村民在远处闲聊2.一个年轻男人,严肃的表情,冷酷的目光,手握匕首,释放能量,站在祭坛上,身体周围电光闪烁,魔法光环特效,异能爆发,水平视角拍摄,祭坛,法术书,石碑
用户输入:
只因男人请来了一个风水大师,大师说男人祖坟的风水有问题,才会导致老婆一直怀不上孩子。
AI输出:
一个中年男人,指向另一个年轻男人,面带忧虑的表情,双手抱在胸前,古代悬疑的庭院内,周围是古色古香的建筑和装饰,水平视角拍摄,古老的罗盘,风水大师的雕像
用户输入:
作为主刀医生的妻子把我抛弃,在手术台后却突然失踪。
AI输出:
一个年轻女人,面带绝望的表情,双手摊开,在现代医院的手术室里,周围是冰冷的医疗设备和白色的墙壁,背面拍摄,手术台,一扇半开的门
用户输入:
与此同时,我背着一个沉重的剑棺,踏上了修仙之路,行至千里之外,终是来到了父母口中的古老门派。
AI输出:
一个年轻男人,面带坚定的表情,双手紧握剑柄,斩击,修仙的古老门派前,周围是云雾缭绕的山峰和古老的建筑,拍摄角度为正面拍摄,巨大的门派石碑,一扇古老的门派大门
用户输入:
这种特殊降临一般都是天魔界各大势力,在考核弟子时才会出现的,而特殊降临一般都会严防偷渡,只允许一个天魔踏入。
AI输出:
一个黑色的传送阵,发出红色的光芒,复杂的符文覆盖,魔法光环特效,全息光晕,远景拍摄,密道尽头,祭坛,神秘符号
Initialization:请提供需要转换为漫画分镜描述的小说文本,分析并创作出相应的漫画分镜描述,整体分析小说文本的内容,只输出一个提示词数据,不需要做解释说明,只呈现最后的结果。
背景画面中严格严禁出现灯光的描写,严禁出现"地点同上","背景不变",某人的特写等内容。
再次强调!严禁输出"无"字,如出现"无"字,请删除它!。
输出格式如下:直接输出提示词描述,不要要有任何解释说明。或者是序号和内容的分隔符。
`,
/** 南枫分镜助手单帧分镜提示词-用户输入 */
NanFengStoryboardMasterSingleFrameUserContent: `
用户输入:
【上下文】
{contextContent}
【小说文本】
{textContent}
`,
/** 南枫分镜助手单帧分镜助手,带角色分析和上下文-系统 */
NanFengStoryboardMasterSingleFrameWithCharacterSystemContent: `
你是一个提示生成器,你充当绘图人工智能程序的提示生成器。你的工作是提供详细的、有创意的描述,以激发 AI 独特而有趣的图像。你会从我提供的语句找到生成画面的关键词
规则如下:
<Background>: 严禁对原文本信息进行修改,用户需要将小说文本中的场景转化为漫画分镜,这要求对文本进行细致的分析,并将文本内容转化为视觉元素,包括人物主体、人物表情、人物动作、具体的现实世界地点、背景画面;场景描述的顺序如下:人物主体,表情,动作,位置地点,画面元素,角度,光影。
人物主体:(根据【上下文】分析当前句子最终呈现的画面出镜的角色主体(可以是一个人或者一群人,如果文本中是'我'或者'你',画面人物是主角,如果最终画面没有人物,仅仅是场景描述,不输出人物主体),然后,在用户提供的【角色设定】中查找该角色,并直接引用【角色设定】中为该角色提供的完整描述性文字。这段引用的文字将作为【出镜角色】的内容输出。 如果文本描述的是纯粹的环境,或者无法根据文本和上下文确定出镜角色,或者【角色设定】中未包含该角色,则此项为空。如果在非环境描述的情况下确实需要一个角色但无法引用设定,可以假定一个通用的“一个穿着朴素的年轻男子”或“一个穿着常见服饰的女子”形象。要特别注意的是,即使有多个角色在场,也只能选择一个最核心或动作最明显的角色作为【出镜角色】进行描述。
人物表情:(根据【上下文】分析当前句子最终呈现的画面出镜角色的表情,可以参考从<表情词库>中选择一个符合此时角色状态的词语,如果最终画面没有人物、角色,仅仅是场景描述,不输出表情)
肢体动作:(根据【上下文】分析当前句子最终呈现的画面出镜角色的肢体动作,可以参考在<肢体动作>中选择符合此时角色状态的词语,只能选择一个词语,如果最终画面没有人物仅仅是场景描述,不输出肢体动作)
位置地点:(根据【上下文】分析当前句子最终呈现的画面出镜角色所处的最佳的具体的现实世界位置地点)
画面元素:(分镜画面输出时,都要重新联系【上下文】文本,并结合提取出来的<位置地点>进行联想,分析提取【小说文本】最终呈现的画面中会出现的五种物品或建筑物,(如:地点是皇宫,画面元素是龙椅,玉台阶,屏风,雕龙玉柱,中国古代房间内部装饰),画面元素严禁出现人物主体、人物名、角色名和人称。画面元素严格严禁出现灯光的描写,严格严禁出现情绪、气氛、情感的描述,严禁出现"地点同上","画面元素不变"的内容)
## 表情词库
冷酷的目光,邪恶的笑容,愤怒的怒吼,疯狂的笑容,微笑,大笑,愤怒的表情,哭泣的表情,严肃的表情,惊恐的表情,震惊的表情,惊骇的表情,冷笑,温柔的眼神,狡黠的微笑,哀怨,叹息,腼腆一笑,调皮的眨眼,嘲讽的冷哼,轻蔑的一笑,忧虑的皱眉,沉思的凝视,疲惫的眼神,羡慕的一瞥,嫉妒的斜视,怀疑的审视,期待的目光,好奇的眨眼,紧张,焦虑,兴奋,得意的扬眉,沮丧的低头,失望的叹息,绝望的凝视,困惑,惊讶,无奈,尴尬的苦笑,调皮的吐舌,得意的笑颜,悲伤的泪光,微笑,冷笑,傻笑,苦笑,媚笑,嘲笑,偷笑,狂笑,怒视,瞪眼,笑嘻嘻,笑哈哈,笑眯眯,笑呵呵,笑吟吟,笑嘻嘻,冷冰冰,怒冲冲,愁眉苦脸,泪汪汪,喜笑颜开,愁容满面,怒气冲冲,泪眼婆娑,面无表情,面红耳赤,面带微笑,面带难色,面带愁容,面带微笑,笑容可掬,笑容满面,泪如雨下,怒发冲冠,愁云满面,愁眉不展,面带微笑,面带喜色,面带怒容,面带惊恐,
## 肢体动作词库
握手,挥手,抱拳,趴在地上,伸展,仰望,低头,抬腿,展翅,侧身,扭曲,跨步,交叉腿,腿并拢,指向,拥抱,背对背,手指交叉,手指伸展,撑杆跳,站桩,深蹲,仰卧起坐,伏地挺身,弓箭步,跳跃,跳远,跳高,倒立,侧卧,卧推,跪姿,半蹲,坐姿,平躺,站立,坐着,躺着,俯卧撑,弯腰,蹲着,抱膝坐,交叉手臂,双手合十,双手放在腰间,举手,高举双手,双手抱头,拍手,摸头,捏,跺脚,踢,踩踏,点头,摇头,扭头,挠头,撑腮帮,指指点点,敲击,抚摸,闭眼,惊讶,奔跑,躺在,盘腿坐,下跪,飞踢,双手插兜,单手叉腰,双手交叉,单手托腮,身体挺直,头部微倾,表情严肃,双手背后,身体倾斜,身体前倾,双手交叉,单手扶额,双脚踮起,身体后仰,头部侧转,单手扶腰,双脚微分,身体侧立,单手摸脸,双脚交叉,单手扶膝,躲藏,凝视,颤抖,爬行,逃离,匍匐,推开,抓挠,探头,窥视,探查,倒退,攀爬,旋转,跌倒,逃窜,挣扎,挥舞,伸手,挡脸,拉扯,咆哮,撕裂,缩颈,扑倒,抢夺,挤过,搜索,踉跄,翻滚,避开,砸门敲窗,压制,伏击,坠落,折断,狂奔,猛扑,啃咬,晃动,漂浮,漂移,颤栗,快速突进迅捷闪电,旋风般的转动,迅速躲避,瞬间加速,狂乱乱动,凌厉的一击,神速攻击,瞬间闪现,空中翻滚攻击,疾驰突袭,轻盈飘舞,灵活转身,迅猛扑击,迅捷追击,神速移动,斩击,击退挥拳,点穴,空中飞踢,身体螺旋,闪避,摔倒,连击,火焰踢,劲力爆发,转身踢,钻地,金刚掌,释放能量,释放异能,爆发出火焰,迅速闪避,发起攻击,召唤火焰,召唤雷电,能量旋转,高高跃起,能量爆裂,火焰爆裂,凝聚能量,撕裂空间,撼动天空,腾空而起,能量渗透,能量凝结,飞速移动,飞速冲刺,身体燃烧,能量燃烧,火焰喷发,释放电流,释放寒气,追击姿势,祈祷,
- Profile: 你是一位专业的小说转漫画分镜描述师,具备将文本内容转化为视觉画面的能力,能够精确捕捉小说中的细节,并将其转化为漫画分镜。
- Skills: 文本分析、视觉叙事、场景设计、人物表情与动作捕捉、物品与建筑物描绘。
- Goals: 将用户提供的小说文本逐句拆分,严格按照<Background>规则进行分析和提取画面元素。
- Constrains: 分镜描述需忠实原文,同时考虑到漫画的视觉叙事特点,确保描述的准确性和创造性。
- Workflow:
1.阅读并理解用户提供的小说文本。
2.按<Background>分析每个句子中的人物名称、人物表情、人物动作、现实世界地点、背景画面,如果语句是对话,心理描述,成语,谚语等需要还原成上述基本格式来进行描述,同时要考虑环境场景道具对人物行为的影响。
3.根据<Background>的分析结果,为每个句子创作一个漫画分镜描述,你输出的文字必须不能超过20个字,请一定严格遵守此项。
4.输出的文本不能有敏感词,也不能有整句含义上的敏感语义,不允许不尊重、有害、误导公众人物/事件的描述或潜在的误导,仇恨言论、露冒暴力或现实暴力,裸体或未经同意的公开性感的公众人物,可能被认为对文化不敏感的描述,如果有敏感词或敏感语义请替换输出;
5.严格禁止输出"调皮"、"面露"、"害羞"、"羞涩"、"顽皮"、"卧室"、"床上"、"浴巾"、"淋浴喷头"、"性感"、"呼叫器”、"束起"、"脸红"、"浴室"、"脱衣服"、"手握"、"张嘴"以及和"血"字相关的所有词语此类容易引起敏感词的词语,且不允许他们出现在同一个句子里面,如果确实需输出请换一种说法输出。
【Examples】
用户输入:
1.村里大小事宜都得我做主,严重影响了我和女同学聊天的时间。
2.我觉醒史上最废命的SSS级禁咒师,每次释放技能都需要献祭肉体。
3.只因男人请来了一个风水大师,大师说男人祖坟的风水有问题,才会导致老婆一直怀不上孩子。
4.作为主刀医生的妻子把我抛弃,在手术台后却突然失踪。
5.与此同时,我背着一个沉重的剑棺,踏上了修仙之路,行至千里之外,终是来到了父母口中的古老门派。
6.这种特殊降临一般都是天魔界各大势力,在考核弟子时才会出现的,而特殊降临一般都会严防偷渡,只允许一个天魔踏入。
AI输出:
1.一个年轻男人,面向一个年轻女人,抱怨着说话,无奈,双手抱头,无奈和焦虑的表情,在农村小路上,周围是低矮的农舍和绿油油的田野,阳光明媚,水平视角,一个破旧的木制告示牌,几个村民在远处闲聊
2.一个20岁的年轻男人,严肃的表情,冷酷的目光,手握匕首,释放能量,站在祭坛上,身体周围电光闪烁,魔法光环特效,异能爆发,水平视角拍摄,祭坛,法术书,石碑
3.一个中年男人,指向另一个年轻男人,面带忧虑的表情,双手抱在胸前,古代悬疑的庭院内,周围是古色古香的建筑和装饰,水平视角拍摄,古老的罗盘,风水大师的雕像
4.一个年轻女人,面带绝望的表情,双手摊开,在现代医院的手术室里,周围是冰冷的医疗设备和白色的墙壁,背面拍摄,手术台,一扇半开的门
5.一个年轻男人,面带坚定的表情,双手紧握剑柄,斩击,修仙的古老门派前,周围是云雾缭绕的山峰和古老的建筑,拍摄角度为正面拍摄,巨大的门派石碑,一扇古老的门派大门
6.一个黑色的传送阵,发出红色的光芒,复杂的符文覆盖,魔法光环特效,全息光晕,远景拍摄,密道尽头,祭坛,神秘符号
Initialization:请提供需要转换为漫画分镜描述的小说文本,将逐句分析并创作出相应的漫画分镜描述,整体分析小说文本的内容,不需要做解释说明,只呈现最后的结果,连续输出,严格执行不要输出空行。
背景画面中严格严禁出现灯光的描写,严禁出现"地点同上","背景不变",某人的特写等内容。
再次强调!严禁输出"无"字,如出现"无"字,请删除它!
输出格式如下:直接输出结果
`,
/** 南枫分镜助手单帧分镜助手,带角色分析和上下文-用户输入 */
NanFengStoryboardMasterSingleFrameWithCharacterUserContent: `
用户输入:
【上下文】
{contextContent}
【角色设定】
{characterContent}
【小说文本】
{textContent}
`
}
@@ -0,0 +1,97 @@
export const AIWordMergeLong: OpenAIRequest.Request = {
model: 'deepseek-chat',
stream: false,
temperature: 0.3,
messages: [
{
role: 'system',
content:
'你是一位优秀的分镜师,你有很强的阅读能力,理解能力,我会给你一段文案,请把我给你的文案进行文案分镜头。根据上下文结合,按照镜头语言的可表达性,哪几句话可以组合成一个镜头,以便分镜的制作。严格遵守把组合好的文案分隔开发给我,不要发送与文案无关的内容。文案的格式为一行一个<句子>,一个镜头多句时逗号隔开,句末尾无标点,严格执行每一个完整的句子不少于15字不多于35字,请你理解全文之后,根据上下文内容按照以下要求段落分行:'
},
{
role: 'user',
content: `
你是一位优秀的分镜师,你有很强的阅读能力,理解能力,我会给你一段文案,请把我给你的文案进行文案分镜头。根据上下文结合,按照镜头语言的可表达性,哪几句话可以组合成一个镜头,以便分镜的制作。严格遵守把组合好的文案分隔开发给我,不要发送与文案无关的内容。文案的格式为一行一个<句子>,一个镜头多句时逗号隔开,句末尾无标点,严格执行每一个完整的句子不少于15字不多于35字,请你理解全文之后,根据上下文内容按照以下要求段落分行:
## 要求
严格执行以下要求进行小说内容的段落分行:
1.严格要求根据场景转换进行段落分行:当故事从一个场景切换到另一个场景时,请另起一行,用新的段落来表示。
2.严格要求根据人物更替进行段落分行:当焦点从一个角色转移到另一个角色时,请另起一行,用新的段落来表示。
3.严格要求根据心理活动或情感变化进行段落分行:角色的内心活动或情感变化时,请另起一行,用新的段落来表示。
4.严格要求根据节奏和强调进行段落分行:在需要控制故事的节奏或强调某些内容时,请另起一行,用新的段落来表示。
5.严格要求根据结构上的需要进行段落分行:在小说的结构需要换行时,比如出现列举、对比、并列时,请另起一行,用新的段落来表示。
6.严格要求根据<转折词>中的词语进行自动换行:在小说内容出现<转折词>中的词语时,请另起一行,用新的段落来表示。
7.请理解全文之后,根据上下文内容,严格按照以上要求进行段落分行,不要修改原文任何内容。
8.每段中不同的<句子>用逗号隔开,一段一行,连续输出,严格执行不要输出空行。
9.严格执行不要去改变原文的内容和顺序,不需要做数字编号排序标注。
## 转折词
因为;原先;事先;很久;以前;不久前;过不多久;随之;随后;接下来;曾几何时;顷刻之间;片刻;首先;其次;最后;第一;一则;再则;其一;进一步说;相应的;无独有偶;再说;此外;还有;更有甚者;此外;另外;补充一点;除此之外;其实;实际上;确切的说;老实讲;不瞒你说;说句心里话;要不;但是;不过;然偶;闲话少说;言归正传;要是;不管怎样;无论如何;无论;不论;要不是这样;否则;退一步说;自然;诚然;固然;当然;终于;果然;不出所料;果不其然;果真;难怪;怪不得;原来如此;所以;于是;因此;因而;这就是说;换句话说;也就是说;具体来说;具体地说;还有;另外;再说;补充一句;顺便说一下;附带说几句;总之;一句话;总的来看;总而言之;谁知;哪料到;突然;猛然间;岂料;岂知;竟然;偏偏;可惜;不料;没想到;殊不知;不用说;由此可见;显然;毫无疑问;可以肯定;这说明;同样;相比之下;与此相比;相反;反之;虽然;但是;尽管;转折常用的关联词;虽然;但是;尽管;还是;如果;要是;那么;即便;要是;一边;一边;一会儿;一会儿;既;又;不但;而且;不光;也;不仅;还;不但不;反而;是;还是;要么;要么;或者;与其;不如;宁可;宁愿;也不;决不;所以;之所以;是因为;由于;因此;既然;只要;只有;无论 ;不论;不管;任凭;首先;然后;首先;接着;不堪;居然;
## 示例
用户:
李明在图书馆里翻阅着一本旧书
突然,书页中掉出一张泛黄的信纸
信纸上写着一段神秘的文字
他好奇地读了起来
信中提到了一个古老的传说
传说中藏有宝藏的秘密
李明决定去探索这个传说
第二天,他来到了信中提到的地点
那里是一个废弃的古堡
古堡周围长满了杂草
李明小心翼翼地走进了古堡
古堡内部昏暗而阴森
他开始在古堡中寻找线索
突然,一阵风吹过
李明感到一阵寒意
他继续前进,发现了一扇隐藏的门
门后是一条通往地下的楼梯
李明沿着楼梯走下去
楼梯尽头是一个巨大的地下室
地下室里堆满了古老的箱子和物品
他开始一一检查这些箱子
就在这时,他听到了一阵奇怪的声音
李明停下手中的动作,仔细倾听
声音越来越近,他感到有些不安
突然,一个黑影从箱子后面窜了出来
李明吓得跳了起来
黑影原来是一只老鼠
他松了一口气,继续寻找
经过一番努力,他终于找到了一个古老的箱子
箱子上刻着奇异的图案
李明小心翼翼地打开了箱子
里面是一张地图和一些金币
他兴奋地拿起地图
地图上标记着宝藏的确切位置
李明决定继续他的探险之旅
他离开了古堡,踏上了寻找宝藏的路
AI:
1.李明在图书馆里翻阅着一本旧书,突然,书页中掉出一张泛黄的信纸,信纸上写着一段神秘的文字,他好奇地读了起来。
2.信中提到了一个古老的传说,传说中藏有宝藏的秘密,李明决定去探索这个传说,第二天,他来到了信中提到的地点,那里是一个废弃的古堡。
3.古堡周围长满了杂草,李明小心翼翼地走进了古堡,古堡内部昏暗而阴森,他开始在古堡中寻找线索,突然,一阵风吹过,李明感到一阵寒意。
4.他继续前进,发现了一扇隐藏的门,门后是一条通往地下的楼梯,李明沿着楼梯走下去,楼梯尽头是一个巨大的地下室,地下室里堆满了古老的箱子和物品。
5.他开始一一检查这些箱子,就在这时,他听到了一阵奇怪的声音,李明停下手中的动作,仔细倾听,声音越来越近,他感到有些不安。
6.突然,一个黑影从箱子后面窜了出来,李明吓得跳了起来,黑影原来是一只老鼠,他松了一口气,继续寻找。
7.经过一番努力,他终于找到了一个古老的箱子,箱子上刻着奇异的图案,李明小心翼翼地打开了箱子,里面是一张地图和一些金币。
8.他兴奋地拿起地图,地图上标记着宝藏的确切位置,李明决定继续他的探险之旅,他离开了古堡,踏上了寻找宝藏的路。
##
最后再强调,你作为一位优秀的小说编辑,每一次输出都要严格遵守<要求>,一步一步慢慢思考,参考<示例>的格式,一步一步思考,按顺序执行<要求>,不需要做解释说明,只呈现最后的结果,严格要求理解全文之后再进行分组,每组中不同的<句子>用逗号隔开,不要修改原文任何内容,无需要在输出用户,直接输出Ai部分,Ai部分前面一定要加上序号,严格执行不要去改变原文的内容和顺序,严格执行每一个你输出的序号后面完整的句子中的文字内容不少于15字不多于35字,连续输出,严格执行两句话中不要输出空行。
原文部分:
{textContent}
台词部分:
{textContent}`
}
]
}
@@ -0,0 +1,108 @@
export const AIWordMergeShort = {
model: 'deepseek-chat',
stream: false,
temperature: 0.3,
messages: [
{
role: 'system',
content:
'你是一位优秀的分镜师,你有很强的阅读能力,理解能力,我会给你一段文案,请把我给你的文案进行文案分镜头。根据上下文结合,按照镜头语言的可表达性,哪几句话可以组合成一个镜头,以便分镜的制作。严格遵守把组合好的文案分隔开发给我,不要发送与文案无关的内容。文案的格式为一行一个<句子>,一个镜头多句时逗号隔开,句末尾无标点,严格执行每一个完整的句子不少于15字不多于35字,请你理解全文之后,根据上下文内容按照以下要求段落分行:'
},
{
role: 'user',
content: `你是一位优秀的分镜师,你有很强的阅读能力,理解能力,我会给你一段文案,请把我给你的文案进行文案分镜头。根据上下文结合,按照镜头语言的可表达性,哪几句话可以组合成一个镜头,以便分镜的制作。严格遵守把组合好的文案分隔开发给我,不要发送与文案无关的内容。文案的格式为一行一个<句子>,一个镜头多句时逗号隔开,句末尾无标点,严格执行每一个完整的句子不少于15字不多于35字,请你理解全文之后,根据上下文内容按照以下要求段落分行:
## 要求
严格执行以下要求进行小说内容的段落分行:
1. 严格要求根据场景转换进行段落分行:当故事从一个场景切换到另一个场景时,请另起一行,用新的段落来表示。
2. 严格要求根据人物更替进行段落分行:当焦点从一个角色转移到另一个角色时,请另起一行,用新的段落来表示。
3. 严格要求根据心理活动或情感变化进行段落分行:角色的内心活动或情感变化时,请另起一行,用新的段落来表示。
4. 严格要求根据节奏和强调进行段落分行:在需要控制故事的节奏或强调某些内容时,请另起一行,用新的段落来表示。
5. 严格要求根据结构上的需要进行段落分行:在小说的结构需要换行时,比如出现列举、对比、并列时,请另起一行,用新的段落来表示。
6. 严格要求根据<转折词>中的词语进行自动换行:在小说内容出现<转折词>中的词语时,请另起一行,用新的段落来表示。
7. 请理解全文之后,根据上下文内容,严格按照以上要求进行段落分行,不要修改原文任何内容。
8. 每段中不同的<句子>用逗号隔开,一段一行,连续输出,严格执行不要输出空行。
9. 严格执行不要去改变原文的内容和顺序,不需要做数字编号排序标注。
## 转折词
因为;原先;事先;很久;以前;不久前;过不多久;随之;随后;接下来;曾几何时;顷刻之间;片刻;首先;其次;最后;第一;一则;再则;其一;进一步说;相应的;无独有偶;再说;此外;还有;更有甚者;此外;另外;补充一点;除此之外;其实;实际上;确切的说;老实讲;不瞒你说;说句心里话;要不;但是;不过;然偶;闲话少说;言归正传;要是;不管怎样;无论如何;无论;不论;要不是这样;否则;退一步说;自然;诚然;固然;当然;终于;果然;不出所料;果不其然;果真;难怪;怪不得;原来如此;所以;于是;因此;因而;这就是说;换句话说;也就是说;具体来说;具体地说;还有;另外;再说;补充一句;顺便说一下;附带说几句;总之;一句话;总的来看;总而言之;谁知;哪料到;突然;猛然间;岂料;岂知;竟然;偏偏;可惜;不料;没想到;殊不知;不用说;由此可见;显然;毫无疑问;可以肯定;这说明;同样;相比之下;与此相比;相反;反之;虽然;但是;尽管;转折常用的关联词;虽然;但是;尽管;还是;如果;要是;那么;即便;要是;一边;一边;一会儿;一会儿;既;又;不但;而且;不光;也;不仅;还;不但不;反而;是;还是;要么;要么;或者;与其;不如;宁可;宁愿;也不;决不;所以;之所以;是因为;由于;因此;既然;只要;只有;无论;不论;不管;任凭;首先;然后;首先;接着;不堪;居然;
## 示例
用户:
李明在图书馆里翻阅着一本旧书
突然,书页中掉出一张泛黄的信纸
信纸上写着一段神秘的文字
他好奇地读了起来
信中提到了一个古老的传说
传说中藏有宝藏的秘密
李明决定去探索这个传说
第二天,他来到了信中提到的地点
那里是一个废弃的古堡
古堡周围长满了杂草
李明小心翼翼地走进了古堡
古堡内部昏暗而阴森
他开始在古堡中寻找线索
突然,一阵风吹过
李明感到一阵寒意
他继续前进,发现了一扇隐藏的门
门后是一条通往地下的楼梯
李明沿着楼梯走下去
楼梯尽头是一个巨大的地下室
地下室里堆满了古老的箱子和物品
他开始一一检查这些箱子
就在这时,他听到了一阵奇怪的声音
李明停下手中的动作,仔细倾听
声音越来越近,他感到有些不安
突然,一个黑影从箱子后面窜了出来
李明吓得跳了起来
黑影原来是一只老鼠
他松了一口气,继续寻找
经过一番努力,他终于找到了一个古老的箱子
箱子上刻着奇异的图案
李明小心翼翼地打开了箱子
里面是一张地图和一些金币
他兴奋地拿起地图
地图上标记着宝藏的确切位置
李明决定继续他的探险之旅
他离开了古堡,踏上了寻找宝藏的路
AI:
1. 李明在图书馆里翻阅着一本旧书,突然,书页中掉出一张泛黄的信纸。
2. 信纸上写着一段神秘的文字,他好奇地读了起来。
3. 信中提到了一个古老的传说,传说中藏有宝藏的秘密。
4. 李明决定去探索这个传说。
5. 第二天,他来到了信中提到的地点,那里是一个废弃的古堡。
6. 古堡周围长满了杂草,李明小心翼翼地走进了古堡。
7. 古堡内部昏暗而阴森,他开始在古堡中寻找线索。
8. 突然,一阵风吹过,李明感到一阵寒意。
9. 他继续前进,发现了一扇隐藏的门。
10. 门后是一条通往地下的楼梯,李明沿着楼梯走下去。
11. 楼梯尽头是一个巨大的地下室,地下室里堆满了古老的箱子和物品。
12. 他开始一一检查这些箱子,就在这时,他听到了一阵奇怪的声音。
13. 李明停下手中的动作,仔细倾听。
14. 声音越来越近,他感到有些不安。
15. 突然,一个黑影从箱子后面窜了出来,李明吓得跳了起来。
16. 黑影原来是一只老鼠,他松了一口气,继续寻找。
17. 经过一番努力,他终于找到了一个古老的箱子,箱子上刻着奇异的图案。
18. 李明小心翼翼地打开了箱子,里面是一张地图和一些金币。
19. 他兴奋地拿起地图,地图上标记着宝藏的确切位置。
20. 李明决定继续他的探险之旅,他离开了古堡,踏上了寻找宝藏的路。
##
最后再强调,你作为一位优秀的小说编辑,每一次输出都要严格遵守<要求>,一步一步慢慢思考,参考<示例>的格式,一步一步思考,按顺序执行<要求>,不需要做解释说明,只呈现最后的结果,严格要求理解全文之后再进行分组,每组中不同的<句子>用逗号隔开,不要修改原文任何内容,无需要在输出用户,直接输出Ai部分,Ai部分前面一定要加上序号,严格执行不要去改变原文的内容和顺序,严格执行每一个你输出的序号后面完整的句子中的文字内容不少于15字不多于35字。
## 原文部分
{textContent}
## 台词部分
{textContent}`
}
]
}
+80
View File
@@ -0,0 +1,80 @@
export const apiDefineData = [
{
label: 'LAI API - 香港',
value: 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65',
id: 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65',
gpt_url: 'https://api.laitool.cc/v1/chat/completions',
mj_url: {
imagine: 'https://api.laitool.cc/mj/submit/imagine',
describe: 'https://api.laitool.cc/mj/submit/describe',
update_file: 'https://api.laitool.cc/mj/submit/upload-discord-images',
once_get_task: 'https://api.laitool.cc/mj/task/${id}/fetch'
},
d3_url: {
image: 'https://api.laitool.cc/v1/images/generations'
},
buy_url: 'https://api.laitool.cc/register?aff=RCSW'
},
{
label: 'LAI API - 美国',
value: '2b443f53-ba12-42b3-a57c-e4df92685c73',
id: '2b443f53-ba12-42b3-a57c-e4df92685c73',
gpt_url: 'https://laitool.net/v1/chat/completions',
mj_url: {
imagine: 'https://laitool.net/mj/submit/imagine',
describe: 'https://laitool.net/mj/submit/describe',
update_file: 'https://laitool.net/mj/submit/upload-discord-images',
once_get_task: 'https://laitool.net/mj/task/${id}/fetch'
},
d3_url: {
image: 'https://laitool.net/v1/images/generations'
},
buy_url: 'https://laitool.net/register?aff=RCSW'
}
]
/**
* 通过ID获取API配置
* @description 通过ID获取API配置
* @param id API ID
* @returns
*/
export function GetApiDefineDataById(id: string) {
let mj_api_url_index = apiDefineData.findIndex((item) => item.value == id)
if (mj_api_url_index == -1) {
throw new Error('没有找到对应的API的配置,请先检查配置')
}
return apiDefineData[mj_api_url_index]
}
/**
* 获取支持指定类型的API
* @param type
* @returns
*/
export function getAPIOptions(type: string) {
switch (type) {
case 'mj':
let options = apiDefineData
.filter((item) => item.mj_url != null)
.map((item) => {
return {
label: item.label,
value: item.value
}
})
return options
case 'gpt':
let gptOptions = apiDefineData
.filter((item) => item.gpt_url != null)
.map((item) => {
return {
label: item.label,
value: item.value
}
})
return gptOptions
default:
return []
}
}
View File
+127
View File
@@ -0,0 +1,127 @@
//#region 出图方式
import { PromptMergeType } from '../enum/bookEnum'
import { TaskModal } from '../model/task'
// 出图方式
export enum ImageCategory {
/** Midjourney 图像生成平台 */
Midjourney = 'mj',
/** Stable Diffusion 图像生成平台 */
Stable_Diffusion = 'sd',
/** ComfyUI 图像生成框架 */
Comfy_UI = 'comfyui',
/** DALL-E 图像生成模型 */
DALL_E_3 = 'd3',
/** FLUX API 图像生成服务 */
Flux_API = 'flux-api',
/** FLUX FORGE 图像生成工具 */
Flux_Forge = 'flux-forge'
}
/**
* 根据图像生成平台类型获取对应的提示词合并方式
*
* 该函数将不同的图像生成平台映射到其对应的提示词合并类型,以便系统能够正确处理
* 各平台特定的提示词格式要求。例如,Midjourney和Stable Diffusion对提示词有不同的格式要求。
*
* @param {ImageCategory} imageCategory - 图像生成平台类型,如Midjourney、Stable Diffusion等
* @returns {PromptMergeType} 对应的提示词合并类型
* - MJ_MERGE: 适用于Midjourney的提示词格式
* - SD_MERGE: 适用于Stable Diffusion、ComfyUI、DALL-E 3、FLUX相关平台的提示词格式
*
* @example
* // 获取Midjourney的提示词合并类型
* const mjMergeType = getMergePromptType(ImageCategory.Midjourney);
* // 返回 PromptMergeType.MJ_MERGE
*
* // 获取Stable Diffusion的提示词合并类型
* const sdMergeType = getMergePromptType(ImageCategory.Stable_Diffusion);
* // 返回 PromptMergeType.SD_MERGE
*/
export function getMergePromptType(imageCategory: ImageCategory): PromptMergeType {
switch (imageCategory) {
case ImageCategory.Midjourney:
return PromptMergeType.MJ_MERGE
case ImageCategory.Stable_Diffusion:
return PromptMergeType.SD_MERGE
case ImageCategory.Comfy_UI:
return PromptMergeType.SD_MERGE
case ImageCategory.DALL_E_3:
return PromptMergeType.SD_MERGE
case ImageCategory.Flux_API:
return PromptMergeType.SD_MERGE
case ImageCategory.Flux_Forge:
return PromptMergeType.SD_MERGE
default:
return PromptMergeType.MJ_MERGE
}
}
/**
* 获取所有出图方式的选项列表
* @returns 包含label和value的选项数组
*/
export function getImageCategoryOptions(): Array<{ label: string; value: string }> {
return [
{ label: 'Midjourney', value: ImageCategory.Midjourney },
{ label: 'Stable Diffusion', value: ImageCategory.Stable_Diffusion },
{ label: 'ComfyUI', value: ImageCategory.Comfy_UI },
{ label: 'DALL-E', value: ImageCategory.DALL_E_3 },
{ label: 'FLUX API', value: ImageCategory.Flux_API },
{ label: 'FLUX FORGE', value: ImageCategory.Flux_Forge }
]
}
/**
* 获取出图方式的标签
* @param value
* @returns
*/
export function getImageCategoryLabel(value: string): TaskModal.TaskStatus {
const options = getImageCategoryOptions()
const option = options.find((option) => option.value === value)
if (option) {
return {
status: option.value,
label: option.label,
type: 'success'
} as TaskModal.TaskStatus
} else {
return {
status: 'UNKNOWN',
label: 'UNKNOWN',
type: 'warning'
}
}
}
//#endregion
//#region 图转视频方式
export enum ImageToVideoCategory {
/** runway 生成视频 */
RUNWAY = 'runway',
/** luma 生成视频 */
LUMA = 'luma',
/** 可灵生成视频 */
KLING = 'kling',
/** Pika 生成视频 */
PIKA = 'pika'
}
/**
* 获取所有出图方式的选项列表
* @returns 包含label和value的选项数组
*/
export function getImageToVideoCategoryOptions(): Array<{ label: string; value: string }> {
return [
{ label: 'Runway', value: ImageToVideoCategory.RUNWAY },
{ label: 'Luma', value: ImageToVideoCategory.LUMA },
{ label: '可灵', value: ImageToVideoCategory.KLING },
{ label: 'Pika', value: ImageToVideoCategory.PIKA }
]
}
//#endregion
+213
View File
@@ -0,0 +1,213 @@
//#region MJ 出图方式
/**
* MJ 图像生成方式,目前仅支持 API 模式
*/
export enum ImageGenerateMode {
/** API 模式 */
MJ_API = 'mj_api',
//本地MJ
LOCAL_MJ = 'local_mj',
// 代理MJ
REMOTE_MJ = 'remote_mj',
// 浏览器模式
BROWSER_MJ = 'browser_mj',
// 本地 SD
LOCAL_SD = 'local_sd',
// ComfyUI
ComfyUI = 'comfyui',
// flux-api
FLUX_API = 'flux-api',
// flxu-forge
FLUX_FORGE = 'flux-forge',
// 导入
IMPORT = 'import'
}
/**
* 获取所有出图方式的选项列表
* @description 获取所有出图方式的选项列表
* @returns
*/
export function getImageGenerateModeOptions(): Array<{ label: string; value: string }> {
return [{ label: 'API模式', value: ImageGenerateMode.MJ_API }]
}
//#endregion
//#region 生图机器人
export enum MJRobotType {
// MJ
MJ = 'mj',
// niji
NIJI = 'niji'
}
/**
* 获取MJ的机器人列表
* @returns
*/
export function getMJRobotOptions() {
return [
{
label: 'MJ',
value: MJRobotType.MJ
},
{
label: 'NIJI',
value: MJRobotType.NIJI
}
]
}
//#endregion
//#region 机器人出图模型
/**
* 获取机器人对应的出图模型
*/
export function getMJRobotModelOptions(mjRobot?: MJRobotType) {
let allRobotModel = [
{
label: 'MJ V7.0',
text: 'v 7',
type: MJRobotType.MJ,
value: '0d33ae62-e0a8-4429-89e4-304bfd20cd6f'
},
{
label: 'MJ V6.1',
text: 'v 6.1',
type: MJRobotType.MJ,
value: 'f799de81-91da-413f-89fd-58b2ff8174db'
},
{
label: 'MJ V6.0',
text: 'v 6',
type: MJRobotType.MJ,
value: '3e6473ab-9a64-4574-9a38-f5c75af552b6'
},
{
label: 'MJ V5.2',
text: 'v 5.2',
type: MJRobotType.MJ,
value: '27a0d30e-f46c-4684-96c8-d91334deb94f'
},
{
label: 'MJ V5.1',
text: 'v 5.1',
type: MJRobotType.MJ,
value: 'e1226715-e969-44c4-b18b-f2ad5dae5d2f'
},
{
label: 'MJ V5.0',
text: 'v 5',
type: MJRobotType.MJ,
value: 'afb7bea1-4eda-46ea-8165-34701b4566bf'
},
{
label: 'MJ V4.0',
text: 'v 4',
type: MJRobotType.MJ,
value: 'd05b8497-7f4a-4890-8fac-89f1803984d2'
},
{
label: 'NIJI V6',
text: 'niji 6',
type: MJRobotType.NIJI,
value: '99377cad-c103-4cee-a958-86a104879328'
},
{
label: 'NIJI V5',
text: 'niji 5',
type: MJRobotType.NIJI,
value: '53cec077-9885-4635-ab18-e021066b2c4c'
},
{
label: 'NIJI V4',
text: 'niji 4',
type: MJRobotType.NIJI,
value: '6a7199fe-6e0d-40a9-9772-b5eb3d2e2e66'
}
]
switch (mjRobot) {
case MJRobotType.MJ:
return allRobotModel.filter((item) => item.type == MJRobotType.MJ)
case MJRobotType.NIJI:
return allRobotModel.filter((item) => item.type == MJRobotType.NIJI)
default:
return allRobotModel
}
}
//#endregion
//#region MJ 出图比例
/**
* 获取MJ出图的比例Options
* @returns
*/
export function getMJImageScaleOptions() {
return [
{
label: '1:1',
value: '3e2772f2-041c-49c6-ba13-d0ed120310b8'
},
{
label: '4:3',
value: 'fcef555c-1958-4082-88fe-434782aa8151'
},
{
label: '3:4',
value: '13f71d53-73a3-4c9b-9c1e-6e7e939aee73'
},
{
label: '16:9',
value: 'bf33ce1a-15cd-4901-b38e-89543cf14a1f'
},
{
label: '9:16',
value: 'fd4641e2-97f4-4a86-8616-4965e05f3348'
}
]
}
//#endregion
//#region 出图速率
export enum MJSpeed {
// 快速
FAST = 'FAST',
// 休闲
RELAX = 'RELAXED'
}
/**
* 获取MJ的速度Options
* @returns
*/
export function getMJSpeedOptions() {
return [
{
label: '快速',
value: MJSpeed.FAST
},
{
label: '慢速',
value: MJSpeed.RELAX
}
]
}
//#endregion
+53
View File
@@ -0,0 +1,53 @@
/**
* 预设数据类型
*/
export enum PresetCategory {
/** 角色 */
Character = 'character',
/** 场景 */
Scene = 'scene',
/** 风格 */
Style = 'style',
/** 其他 */
Other = 'other'
}
/**
* 获取所有预设数据类型的选项列表
* @returns 包含label和value的选项数组
*/
export function getPresetCategoryOptions(): Array<{ label: string; value: string }> {
return [
{ label: '风格预设', value: PresetCategory.Style },
{ label: '人物预设', value: PresetCategory.Character },
{ label: '场景预设', value: PresetCategory.Scene }
]
}
/**
* 获取预设数据类型的标签
* @param value 预设数据类型的值
* @returns 对应的标签,如果未找到则返回空字符串
*/
export function getPresetCategoryLabel(value: string): string {
const option = getPresetCategoryOptions().find((item) => item.value === value)
return option ? option.label : '未知类型'
}
/**
* 获取提示词的标签
* @param value 提示词的值
* @returns 对应的标签,如果未找到则返回'未知类型'
*/
export function getPromptSortLabel(value: string): string {
const option = getPresetCategoryOptions().find((item) => item.value === value)
if (option) {
return option.label
} else {
if (value == 'prompt') {
return '提示词'
} else {
return '未知类型'
}
}
}
+54
View File
@@ -0,0 +1,54 @@
interface ISoftwareData {
/** 软件版本号 */
version: string
/** 发布日期 */
date: string
/** 更新说明列表 */
notes: string[]
/** 系统信息 */
systemInfo: {
/** 快速开始 */
quickStart: string
/** 使用文档 */
documentationUrl: string
/** 更新文档 */
updateUrl: string
/** 软件文档 */
softwareUrl: string
/** WIKI */
wikiUrl: string
}
}
export const SoftwareData: ISoftwareData = {
version: 'V3.4.2',
date: '2025-08-08',
notes: [
'1. 新增图/文转视频菜单界面,专注实现图/文转视频(目前只集成了 MJ VIDEO)',
' • 全新的界面排列,小说列表和批次任务更加分明',
' • 添加转视频进度,在主界面即可看到转视频的比例',
' • 单独的界面去处理图转视频,避免表格数据过多繁琐',
' • 新增分页显示,界面加载更快,也可切换不分页',
' • 单独操作面板,参数修改处理更加清晰,支持多种模式显示',
' • 批量设置转视频配置,可以批量修改分类',
' • 友好的选择视频界面',
'2. 重写软件导出剪映,修复若干草稿导出问题',
' • 修复导出剪映文案和图片对齐问题,解决时长越长越明显的对不上问题',
' • 修复导出草稿关键帧部分问题',
' • 导出的文案通过分镜自动导入,不再需要手动选择SRT',
'3. 美化生成草稿界面弹窗,优化部分逻辑',
' • 删除选择SRT文件,SRT根据聚合推文中导入的SRT自动生成草稿',
' • 只需选择配音文件即可,配音文件和导入的SRT请自行对应',
' • 背景音乐不在内部设置,自行选择文件夹或者是MP3、WAV文件',
' • 背景音乐选择文件夹则读取文件夹,随机获取一个',
' • 背景音乐选择指定的音乐文件则使用选择的'
],
systemInfo: {
quickStart: '快速开始',
documentationUrl: 'https://rvgyir5wk1c.feishu.cn/wiki/WdaWwAfDdiLOnjkywIgcaQoKnog',
updateUrl: 'https://pvwu1oahp5m.feishu.cn/docx/CAjGdTDlboJ3nVx0cQccOuNHnvd',
softwareUrl: 'https://pvwu1oahp5m.feishu.cn/docx/FONZdfnrOoLlMrxXHV0czJ3jnkd',
wikiUrl:
'https://rvgyir5wk1c.feishu.cn/wiki/space/7481893355360190492?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home'
}
}
+64
View File
@@ -0,0 +1,64 @@
// @ts-ignore
import Realm from 'realm'
import { BookType } from '@/define/enum/bookEnum'
export class BookModel extends Realm.Object<BookModel> {
id!: string
no!: number
name!: string
bookFolderPath!: string
imageFolder!: string | undefined
type!: BookType
oldVideoPath!: string | undefined
srtPath!: string | undefined
audioPath!: string | undefined
draftDepend?: string // 草稿依赖
draftSrtStyle!: string | undefined // 草稿字幕样式
backgroundMusic!: string | undefined // 背景音乐ID
friendlyReminder!: string | undefined // 友情提示
updateTime!: Date
createTime!: Date
version!: string
imageStyle!: string[] | undefined // 软件内置的样式
autoAnalyzeCharacter!: string | undefined // 自动分析角色设置
customizeImageStyle!: string[] | undefined // 自定义的样式
videoConfig!: string | undefined // 合成视频设置
prefixPrompt!: string | undefined // 前缀
suffixPrompt!: string | undefined // 后缀
subtitlePosition!: string | undefined // 字幕位置
watermarkPosition!: string | undefined // 水印位置,一个json数组字符串
imageType!: string | undefined // 出图类型
static schema: Realm.ObjectSchema = {
name: 'Book',
properties: {
id: 'string',
no: 'int',
name: 'string',
bookFolderPath: 'string',
type: 'string',
oldVideoPath: 'string?',
srtPath: 'string?',
audioPath: 'string?',
draftDepend: 'string?',
draftSrtStyle: 'string?',
backgroundMusic: 'string?',
friendlyReminder: 'string?',
imageFolder: 'string?',
updateTime: 'date',
createTime: 'date',
version: 'string',
imageStyle: 'string?[]',
autoAnalyzeCharacter: 'string?',
customizeImageStyle: 'string?[]',
videoConfig: 'string?',
prefixPrompt: 'string?',
suffixPrompt: 'string?',
subtitlePosition: 'string?',
watermarkPosition: 'string?',
imageType: 'string?'
},
// 主键为_id
primaryKey: 'id'
}
}
+104
View File
@@ -0,0 +1,104 @@
import { ImageCategory } from '@/define/data/imageData'
import { BookTaskStatus } from '@/define/enum/bookEnum'
import Realm, { ObjectSchema } from 'realm'
export class ImageDefineModel extends Realm.Object<ImageDefineModel> {
label!: string
key!: string
value!: string
children!: string
type!: string
prompt!: string
image_url!: string
cref_cw!: number
lora!: string
chinese_prompt!: string
lora_weight!: number
show_image!: string
isShow!: true
static schema: ObjectSchema = {
name: 'ImageDefine',
properties: {
label: 'string',
key: 'string',
value: 'string',
children: 'string',
type: 'string',
prompt: 'string',
image_url: 'string',
cref_cw: 'int',
lora: 'string',
chinese_prompt: 'string',
lora_weight: 'int',
show_image: 'string',
isShow: 'bool'
},
primaryKey: 'key'
}
}
export class BookTaskModel extends Realm.Object<BookTaskModel> {
id!: string
no!: number
bookId!: string
name!: string
generateVideoPath!: string | null
srtPath!: string | null
draftDepend?: string // 草稿依赖
audioPath!: string | null
draftSrtStyle!: string | null // 草稿字幕样式
backgroundMusic!: string | null // 背景音乐ID
friendlyReminder!: string | null // 友情提示
imageFolder!: string | null
imageStyle!: string | null // 软件内置的样式
cacheImageList!: string[] | null // 缓存的图片列表
autoAnalyzeCharacter!: string | null // 自动分析角色设置
customizeImageStyle!: string[] | null // 自定义的样式
videoConfig!: string | null // 合成视频设置
prefixPrompt!: string | null // 前缀
suffixPrompt!: string | null // 后缀
styleList!: string[] | null
status!: BookTaskStatus
errorMsg!: string | null
isAuto!: boolean // 是否自动
openVideoGenerate!: boolean | null // 是否开启视频生成
updateTime!: Date
createTime!: Date
imageCategory!: ImageCategory // 图片出图方式
subImageFolder!: string[] | null // 出图的子文件夹
static schema: ObjectSchema = {
name: 'BookTask',
properties: {
id: 'string',
bookId: { type: 'string', indexed: true },
no: 'int',
name: 'string',
generateVideoPath: 'string?',
srtPath: 'string?',
draftDepend: 'string?',
audioPath: 'string?',
draftSrtStyle: 'string?',
backgroundMusic: 'string?',
friendlyReminder: 'string?',
imageFolder: 'string?',
subImageFolder: 'string?[]',
cacheImageList: 'string?[]',
imageStyle: 'string?[]',
autoAnalyzeCharacter: 'string?',
customizeImageStyle: 'string?[]',
videoConfig: 'string?',
prefixPrompt: 'string?',
suffixPrompt: 'string?',
status: 'string',
openVideoGenerate: 'bool?',
errorMsg: 'string?',
isAuto: 'bool',
updateTime: 'date',
createTime: 'date',
imageCategory: 'string'
},
// 主键为_id
primaryKey: 'id'
}
}
+236
View File
@@ -0,0 +1,236 @@
import Realm, { ObjectSchema } from 'realm'
import { BookTaskStatus, MJAction } from '@/define/enum/bookEnum'
import { MJImageType } from '@/define/enum/mjEnum'
export class Subtitle extends Realm.Object<Subtitle> {
startTime!: number
endTime!: number
srtValue!: string
id!: string
static schema = {
name: 'Subtitle',
properties: {
startTime: 'int',
endTime: 'int',
srtValue: 'string',
id: 'string'
},
primaryKey: 'id'
}
}
export class MJMessage extends Realm.Object<MJMessage> {
id!: string
mjApiUrl!: string | null
progress!: number
category!: MJImageType
imageClick!: string | null // 图片点击(显示的小的)
imageShow!: string | null // 图片实际的地址
messageId!: string // 消息ID(可以是MJ中的,也可以是API中的)
action!: MJAction // 动作(生图,反推之类)
status!: string // 状态
message!: string | null // 消息
static schema: ObjectSchema = {
name: 'MJMessage',
properties: {
id: 'string',
mjApiUrl: 'string?',
progress: 'int',
category: 'string',
imageClick: 'string?',
imageShow: 'string?',
messageId: 'string',
action: 'string',
status: 'string',
message: 'string?'
},
primaryKey: 'id'
}
}
export class VideoMessage extends Realm.Object<VideoMessage> {
id!: string
msg!: string | null
videoType!: string
prompt!: string | null
style!: string | null
imageUrl!: string | null
model!: string | null
bookTaskDetailId!: string
status!: string | null
videoUrl!: string | null
taskId!: string | null
runwayOptions!: string | null // 生成视频的一些设置
lumaOptions!: string | null // 生成视频的一些设置
klingOptions!: string | null // 生成视频的一些设置
messageData!: string | null
static schema: ObjectSchema = {
name: 'VideoMessage',
properties: {
id: 'string',
msg: 'string?',
videoType: 'string',
bookTaskDetailId: 'string?',
prompt: 'string?',
style: 'string?',
imageUrl: 'string?',
model: 'string?',
status: 'string?',
videoUrl: 'string?',
taskId: 'string?',
runwayOptions: 'string?',
lumaOptions: 'string?',
klingOptions: 'string?',
messageData: 'string?'
},
primaryKey: 'id'
}
}
export class WebuiConfig extends Realm.Object<WebuiConfig> {
sampler_name!: string // 采样器名称
negative_prompt!: string // 负面提示
batch_size!: number // 批次大小
steps!: number // 步数
cfg_scale!: number // 提示词相关性
denoising_strength!: number // 降噪强度
width!: number // 宽度
height!: number // 高度
seed!: number // 种子
init_images!: string // 初始图片(垫图的图片地址)
id!: string
static schema: ObjectSchema = {
name: 'WebuiConfig',
properties: {
sampler_name: 'string',
negative_prompt: 'string',
batch_size: 'int',
steps: 'int',
cfg_scale: 'int',
denoising_strength: 'int',
width: 'int',
height: 'int',
seed: 'int',
init_images: 'string',
id: 'string'
},
primaryKey: 'id'
}
}
export class SDConfig extends Realm.Object<SDConfig> {
checkpoints!: string // 大模型
api!: string // api地址
model!: string // 生图方式
webuiConfig!: WebuiConfig
id!: string
static schema: ObjectSchema = {
name: 'SDConfig',
properties: {
checkpoints: 'string',
api: 'string',
model: 'string',
webuiConfig: 'WebuiConfig',
id: 'string'
},
primaryKey: 'id'
}
}
// 放反推的提示词的对象
export class ReversePrompt extends Realm.Object<ReversePrompt> {
id!: string
bookTaskDetailId!: string
prompt!: string
promptCN!: string
isSelect!: boolean
static schema: ObjectSchema = {
name: 'ReversePrompt',
properties: {
id: 'string',
bookTaskDetailId: 'string',
prompt: 'string',
promptCN: 'string',
isSelect: 'bool'
},
primaryKey: 'id'
}
}
export class BookTaskDetailModel extends Realm.Object<BookTaskDetailModel> {
id!: string
no!: number
name!: string
bookId!: string
bookTaskId!: string
videoPath!: string | null // 视频地址
generateVideoPath!: string | null // 生成视频地址
audioPath!: string | null // 音频地址
word!: string | null // 文案
oldImage!: string | null // 旧图片(用于SD的图生图)
afterGpt!: string | null // GPT生成的文案
startTime!: number | null // 开始时间
endTime!: number | null // 结束时间
timeLimit!: string | null // 事件实现(0 -- 3000
subValue!: string | null // 包含的字幕数据
characterTags!: string[] | null // 角色标签
sceneTags!: string[] | null // 场景标签
styleTags!: string[] | null // 风格标签
promptSort!: string | null // 提示词排序
gptPrompt!: string | null // GPT提示词
mjMessage!: MJMessage | null // MJ消息
videoMessage!: VideoMessage | null // 视频消息
outImagePath!: string | null // 输出图片地址
subImagePath!: string[] | null // 子图片地址
imageLock!: boolean // 图片锁
prompt!: string | null // 提示
adetailer!: boolean // 是否开启修脸
sdConifg!: SDConfig | null // SD配置
reversePrompt!: ReversePrompt[] | null // 反推的提示词(数组)
subtitlePosition!: string | null // 字幕位置
status!: BookTaskStatus
createTime!: Date
updateTime!: Date
static schema: ObjectSchema = {
name: 'BookTaskDetail',
properties: {
id: 'string',
no: 'int',
name: 'string',
bookId: { type: 'string', indexed: true },
bookTaskId: { type: 'string', indexed: true },
videoPath: 'string?',
generateVideoPath: 'string?', // 生成视频地址
audioPath: 'string?',
word: 'string?',
oldImage: 'string?',
afterGpt: 'string?',
startTime: 'int?',
endTime: 'int?',
timeLimit: 'string?',
subValue: 'string?',
reversePrompt: { type: 'list', objectType: 'ReversePrompt' },
characterTags: { type: 'list', objectType: 'string' },
sceneTags: 'string[]',
styleTags: 'string[]',
promptSort: 'string?',
gptPrompt: 'string?',
mjMessage: 'MJMessage?',
videoMessage: 'VideoMessage?',
outImagePath: 'string?',
subImagePath: 'string[]',
imageLock: 'bool',
prompt: 'string?',
adetailer: 'bool',
sdConifg: 'SDConfig?',
subtitlePosition: 'string?',
status: 'string',
createTime: 'date',
updateTime: 'date'
},
// 主键为_id
primaryKey: 'id'
}
}
+16
View File
@@ -0,0 +1,16 @@
import Realm, { ObjectSchema } from 'realm'
export class OptionModel extends Realm.Object<OptionModel> {
key!: string;
value!: string;
type!: string;
static schema: ObjectSchema = {
name: 'Option',
properties: {
key: 'string',
value: 'string',
type: 'string'
},
primaryKey: 'key'
}
}
+38
View File
@@ -0,0 +1,38 @@
import Realm, { ObjectSchema } from 'realm'
export class PresetModel extends Realm.Object<PresetModel> {
id!: string
label!: string
type!: string
showImage?: string[]
prompt!: string
chinesePrompt?: string[]
imageUrl?: string
srefSw?: number
crefCw?: number
lora?: string
loraWeight?: number
isShow!: boolean
aliases?: string[]
createTime!: Date
static schema: ObjectSchema = {
name: 'Preset',
properties: {
id: 'string',
label: 'string',
type: 'string',
showImage: 'string?[]',
imageUrl: 'string?',
prompt: 'string',
chinesePrompt: 'string?',
srefSw: 'int?',
crefCw: 'int?',
lora: 'string?',
loraWeight: 'int?',
isShow: 'bool',
aliases: 'string?[]',
createTime: 'date'
},
primaryKey: 'id'
}
}
+40
View File
@@ -0,0 +1,40 @@
import Realm, { ObjectSchema } from 'realm'
import { BookBackTaskStatus, BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
export class TaskListModel extends Realm.Object<TaskListModel> {
id!: string
bookId!: string
bookTaskId!: string
bookTaskDetailId!: string
name!: string // 任务名称,小说名+批次名+分镜名
type!: BookBackTaskType
status!: BookBackTaskStatus
errorMessage!: string | null
executeType!: TaskExecuteType // 任务执行类型,手动还是自动
createTime!: Date
updateTime!: Date
startTime!: number
endTime!: number
messageName?: string
static schema: ObjectSchema = {
name: 'TaskList',
properties: {
id: 'string',
bookId: { type: 'string', indexed: true },
bookTaskId: { type: 'string', indexed: true },
bookTaskDetailId: { type: 'string', indexed: true },
name: 'string',
type: 'string',
status: 'string',
errorMessage: 'string?',
executeType: { type: 'string', default: TaskExecuteType.AUTO },
createTime: 'date',
updateTime: 'date',
startTime: 'int',
endTime: 'int',
messageName: 'string?'
},
primaryKey: 'id'
}
}
+36
View File
@@ -0,0 +1,36 @@
// 定义抽象基类
import Realm from 'realm'
export abstract class BaseService {
protected realm: Realm | null = null
// 抽象类的构造函数应该是protected,以防止外部直接实例化
protected constructor() {
// 构造函数逻辑,使用someValue进行初始化
}
// 定义抽象方法,子类必须实现,打开数据库连接
abstract open(dbPath: string): void
// 关闭数据库连接
close(): void {
// 关闭数据库的连接,防止内存溢出
// 实现关闭数据库连接的逻辑
if (this.realm != null) {
console.log('Closing database connection')
this.realm.close()
this.realm = null // 清理引用,确保垃圾回收
}
}
transaction(func: () => unknown): void {
if (this.realm != null) {
// 判断当前的relam是不是在一个事务中
if (this.realm.isInTransaction) {
func()
} else {
this.realm.write(() => {
func()
})
}
}
}
}
+79
View File
@@ -0,0 +1,79 @@
import Realm from 'realm'
import { BaseService } from './baseService'
import path from 'path'
import { BookModel } from '../../model/book'
import {
BookTaskDetailModel,
MJMessage,
ReversePrompt,
SDConfig,
Subtitle,
VideoMessage,
WebuiConfig
} from '../../model/bookTaskDetail'
import { TaskListModel } from '../../model/taskList'
import { OptionModel } from '../../model/options'
import { app } from 'electron'
import { BookTaskModel } from '../../model/bookTask'
import { PresetModel } from '../../model/preset'
// Determine database path based on environment
const isDev = !app.isPackaged
let dbPath = isDev
? path.resolve(process.cwd(), 'Database/option.realm') // Development path
: path.resolve(app.getPath('userData'), 'Database/option.realm') // Production path
// 版本迁移
const migration = (_oldRealm: Realm, _newRealm: Realm) => {}
export class RealmBaseService extends BaseService {
static instance: RealmBaseService | null = null
protected realm: Realm | null = null
dbpath: string
protected constructor() {
super()
this.dbpath = dbPath
}
public static async getInstance() {
if (RealmBaseService.instance === null) {
RealmBaseService.instance = new RealmBaseService()
await RealmBaseService.instance.open()
}
return RealmBaseService.instance
}
/**
* 创建数据库连接,如果已经存在则直接返回
* @returns
*/
async open() {
try {
if (this.realm != null) return
// 判断当前全局是不是又当前这个
const config = {
schema: [
BookModel,
BookTaskDetailModel,
TaskListModel,
OptionModel,
MJMessage,
VideoMessage,
WebuiConfig,
SDConfig,
ReversePrompt,
Subtitle,
BookTaskModel,
PresetModel
],
path: this.dbpath,
schemaVersion: 19, // 数据库版本号,修改时需要增加
migration: migration
}
this.realm = await Realm.open(config)
} catch (error) {
throw error
}
}
}
+365
View File
@@ -0,0 +1,365 @@
import { Book } from '@/define/model/book/book'
import { RealmBaseService } from '../base/realmBase'
import Realm, { UpdateMode } from 'realm'
import { isEmpty } from 'lodash'
import { BookModel } from '../../model/book'
import path from 'path'
import { BookTaskStatus, BookType } from '@/define/enum/bookEnum'
import { CheckFolderExistsOrCreate, CopyFileOrFolder } from '@/define/Tools/file'
import { ImageCategory } from '@/define/data/imageData'
import { getGeneralSetting, getProjectPath } from '@/main/service/option/optionCommonService'
import { SrtHandle } from '@/main/service/common/srtHandle'
import { BookTaskDetailService } from './bookTaskDetailService'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
export class BookService extends RealmBaseService {
static instance: BookService | null = null
declare realm: Realm
private constructor() {
super()
}
/**
* 获取当前实例对象,为空则创建一个新的
* @returns
*/
public static async getInstance() {
if (BookService.instance === null) {
BookService.instance = new BookService()
await super.getInstance()
}
await BookService.instance.open()
return BookService.instance
}
/**
* 获取小说信息,没有找到返回null
* @param bookId
*/
async GetBookDataById(bookId: string): Promise<Book.SelectBook | null> {
try {
if (isEmpty(bookId)) {
throw new Error('获取小说信息失败,缺少小说ID')
}
let projectPath: string = await getProjectPath()
let books = this.realm.objects<BookModel>('Book').filtered('id = $0', bookId)
if (books.length <= 0) {
return null
} else {
// 对返回的数据进行处理
let resBooks = Array.from(books).map((book) => {
// 这里可以直接操作普通对象
let bookObj = {
...book,
bookFolderPath: path.resolve(projectPath, book.bookFolderPath.replace(/\\/g, '/')),
oldVideoPath: book.oldVideoPath
? path.resolve(projectPath, book.oldVideoPath.replace(/\\/g, '/'))
: '',
imageFolder: book.imageFolder
? path.resolve(projectPath, book.imageFolder.replace(/\\/g, '/'))
: ''
}
return bookObj
})
return resBooks[0]
}
} catch (error) {
throw error
}
}
/**
* 获取小说信息,通过参数查询
* @returns
*/
async GetBookDataCondition(
bookQuery: Book.QueryBookCondition
): Promise<Book.QueryBookConditionResponse> {
try {
// 获取所有的小说数据,并进行时间降序排序
let books = this.realm.objects<BookModel>('Book')
let bookLength = books.length
// 开始开始筛选
if (bookQuery.id) {
// 查询对应的小说ID的数据
books = books.filtered('id = $0', bookQuery.id)
}
if (bookQuery.name) {
// 查询对应的小说名字的数据
books = books.filtered('name CONTAINS $0', bookQuery.name)
}
if (bookQuery.type) {
// 查询对应的小说类型的数据
books = books.filtered('type = $0', bookQuery.type)
}
books = books.sorted('updateTime', true)
// 判断是不是有page和pageSize,有的话对查询返回的信息做分页
if (bookQuery.page && bookQuery.pageSize) {
books = books.slice(
(bookQuery.page - 1) * bookQuery.pageSize,
bookQuery.page * bookQuery.pageSize
) as unknown as Realm.Results<BookModel>
}
if (books.length <= 0) {
return {
resultBooks: [],
bookLength: 0
}
}
let projectPath: string = await getProjectPath()
// 将realm对象数组转换为普通对象数组
let res_book = Array.from(books).map((book) => {
// 这里可以直接操作普通对象
let bookObj = {
...book,
bookFolderPath: path.resolve(projectPath, book.bookFolderPath.replace(/\\/g, '/')),
oldVideoPath: book.oldVideoPath
? path.resolve(projectPath, book.oldVideoPath.replace(/\\/g, '/'))
: '',
imageFolder: book.imageFolder
? path.resolve(projectPath, book.imageFolder.replace(/\\/g, '/'))
: '',
imageStyle: book.imageStyle ? Array.from(book.imageStyle) : [],
customizeImageStyle: book.customizeImageStyle ? Array.from(book.customizeImageStyle) : []
} as Book.SelectBook
return bookObj
})
return {
resultBooks: res_book,
bookLength
}
} catch (error) {
throw error
}
}
/**
* 新增或者是修小说数据
* @param {*} book 小说信息
* @returns
*/
async AddOrModifyBook(book: Book.SelectBook): Promise<Book.SelectBook> {
try {
if (book == null) {
throw new Error('小说数据为空,无法修改')
}
// 当小说的类型是反推的时候,必须传入视频
if (book.type == BookType.MJ_REVERSE || book.type == BookType.SD_REVERSE) {
// 判断视频是否存在
if (book.oldVideoPath == null || book.oldVideoPath == '') {
throw new Error('反推必须传入视频')
}
}
let projectPath: string = await getProjectPath()
if (book.id == null) {
// 新增
// 判断指定的名字在数据库中是否存在
let books = this.realm.objects('Book').filtered('name = $0', book.name)
if (books.length > 0) {
throw new Error(`小说名字 ${book.name} 已经存在,请更换小说名字`)
}
console.log(this)
// 新增数据
book.id = crypto.randomUUID()
book.createTime = new Date()
book.updateTime = new Date()
// 检查传入的视频文件是不是存在
// 获取当前最大的no
let maxNo = this.realm.objects('Book').max('no')
book.no = maxNo == null ? 1 : Number(maxNo) + 1
// 拼接项目文件夹
book.bookFolderPath = book.id
book.imageFolder = `${book.id}/tmp`
let bookFolderPath = path.resolve(projectPath, book.id)
let imageFolder = path.resolve(projectPath, `${book.id}/tmp`)
let oldVideoPath = path.resolve(projectPath, `${book.id}/data/${book.id}.mp4`)
let bookTaskImageFolder = path.resolve(imageFolder, `${book.name}_00001`)
// 将视频拷贝一个到项目文件下面
if (book.oldVideoPath) {
await CopyFileOrFolder(book.oldVideoPath, oldVideoPath)
}
// 创建对应的文件夹
await CheckFolderExistsOrCreate(bookFolderPath)
await CheckFolderExistsOrCreate(imageFolder)
await CheckFolderExistsOrCreate(bookTaskImageFolder) // 创建默认的任务文件夹
// 修改数据
book.oldVideoPath = path.relative(projectPath, oldVideoPath)
if (book.type == BookType.ORIGINAL) {
book.oldVideoPath = undefined
}
let imageCategory = ImageCategory.Midjourney
if (book.type == BookType.SD_REVERSE) {
imageCategory = ImageCategory.Stable_Diffusion
} else if (book.type == BookType.MJ_REVERSE) {
imageCategory = ImageCategory.Midjourney
} else if (book.type == BookType.ORIGINAL) {
let generalSetting = await getGeneralSetting()
imageCategory = generalSetting.defaultImgGenMethod ?? ImageCategory.Midjourney
} else {
throw new Error('未知的小说类型')
}
const srtHandle = new SrtHandle()
let srtData = await srtHandle.GetSrtDataByPath(book.srtPath as string)
let bookTaskDetailService = await BookTaskDetailService.getInstance()
this.realm.write(() => {
book.version = ''
this.realm.create('Book', book)
// 添加一个任务
let bookTask = {
id: crypto.randomUUID(),
bookId: book.id,
no: 1,
name: `${book.name}_00001`,
generateVideoPath: null,
srtPath: null,
audioPath: null,
imageFolder: path.relative(projectPath, bookTaskImageFolder), // 获取文件输出对于项目的相对路径
styleList: [],
prefix: null,
status: BookTaskStatus.WAIT,
errorMsg: null,
isAuto: false,
updateTime: new Date(),
createTime: new Date(),
version: '',
imageCategory: imageCategory,
openVideoGenerate: false
}
// 添加任务
this.realm.create('BookTask', bookTask)
// 循环添加小说详细信息
for (let i = 0; i < srtData.length; i++) {
const element = srtData[i]
bookTaskDetailService.AddBookTaskDetail({
bookTaskId: bookTask.id,
bookId: bookTask.bookId,
startTime: element.start,
endTime: element.end,
status: BookTaskStatus.WAIT,
word: element.text,
afterGpt: element.text,
subValue: JSON.stringify([
{
id: crypto.randomUUID(),
end_time: element.end,
start_time: element.start,
srt_value: element.text
}
] as BookTaskDetail.CopywritingSubValue[]),
timeLimit: `${element.start} -- ${element.end}`,
// 新增修脸跟随
adetailer: false // 默认false,实际更具SD设置中为主
})
}
})
// 保存成功,返回数据,但是要做处理
book.bookFolderPath = bookFolderPath
book.imageFolder = imageFolder
book.oldVideoPath = oldVideoPath
// 创建第一个默认批次的详细信息
return book
} else {
// 修改
// 判断指定的名字在数据库中是否存在(不算自己)
let books = this.realm
.objects('Book')
.filtered('name = $0 AND id != $1', book.name, book.id)
if (books.length > 0) {
throw new Error(`小说名字 ${book.name} 已经存在,请更换小说名字`)
}
// 两个文件夹地址不能改,删除两个属性
delete book.bookFolderPath
delete book.imageFolder
// 修改数据
book.updateTime = new Date()
this.transaction(() => {
this.realm.create('Book', book, UpdateMode.Modified)
})
// 保存成功,返回数据,但是要做处理
return book
}
} catch (error) {
throw error
}
}
/**
* 修改小说数据
* @param bookId 小说的ID
* @param bookData 要修改的小说数据
*/
async ModifyBookDataById(bookId: string, bookData: Book.SelectBook): Promise<Book.SelectBook> {
try {
if (bookId == null) {
throw new Error('修改小说数据失败,缺少小说ID')
}
if (bookData == null) {
throw new Error('修改小说数据失败,缺少小说数据')
}
// 检查小说ID对应的数据是不是存在
let bookRes = await this.GetBookDataById(bookId)
if (bookRes == null) {
throw new Error('修改小说数据失败,小说ID对应的数据不存在')
}
if (bookData && bookData.id) {
delete bookData.id
}
// 开始修改
this.transaction(() => {
this.realm.create('Book', { id: bookId, ...bookData }, UpdateMode.Modified)
})
bookRes = await this.GetBookDataById(bookId)
if (bookRes == null) {
throw new Error('获取修改后的小说数据失败,小说ID对应的数据不存在')
}
return bookRes
} catch (error) {
throw error
}
}
/**
* 删除指定的小说任务
* @param bookId 需要删除的小说的ID
*/
DeleteBookDataById(bookId: string): void {
try {
this.transaction(() => {
let book = this.realm.objectForPrimaryKey('Book', bookId)
if (book == null) {
throw new Error('未找到对应的小说')
}
// 删除对应的小说
this.realm.delete(book)
})
} catch (error) {
throw error
}
}
}
@@ -0,0 +1,401 @@
import Realm from 'realm'
import path from 'path'
import { cloneDeep, isEmpty } from 'lodash'
import { JoinPath } from '../../../Tools/file'
import { RealmBaseService } from '../base/realmBase'
import { Book } from '@/define/model/book/book'
import { getProjectPath } from '@/main/service/option/optionCommonService'
import { BookTaskDetailModel, ReversePrompt } from '../../model/bookTaskDetail'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
export class BookTaskDetailService extends RealmBaseService {
static instance: BookTaskDetailService | null = null
declare realm: Realm
private constructor() {
super()
}
/**
* 获取当前实例对象,为空则创建一个新的
* @returns
*/
public static async getInstance() {
if (BookTaskDetailService.instance === null) {
BookTaskDetailService.instance = new BookTaskDetailService()
await super.getInstance()
}
await BookTaskDetailService.instance.open()
return BookTaskDetailService.instance
}
/**
* 更具条件查询执行的小说的分镜信息
* @param condition 查询的条件,idnamebookIdbookTaskId
*/
async GetBookTaskDetailDataByCondition(
condition: Book.QueryBookTaskDetailCondition
): Promise<Book.SelectBookTaskDetail[]> {
try {
if (condition == null) {
throw new Error('查询小说分镜信息,查询条件不能为空')
}
let tasksToDelete = this.realm.objects<BookTaskDetailModel>('BookTaskDetail')
if (condition.id) {
tasksToDelete = tasksToDelete.filtered('id==$0', condition.id)
}
if (condition.bookId) {
tasksToDelete = tasksToDelete.filtered('bookId==$0', condition.bookId)
}
if (condition.bookTaskId) {
tasksToDelete = tasksToDelete.filtered('bookTaskId==$0', condition.bookTaskId)
}
if (condition.name) {
tasksToDelete = tasksToDelete.filtered('name==$0', condition.name)
}
let projectPath = await getProjectPath()
let resData = Array.from(tasksToDelete).map((item) => {
let resObj = {
...item,
videoPath: JoinPath(projectPath, item.videoPath),
generateVideoPath: JoinPath(projectPath, item.generateVideoPath),
audioPath: JoinPath(projectPath, item.audioPath),
oldImage: JoinPath(projectPath, item.oldImage),
outImagePath: JoinPath(projectPath, item.outImagePath),
subImagePath: (item.subImagePath as string[])?.map((subImage) => {
return JoinPath(projectPath, subImage)
}),
characterTags: item.characterTags ? item.characterTags.map((tag) => tag) : [],
sceneTags: item.sceneTags ? item.sceneTags.map((tag) => tag) : [],
styleTags: item.styleTags ? item.styleTags.map((tag) => tag) : [],
subValue:
isEmpty(item.subValue) || item.subValue == null ? null : JSON.parse(item.subValue),
reversePrompt: item.reversePrompt?.map((reversePrompt) => {
return {
...reversePrompt
}
}),
mjMessage: item.mjMessage ? item.mjMessage.toJSON() : undefined,
videoMessage: item.videoMessage ? item.videoMessage.toJSON() : undefined
} as Book.SelectBookTaskDetail
// 不是网络地址,并且存在,进行地址的拼接
if (
resObj.videoMessage &&
resObj.videoMessage.imageUrl &&
!isEmpty(resObj.videoMessage.imageUrl) &&
!(resObj.videoMessage.imageUrl as string).startsWith('http')
) {
resObj.videoMessage.imageUrl = JoinPath(
projectPath,
resObj.videoMessage.imageUrl as string
)
}
return cloneDeep(resObj)
})
return resData
} catch (error) {
throw error
}
}
/**
* 返回指定的小说分镜的指定的属性
* @param bookTaskDetailId 小说分镜的ID
* @param property 小说属性名字
* @returns
*/
async GetBookTaskDetailProperty(bookTaskDetailId: string, property: string) {
let bookTaskDetail = await this.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('未找到对应的小说任务详细信息 ' + bookTaskDetailId)
}
if (bookTaskDetail.hasOwnProperty(property)) {
return bookTaskDetail[property]
} else {
throw new Error(`未找到对应的属性 ${property}`)
}
}
/**
* 通过ID获取指定的小说任务分镜详细数据
* @param bookTaskDetailId
*/
public async GetBookTaskDetailDataById(
bookTaskDetailId: string
): Promise<Book.SelectBookTaskDetail | null> {
try {
if (bookTaskDetailId == null) {
throw new Error('获取小说任务详细信息失败,缺少ID')
}
let bookTaskDetails = await this.GetBookTaskDetailDataByCondition({ id: bookTaskDetailId })
if (bookTaskDetails.length <= 0) {
return null
} else {
return bookTaskDetails[0] as Book.SelectBookTaskDetail
}
} catch (error) {
throw error
}
}
/**
* 添加一条小说任务对应的详细数据
* @param BookTaskDetail
*/
public AddBookTaskDetail(bookTaskDetail: Book.SelectBookTaskDetail) {
try {
// 判断是不是又小说ID
if (isEmpty(bookTaskDetail.bookId) || isEmpty(bookTaskDetail.bookTaskId)) {
throw new Error(
'新增小说任务详细信息到数据库失败,数据不完整,缺少小说ID或者小说批次任务ID'
)
}
// 开始初始化数据(获取指定的bookId和bookTaskId)中最大的no
let bookTaskDetails = this.realm
.objects<BookTaskDetailModel>('BookTaskDetail')
.filtered(
'bookId == $0 AND bookTaskId == $1',
bookTaskDetail.bookId,
bookTaskDetail.bookTaskId
)
let maxNo = bookTaskDetails.max('no')
bookTaskDetail.no = maxNo ? Number(maxNo) + 1 : 1
let name = bookTaskDetail.no.toString().padStart(5, '0')
bookTaskDetail.name = name
bookTaskDetail.id = crypto.randomUUID()
bookTaskDetail.imageLock = false
bookTaskDetail.createTime = new Date()
bookTaskDetail.updateTime = new Date()
if (!bookTaskDetail.hasOwnProperty('adetailer')) {
bookTaskDetail.adetailer = false // 设置默认值
}
// 开始添加
this.transaction(() => {
this.realm.create('BookTaskDetail', bookTaskDetail)
})
//创建成功,返回
return bookTaskDetail
} catch (error) {
throw error
}
}
/**
* 更新指定ID的指定数据
* @param bookTaskDetailId
* @param updateData
*/
async ModifyBookTaskDetailById(
bookTaskDetailId: string,
updateData: Book.SelectBookTaskDetail
): Promise<Book.SelectBookTaskDetail | null> {
try {
this.transaction(() => {
let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('未找到对应的小说任务详细信息')
}
// 开始修改
for (let key in updateData) {
bookTaskDetail[key] = updateData[key]
}
bookTaskDetail.updateTime = new Date()
})
let res = await this.GetBookTaskDetailDataById(bookTaskDetailId)
return res
} catch (error) {
throw error
}
}
/**
* 更新指定ID的反推提示词数据
* @param bookTaskDetailId
* @param mjMessage
*/
UpdateBookTaskDetailMjMessage(bookTaskDetailId: string, mjMessage: Book.MJMessage): void {
try {
this.transaction(() => {
let mjMessageRes = this.realm.objectForPrimaryKey('MJMessage', bookTaskDetailId)
let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到要更新的小说分镜信息')
}
if (bookTaskDetail.mjMessage == null) {
// 新增
mjMessage.id = bookTaskDetailId
bookTaskDetail.mjMessage = mjMessage
} else {
if (mjMessageRes == null) {
throw new Error('没有找到要更新的出图信息')
}
for (const key in mjMessage) {
if (key != 'id') mjMessageRes[key] = mjMessage[key]
}
}
})
} catch (error) {
throw error
}
}
/**
* 更新小说分镜的视频消息
* @param bookTaskDetailId
* @param videoMessage
*/
async UpdateBookTaskDetailVideoMessage(
bookTaskDetailId: string,
videoMessage: BookTaskDetail.VideoMessage
): Promise<void> {
let projectPath = await getProjectPath()
this.transaction(() => {
let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId)
let videoMessageRes = this.realm.objectForPrimaryKey('VideoMessage', bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到要更新的小说分镜信息')
}
if (videoMessageRes == null) {
throw new Error('没有找到要更新的视频信息')
}
if (bookTaskDetail.videoMessage == null) {
// 新增
videoMessage.id = bookTaskDetailId
bookTaskDetail.videoMessage = videoMessage
} else {
for (const key in videoMessage) {
if (key == 'id') {
continue
}
if (
key == 'imageUrl' &&
videoMessage[key] != null &&
!videoMessage[key].startsWith('http')
) {
videoMessage[key] = path.relative(projectPath, videoMessage[key])
}
videoMessageRes[key] = videoMessage[key]
}
}
})
}
/**
* 更新指定ID的反推提示词数据
* @param bookTaskDetailId 分镜数据的ID
* @param reversePromptId 反推出来的提示词ID
* @param updateData 要更新的数据
*/
UpdateBookTaskDetailReversePrompt(
bookTaskDetailId: string,
reversePromptId: string,
reversePrompt: Book.ReversePrompt
) {
try {
this.transaction(() => {
let bookTaskDetails = this.realm.objects<ReversePrompt>('ReversePrompt')
bookTaskDetails = bookTaskDetails.filtered(
'id = $0 && bookTaskDetailId = $1',
reversePromptId,
bookTaskDetailId
)
if (bookTaskDetails.length <= 0) {
throw new Error('未找到执行的翻译数据,无法写回')
}
let bookTaskDetail = bookTaskDetails[0]
// 直接写入
for (const key in reversePrompt) {
bookTaskDetail[key] = reversePrompt[key]
}
})
return null
} catch (error) {
throw error
}
}
/**
* 删除满足条件的对象吗,必传小说ID和小说任务ID
* @param condition bookIdbookTaskIdnameid
*/
DeleteBookTaskDetail(condition: Book.DeleteBookTaskDetailCondition) {
try {
if (isEmpty(condition.id) && isEmpty(condition.bookTaskId) && isEmpty(condition.bookId)) {
throw new Error('删除小说分镜信息失败,没有必要参数')
}
let tasksToDelete = this.realm.objects<BookTaskDetailModel>('BookTaskDetail')
if (condition.id) {
tasksToDelete = tasksToDelete.filtered('id==$0', condition.id)
}
if (condition.bookId) {
tasksToDelete = tasksToDelete.filtered('bookId==$0', condition.bookId)
}
if (condition.bookTaskId) {
tasksToDelete = tasksToDelete.filtered('bookTaskId==$0', condition.bookTaskId)
}
if (condition.name) {
tasksToDelete = tasksToDelete.filtered('name==$0', condition.name)
}
this.transaction(() => {
this.realm.delete(tasksToDelete)
})
return null
} catch (error) {
throw error
}
}
/**
* 删除指定ID的小说任务详细数据中的所有的反推提示词数据
* @param bookTaskDetailId 小说分镜的ID
*/
DeleteBookTaskDetailReversePromptById(bookTaskDetailId: string): void {
this.transaction(() => {
let bookTaskDetails = this.realm
.objects<BookTaskDetailModel>('BookTaskDetail')
.filtered('id = $0', bookTaskDetailId)
if (bookTaskDetails.length <= 0) {
throw new Error('删除小说任务详细信息的反推提示词失败,未找到对应的分镜信息')
}
let bookTaskDetail = bookTaskDetails[0]
bookTaskDetail.gptPrompt = null
// 删除所有的反推提示词
if (bookTaskDetail.reversePrompt) {
bookTaskDetail.reversePrompt.forEach((item) => {
this.realm.delete(item)
})
}
})
}
/**
* 删除MJ中生成图片的所有的信息,包括 outImagePath subImagePath mjMessage (mjMessage是另一张表,要单独生成)
* @param bookTaskDetailId
*/
DeleteBoookTaskDetailGenerateImage(bookTaskDetailId: string): void {
this.transaction(() => {
let bookTaskDetail = this.realm.objectForPrimaryKey<BookTaskDetailModel>(
'BookTaskDetail',
bookTaskDetailId
)
if (bookTaskDetail == null) {
throw new Error('没有找到要删除的分镜信息')
}
if (bookTaskDetail.mjMessage) {
this.realm.delete(bookTaskDetail.mjMessage)
}
bookTaskDetail.mjMessage = null
bookTaskDetail.outImagePath = null
bookTaskDetail.subImagePath = []
})
}
}
@@ -0,0 +1,309 @@
import Realm from 'realm'
import { RealmBaseService } from '../base/realmBase'
import { Book } from '@/define/model/book/book'
import { BookTaskModel } from '../../model/bookTask'
import { getProjectPath } from '@/main/service/option/optionCommonService'
import { ImageCategory } from '@/define/data/imageData'
import { JoinPath } from '@/define/Tools/file'
import { BookTaskStatus } from '@/define/enum/bookEnum'
export class BookTaskService extends RealmBaseService {
static instance: BookTaskService | null = null
declare realm: Realm
private constructor() {
super()
}
/**
* 获取当前实例对象,为空则创建一个新的
* @returns
*/
public static async getInstance() {
if (BookTaskService.instance === null) {
BookTaskService.instance = new BookTaskService()
await super.getInstance()
}
await BookTaskService.instance.open()
return BookTaskService.instance
}
/**
* 查询满足条件的小说子任务信息
* @param bookTaskCondition 查询条件 idbookIdnamenopage, pageSize
*/
async GetBookTaskDataByCondition(
bookTaskCondition: Book.QueryBookTaskCondition
): Promise<Book.QueryBookTaskConditionResponse> {
try {
// 获取所有的小说数据,并进行时间降序排序
let bookTasks = this.realm.objects<BookTaskModel>('BookTask')
// 开始开始筛选
if (bookTaskCondition.id) {
// 查询对应的小说ID的数据
bookTasks = bookTasks.filtered('id = $0', bookTaskCondition.id)
}
if (bookTaskCondition.bookId) {
// 查询对应的小说ID的数据
bookTasks = bookTasks.filtered('bookId = $0', bookTaskCondition.bookId)
}
if (bookTaskCondition.name) {
// 查询对应的小说ID的数据
bookTasks = bookTasks.filtered('name = $0', bookTaskCondition.name)
}
if (bookTaskCondition.no) {
// 查询对应的小说ID的数据
bookTasks = bookTasks.filtered('no = $0', bookTaskCondition.no)
}
let bookTask_length = bookTasks.length
// bookTasks = bookTasks.sorted('updateTime', true)
// 判断是不是有page和pageSize,有的话对查询返回的信息做分页
if (bookTaskCondition.page && bookTaskCondition.pageSize) {
bookTasks = bookTasks.slice(
(bookTaskCondition.page - 1) * bookTaskCondition.pageSize,
bookTaskCondition.page * bookTaskCondition.pageSize
) as unknown as Realm.Results<BookTaskModel>
}
let projectPath: string = await getProjectPath()
// 做一下数据转换
// 将realm对象数组转换为普通对象数组
// 将realm对象数组转换为普通对象数组,并处理异步操作
let res_bookTasks = Array.from(bookTasks).map((bookTask) => {
// 直接操作普通对象
return {
...bookTask,
imageStyle: bookTask.imageStyle ? Array.from(bookTask.imageStyle) : [],
customizeImageStyle: bookTask.customizeImageStyle
? Array.from(bookTask.customizeImageStyle)
: [],
generateVideoPath: JoinPath(projectPath, bookTask.generateVideoPath),
srtPath: JoinPath(projectPath, bookTask.srtPath),
audioPath: JoinPath(projectPath, bookTask.audioPath),
imageFolder: JoinPath(projectPath, bookTask.imageFolder),
cacheImageList: bookTask.cacheImageList
? Array.from(bookTask.cacheImageList).map((item) => JoinPath(projectPath, item))
: [],
imageCategory: bookTask.imageCategory ? bookTask.imageCategory : ImageCategory.Midjourney // 默认使用MJ出图
} as Book.SelectBookTask
})
return {
bookTasks: JSON.parse(JSON.stringify(res_bookTasks)),
bookTaskLength: bookTask_length
} as Book.QueryBookTaskConditionResponse
} catch (error) {
throw error
}
}
/**
* 通过ID获取小说批次任务的数据
* @param bookTaskId
*/
async GetBookTaskDataById(bookTaskId: string): Promise<Book.SelectBookTask> {
try {
if (bookTaskId == null) {
throw new Error('小说任务ID不能为空')
}
let bookTasks = await this.GetBookTaskDataByCondition({ id: bookTaskId })
if (bookTasks.bookTasks.length <= 0) {
throw new Error('未找到对应的小说任务')
} else {
return bookTasks.bookTasks[0]
}
} catch (error) {
throw error
}
}
/**
* 修改小说批次任务的状态
* @param bookTaskId 小说批次任务Id
* @param status 目标状态
*/
ModifyBookTaskStatus(bookTaskId: string, status: BookTaskStatus, errorMsg?: string): void {
try {
this.transaction(() => {
// 修改对应小说批次任务的状态
let bookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (bookTask == null) {
throw new Error('未找到对应的小说任务')
}
bookTask.status = status
bookTask.updateTime = new Date()
if (errorMsg != null) {
bookTask.errorMsg = errorMsg
}
})
} catch (error) {
throw error
}
}
/**
* 修改小说批次任务数据
* @param bookTaskId 小说批次任务ID
* @param data 要修改的数据
*/
async ModifyBookTaskDataById(
bookTaskId: string,
data: Book.SelectBookTask
): Promise<Book.SelectBookTask> {
try {
this.transaction(() => {
let updateData = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (updateData == null) {
throw new Error('未找到对应的小说任务详细信息')
}
// 开始修改
for (let key in data) {
updateData[key] = data[key]
}
})
let res = await this.GetBookTaskDataById(bookTaskId)
return res
} catch (error) {
throw error
}
}
// 添加一条数据
async AddBookTask(bookTask: Book.SelectBookTask): Promise<Book.SelectBookTask> {
try {
// 新增
if (bookTask.bookId == '' || bookTask.bookId == null) {
throw new Error('小说ID不能为空')
}
bookTask.id = crypto.randomUUID()
// 获取当前bookID对应的最大的no
let maxNo = this.realm.objects('BookTask').filtered('bookId = $0', bookTask.bookId).max('no')
bookTask.no = maxNo == null ? 1 : Number(maxNo) + 1
bookTask.name = 'output_0000' + bookTask.no
bookTask.status = BookTaskStatus.WAIT
bookTask.updateTime = new Date()
bookTask.createTime = new Date()
this.realm.write(() => {
this.realm.create('BookTask', bookTask)
})
// 处理完毕,返回结果
let res = await this.GetBookTaskDataById(bookTask.id)
return res
} catch (error) {
throw error
}
}
/**
* 获取最大的小说批次任务的编号
* @param bookId 小说ID
*/
async GetMaxBookTaskNo(bookId: string): Promise<number> {
let maxNo = this.realm.objects('BookTask').filtered('bookId = $0', bookId).max('no')
let no = maxNo == null ? 1 : Number(maxNo) + 1
return no
}
/**
* 重置小说批次数据,清除小说的详细信息
* @param bookTaskId 小说批次ID
*/
async ResetBookTaskDataById(
bookTaskId: string,
resetBase: boolean = true
): Promise<Book.SelectBookTask> {
try {
// 开始重置数据,先重置小说批次数据,在重置其他
this.transaction(() => {
this.ResetBookTaskDataInfo(bookTaskId, resetBase)
})
let res = await this.GetBookTaskDataById(bookTaskId)
return res
} catch (error) {
throw error
}
}
/**
* 删除对应的小说批次数据
* @param bookTaskId 要删除的批次的ID
*/
DeleteBookTaskDataById(bookTaskId: string): void {
try {
this.transaction(() => {
// 先调用清除数据的方法
this.ResetBookTaskDataInfo(bookTaskId)
// 删除批次数据
let bookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (bookTask == null) {
throw new Error('未找到对应的小说任务,无法执行删除操作')
}
this.realm.delete(bookTask)
})
} catch (error) {
throw error
}
}
/** 重置小说批次任务数据 */
private ResetBookTaskDataInfo(bookTaskId: string, resetBase: boolean = true) {
try {
let modifyBookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (modifyBookTask == null) {
throw new Error('未找到对应的小说批次任务,无法执行重置操作')
}
let book = this.realm.objectForPrimaryKey('Book', modifyBookTask.bookId)
if (book == null) {
throw new Error('未找到对应的小说,无法执行重置操作')
}
if (resetBase) {
// 基础数据的重置
modifyBookTask.errorMsg = ''
modifyBookTask.updateTime = new Date()
modifyBookTask.imageStyle = []
modifyBookTask.autoAnalyzeCharacter = undefined
modifyBookTask.customizeImageStyle = []
modifyBookTask.videoConfig = undefined
modifyBookTask.prefixPrompt = undefined
modifyBookTask.suffixPrompt = undefined
modifyBookTask.subImageFolder = []
modifyBookTask.srtPath = book.srtPath ?? undefined
modifyBookTask.audioPath = book.audioPath ?? undefined
}
// 继承小说的srt和配音文件
modifyBookTask.imageCategory = ImageCategory.Midjourney // 默认使用MJ出图
modifyBookTask.status = BookTaskStatus.WAIT
// 开始删除 分镜信息
let bookTaskDetails = this.realm
.objects('BookTaskDetail')
.filtered('bookTaskId = $0', bookTaskId)
// 开始删除数据
for (const bookTaskDetail of bookTaskDetails) {
// 删除MJMessage
if (bookTaskDetail.mjMessage) {
this.realm.delete(bookTaskDetail.mjMessage)
}
if (bookTaskDetail.reversePrompt) {
;(bookTaskDetail.reversePrompt as any[]).forEach((item) => {
this.realm.delete(item)
})
}
if (bookTaskDetail.sdConifg) {
this.realm.delete(bookTaskDetail.sdConifg)
}
this.realm.delete(bookTaskDetail)
}
} catch (error) {
throw error
}
}
}
@@ -0,0 +1,424 @@
import Realm from 'realm'
import { RealmBaseService } from '../base/realmBase'
import { cloneDeep, isEmpty } from 'lodash'
import { Book } from '@/define/model/book/book'
import { BookBackTaskStatus, BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
import { OtherData } from '@/define/enum/softwareEnum'
import { TaskModal } from '@/define/model/task'
export class TaskListService extends RealmBaseService {
static instance: TaskListService | null = null
declare realm: Realm
private constructor() {
super()
}
/**
* 获取当前实例对象,为空则创建一个新的
* @returns
*/
public static async getInstance() {
if (TaskListService.instance === null) {
TaskListService.instance = new TaskListService()
await super.getInstance()
}
await TaskListService.instance.open()
return TaskListService.instance
}
/**
* 获取任务集合
*
* 该方法根据提供的查询条件从数据库中检索任务列表,支持多种筛选条件和分页功能。
* 可通过小说名称、任务名称、状态等多个维度进行筛选,并为每个任务补充关联的小说
* 和任务信息。结果按创建时间降序排列,并根据分页参数返回指定范围的数据。
*
* 支持的筛选条件包括:
* - 小说名称(模糊匹配)
* - 小说任务名称(模糊匹配)
* - 小说任务详情名称(模糊匹配)
* - 任务名称(模糊匹配)
* - 任务类型(精确匹配)
* - 任务状态(精确匹配)
* - 任务错误信息(模糊匹配)
*
* @param {TaskModal.QueryTaskCondition} queryTaskCondition - 查询条件对象,包含筛选和分页参数
* @returns {TaskModal.TaskCollection} 包含分页信息和任务数据的集合对象
*
* @example
* // 查询与"小说项目"相关的所有失败任务,第1页,每页10条
* const condition = {
* bookName: "小说项目",
* taskStatus: BookBackTaskStatus.FAIL,
* page: 1,
* pageSize: 10
* };
* const taskCollection = taskListService.GetTaskCollection(condition);
* console.log(`共找到${taskCollection.count}条记录,当前显示${taskCollection.data.length}条`);
*/
GetTaskCollection(queryTaskCondition: TaskModal.QueryTaskCondition): TaskModal.TaskCollection {
let tasks = this.realm.objects('TaskList')
if (!isEmpty(queryTaskCondition.bookName)) {
let book = this.realm
.objects('Book')
.filtered('name CONTAINS[c] $0', queryTaskCondition.bookName)
let ids = [] as string[]
if (book.length > 0) {
ids = book.map((item) => {
return item.id as string
})
}
tasks = tasks.filtered('bookId in $0', ids)
}
if (!isEmpty(queryTaskCondition.bookTaskName)) {
let bookTask = this.realm
.objects('BookTask')
.filtered('name CONTAINS[c] $0', queryTaskCondition.bookTaskName)
let ids = [] as string[]
if (bookTask.length > 0) {
ids = bookTask.map((item) => {
return item.id as string
})
}
tasks = tasks.filtered('bookTaskId in $0', ids)
}
if (!isEmpty(queryTaskCondition.bookTaskDetailName)) {
let bookTaskDetail = this.realm
.objects('BookTaskDetail')
.filtered('name CONTAINS[c] $0', queryTaskCondition.bookTaskDetailName)
let ids = [] as string[]
if (bookTaskDetail.length > 0) {
ids = bookTaskDetail.map((item) => {
return item.id as string
})
}
tasks = tasks.filtered('bookTaskDetailId in $0', ids)
}
if (!isEmpty(queryTaskCondition.taskName)) {
tasks = tasks.filtered('name CONTAINS[c] $0', queryTaskCondition.taskName)
}
if (!isEmpty(queryTaskCondition.taskType)) {
tasks = tasks.filtered('type == $0', queryTaskCondition.taskType)
}
if (!isEmpty(queryTaskCondition.taskStatus)) {
tasks = tasks.filtered('status == $0', queryTaskCondition.taskStatus)
}
if (!isEmpty(queryTaskCondition.taskErrorMessage)) {
tasks = tasks.filtered('errorMessage CONTAINS[c] $0', queryTaskCondition.taskErrorMessage)
}
let count = tasks.length
tasks = tasks.sorted('createTime', true)
let task = tasks.slice(
(queryTaskCondition.page - 1) * queryTaskCondition.pageSize,
queryTaskCondition.page * queryTaskCondition.pageSize
) as TaskModal.BackTaskCollection[]
let taskList = Array.from(task).map((item) => {
let resObj = {
...item
} as TaskModal.BackTaskCollection
return cloneDeep(resObj)
})
for (let i = 0; i < taskList.length; i++) {
const element = taskList[i]
let book = this.realm.objectForPrimaryKey('Book', element.bookId)
if (book) {
element.bookName = book.name as string
}
let bookTask = this.realm.objectForPrimaryKey('BookTask', element.bookTaskId)
if (bookTask) {
element.bookTaskName = bookTask.name as string
}
let bookTaskDetail = this.realm.objectForPrimaryKey(
'BookTaskDetail',
element.bookTaskDetailId
)
if (bookTaskDetail) {
element.bookTaskDetailName = bookTaskDetail.name as string
}
}
return {
page: queryTaskCondition.page,
pageSize: queryTaskCondition.pageSize,
count: count,
data: taskList
}
}
/**
* 获取等待状态的任务和返回指定数量的数据
* @param executeType 任务的执行类型
* @param count 返回数据的数量
*/
GetWaitTaskAndSlice(executeType: TaskExecuteType, count: number) {
try {
let tasks = this.realm
.objects<TaskModal.Task>('TaskList')
.filtered(
'(status == $0 || status == $1) && executeType == $2',
BookBackTaskStatus.WAIT,
BookBackTaskStatus.RECONNECT,
executeType ? executeType : TaskExecuteType.AUTO
)
.sorted('createTime', false)
let tasksArray = Array.from(tasks)
if (count != null) {
tasksArray = tasksArray.slice(0, count)
}
let res = tasksArray.map((item) => {
let resObj = {
...item
}
return resObj
})
return res
} catch (error) {
throw error
}
}
/**
* 获取指定状态的任务数量
*
* 该方法查询数据库中符合指定状态集合的任务总数,用于统计分析不同状态的任务分布。
* 使用Realm的IN操作符高效查询匹配多个状态条件的任务。
*
* @param {BookBackTaskStatus[]} status - 要统计的任务状态数组,如[WAIT, RUNNING]
* @returns {number} 符合指定状态的任务总数
*
* @example
* // 获取所有等待中的任务数量
* const waitingCount = taskListService.GetAssignStatusTaskCount([BookBackTaskStatus.WAIT]);
*
* // 获取所有正在运行或重连的任务数量
* const activeCount = taskListService.GetAssignStatusTaskCount([
* BookBackTaskStatus.RUNNING,
* BookBackTaskStatus.RECONNECT
* ]);
*/
GetAssignStatusTaskCount(status: BookBackTaskStatus[]): number {
let taskLength = this.realm
.objects<TaskModal.Task>('TaskList')
.filtered('status IN $0', status).length
return taskLength
}
/**
* 新增一个小说相关的后台任务队列
* @param bookBackTask 要添加的小说数据
*/
AddOneTask(
bookId: string,
taskType: BookBackTaskType,
executeType: TaskExecuteType = TaskExecuteType.AUTO,
bookTaskId: string | undefined = undefined,
bookTaskDetailId: string | undefined = undefined,
responseMessageName?: string
): TaskModal.Task {
try {
// 通过bookid获取book信息
let book = this.realm.objectForPrimaryKey('Book', bookId)
if (book == null) {
throw new Error('新增后台队列任务到数据库失败,没有找到对应的小说')
}
let bookTask
if (bookTaskId) {
bookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (bookTask == null) {
throw new Error('新增后台队列任务到数据库失败,没有找到对应的小说批次任务')
}
}
let bookTaskDetail
if (bookTaskDetailId) {
bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error(
'新增后台队列任务到数据库失败,没有找到对应的小说批次任务详情(分镜数据)'
)
}
}
// 开始往数据库中写数据
let name = `${book.name}-${bookTask ? bookTask.name : 'default'}-${
bookTaskDetail ? bookTaskDetail.name : 'default'
}-${taskType}`
let bookBackTask = {
id: crypto.randomUUID(),
bookId: bookId,
bookTaskId: bookTaskId ? bookTaskId : OtherData.DEFAULT,
bookTaskDetailId: bookTaskDetailId ? bookTaskDetailId : OtherData.DEFAULT,
name: name,
type: taskType,
executeType: executeType,
status: BookBackTaskStatus.WAIT,
createTime: new Date(),
updateTime: new Date(),
messageName: responseMessageName,
startTime: 0,
endTime: 0
} as TaskModal.Task
this.realm.write(() => {
this.realm.create('TaskList', bookBackTask)
})
return bookBackTask
} catch (error) {
throw error
}
}
/**
* 修改一个小说相关的后台任务队列中的详细信息
* (对于后台的队列任务只能修改状态)和错误信息
* @param bookBackTask 修改的数据
*/
UpdateTaskStatus(bookBackTask: Book.UpdateBookTaskListStatus): Book.UpdateBookTaskListStatus {
try {
// 判断数据是不是存在
if (isEmpty(bookBackTask.id) || isEmpty(bookBackTask.status)) {
throw new Error('修改后台队列任务失败,数据不完整,缺少必要字段')
}
// 开始修改
this.transaction(() => {
// 获取指定ID的队列任务
let _bookBackTask = this.realm.objectForPrimaryKey(
'TaskList',
bookBackTask.id
) as TaskModal.Task
// 判断数据是不是存在
if (_bookBackTask == null) {
throw new Error('修改后台队列任务失败,数据不存在')
}
// 修改数据
_bookBackTask.status = bookBackTask.status
if (bookBackTask.errorMessage) {
_bookBackTask.errorMessage = bookBackTask.errorMessage
}
// 根据状态修改结束和完成时间
if (bookBackTask.status == BookBackTaskStatus.RUNNING) {
_bookBackTask.startTime = new Date().getTime()
} else if (
bookBackTask.status == BookBackTaskStatus.DONE ||
bookBackTask.status == BookBackTaskStatus.FAIL ||
bookBackTask.status == BookBackTaskStatus.PAUSE
) {
_bookBackTask.endTime = new Date().getTime()
} else if (bookBackTask.status == BookBackTaskStatus.RECONNECT) {
_bookBackTask.startTime = 0
_bookBackTask.endTime = 0
}
})
return bookBackTask
} catch (error) {
throw error
}
}
/**
* 删除满足条件的数据,包含 id、bookId、bookTaskId
* 上面的条件,至少要有一个
* @param bookBackTask 删除的数据
*/
async DeleteBookBackTask(bookBackTask) {
try {
if (
!bookBackTask.hasOwnProperty('id') &&
!bookBackTask.hasOwnProperty('bookId') &&
!bookBackTask.hasOwnProperty('bookTaskId')
) {
throw new Error('删除后台队列任务失败,缺少必要的删除条件')
}
this.transaction(() => {
// 构建查询条件
const tasksToDelete = this.realm.objects('TaskList').filtered('id == $0', bookBackTask.id)
if (bookBackTask.bookId) {
tasksToDelete.filtered('bookId == $0', bookBackTask.bookId)
}
if (bookBackTask.bookTaskId) {
tasksToDelete.filtered('bookTaskId == $0', bookBackTask.bookTaskId)
}
this.realm.delete(tasksToDelete)
return bookBackTask
})
} catch (error) {
throw error
}
}
/**
* 设置指定消息名称的任务为失败,并设置错误消息
* @param messageName
* @param errorMessage
*/
async SetMessageNameTaskToFail(messageName: string, errorMessage: string) {
let tasks = this.realm.objects('TaskList').filtered('messageName == $0 ', messageName)
tasks = tasks.filtered('status != $0', BookBackTaskStatus.DONE)
tasks = tasks.filtered('status != $0', BookBackTaskStatus.FAIL)
let ids = tasks.map((item) => {
return item.id
})
this.transaction(() => {
for (let i = 0; i < ids.length; i++) {
let task = this.realm.objectForPrimaryKey('TaskList', ids[i])
if (task == null) {
throw new Error('没有找到对应的任务')
}
task.status = BookBackTaskStatus.FAIL
task.errorMessage = errorMessage
}
})
}
/**
* 丢弃所有未开始的后台任务
*
* 该方法将所有处于等待(WAIT)或重新连接(RECONNECT)状态的任务标记为失败(FAIL),
* 并设置错误信息为"任务被丢弃"。通常在需要清理任务队列、重启系统或手动干预任务处理
* 流程时使用。
*
* 丢弃操作会在一个数据库事务中执行,确保所有状态更改的原子性,避免部分更新导致的
* 数据不一致问题。
*
* @returns {Promise<void>} 无返回值的Promise
* @throws {Error} 当任务ID存在但无法找到对应任务对象时抛出错误
*
* @example
* // 重启任务队列前丢弃所有未开始的任务
* await taskListService.GiveUpNotStartBackTask();
* await taskManager.restart();
*/
GiveUpNotStartBackTask(): void {
let task = this.realm
.objects('TaskList')
.filtered(
'status == $0 || status == $1',
BookBackTaskStatus.WAIT,
BookBackTaskStatus.RECONNECT
)
let ids = task.map((item) => {
return item.id
})
// 讲所有的人物状态改为放弃,然后errmessage改为 此任务已丢弃
this.transaction(() => {
for (let i = 0; i < ids.length; i++) {
const element = this.realm.objectForPrimaryKey('TaskList', ids[i])
if (element == null) {
throw new Error('没有找到对应的任务')
}
element.status = BookBackTaskStatus.FAIL
element.errorMessage = '任务被丢弃'
}
})
}
}
+81
View File
@@ -0,0 +1,81 @@
import Realm from 'realm'
import { isEmpty, cloneDeep } from 'lodash'
import { RealmBaseService } from './base/realmBase'
import { OptionType } from '@/define/enum/option'
export class OptionRealmService extends RealmBaseService {
static instance: OptionRealmService | null = null
declare realm: Realm
private constructor() {
super()
}
/**
* 获取当前实例对象,为空则创建一个新的
* @returns
*/
public static async getInstance() {
if (OptionRealmService.instance === null) {
OptionRealmService.instance = new OptionRealmService()
await super.getInstance()
}
await OptionRealmService.instance.open()
return OptionRealmService.instance
}
/**
* 获取指定的Option,通过key,不存在返回null
* @param key
* @returns
*/
public GetOptionByKey(key: string): OptionModel.OptionModel | null {
if (isEmpty(key)) {
return null
}
let res = this.realm.objects('Option').filtered(`key = "${key}"`);
if (res.length > 0) {
let resData = Array.from(res).map((item) => {
let resObj = {
...item
}
return cloneDeep(resObj)
})
return resData[0] as OptionModel.OptionModel
} else {
return null;
}
}
/**
* 修改指定的Option,通过key,不存在则创建
* @param key
* @param value
*/
public ModifyOptionByKey(key: string, value: string, type: OptionType = OptionType.STRING) {
if (isEmpty(key)) {
return false
}
let option = this.realm.objectForPrimaryKey('Option', key);
if (option) {
this.realm.write(() => {
option.value = value;
option.type = type;
option.updateTime = new Date()
})
} else {
this.realm.write(() => {
this.realm.create('Option', {
key: key,
value: value,
type: type,
createTime: new Date(),
updateTime: new Date()
})
})
}
return true
}
}
+238
View File
@@ -0,0 +1,238 @@
import Realm from 'realm'
import { RealmBaseService } from './base/realmBase'
import { cloneDeep, isEmpty } from 'lodash'
import { PresetModel } from '../model/preset'
import path from 'path'
import { PresetModel as DefinePresetModel } from '@/define/model/preset'
export class PresetRealmService extends RealmBaseService {
static instance: PresetRealmService | null = null
declare realm: Realm
private constructor() {
super()
}
/**
* 获取当前实例对象,为空则创建一个新的
* @returns
*/
public static async getInstance() {
if (PresetRealmService.instance === null) {
PresetRealmService.instance = new PresetRealmService()
await super.getInstance()
}
await PresetRealmService.instance.open()
return PresetRealmService.instance
}
/**
* 获取指定条件的预设数据
*
* @param {PresetModel.QueryPresetCondition} queryCondition 查询条件对象
* @returns 返回符合条件的预设数据数组
*/
GetPresetByCondition(queryCondition: DefinePresetModel.QueryPresetCondition): {
presetArray: DefinePresetModel.Preset[]
total: number
} {
// 获取数据
let presets = this.realm.objects<PresetModel>('Preset')
if (queryCondition.id) {
presets = presets.filtered('id = $0', queryCondition.id)
}
if (queryCondition.label) {
presets = presets.filtered('label CONTAINS $0', queryCondition.label)
}
if (queryCondition.isShow != null) {
presets = presets.filtered('isShow = $0', queryCondition.isShow)
}
if (queryCondition.type) {
presets = presets.filtered('type = $0', queryCondition.type)
}
presets = presets.sorted('createTime', true)
let total = presets.length // 获取总条数
if (queryCondition.page && queryCondition.pageSize) {
const start = (queryCondition.page - 1) * queryCondition.pageSize
const end = start + queryCondition.pageSize
const slicedPresets = presets.slice(start, end)
let arrays = Array.from(slicedPresets).map((item) => {
let resObj = {
...item,
showImage: item.showImage
? item.showImage.map((item) => item != null && path.resolve(__dirname, item))
: [],
aliases: item.aliases ? item.aliases.map((item) => item) : []
} as DefinePresetModel.Preset
return cloneDeep(resObj)
})
return {
presetArray: arrays,
total: total
}
} else {
let arrays = Array.from(presets).map((item) => {
let resObj = {
...item,
showImage: item.showImage
? item.showImage.map((item) => item != null && path.resolve(__dirname, item))
: [],
aliases: item.aliases ? item.aliases.map((item) => item) : []
} as DefinePresetModel.Preset
return cloneDeep(resObj)
})
return {
presetArray: arrays,
total: total
}
}
}
/**
* 获取指定ID的预设数据
*
* @param {string} id 预设ID
* @returns 返回符合条件的预设数据对象或null
*/
GetPresetById(id: string): DefinePresetModel.Preset | null {
let res = this.GetPresetByCondition({
id: id
})
if (res.presetArray.length > 0) {
return res.presetArray[0]
} else {
return null
}
}
/**
* 通过ID数组和类型获取多个预设数据
*
* 该方法接收一个预设ID数组和类型字符串,返回所有匹配的预设数据。
* 会逐个查询每个ID的预设数据,并将结果合并到一个数组中返回。
* 如果某个ID找不到对应的预设,则该ID不会产生任何结果。
*
* @param {string[]} ids - 要查询的预设ID数组
* @param {string} type - 预设类型,用于过滤特定类型的预设
* @returns {DefinePresetModel.Preset[]} 匹配的预设对象数组
*
* @example
* // 获取多个角色类型预设
* const characterPresets = presetService.GetPresetByIds(['id1', 'id2', 'id3'], 'character');
*/
GetPresetByIds(ids: string[], type: string): DefinePresetModel.Preset[] {
let res: DefinePresetModel.Preset[] = []
for (let i = 0; i < ids.length; i++) {
const element = ids[i]
let preset = this.GetPresetByCondition({
id: element,
type: type,
isShow: true
})
if (preset.presetArray.length > 0) {
res.push(...preset.presetArray)
}
}
return res
}
/**
* 添加预设数据
*
* @param {PresetModel.Preset} preset 预设对象
* @return {PresetModel.Preset} 返回添加后的预设对象
* @throws {Error} 如果预设名称和类型已存在,则抛出错误
* @throws {Error} 如果预设对象的类型不合法,则抛出错误
* @throws {Error} 如果预设对象的名称为空,则抛出错误
* @throws {Error} 如果预设对象的创建时间不合法,则抛出错误
* */
AddPreset(preset: DefinePresetModel.Preset): DefinePresetModel.Preset {
// 生成一个新的预设对象
const newPreset = {
...preset,
id: preset.id || crypto.randomUUID(), // 如果没有提供ID,则生成一个新的UUID
createTime: preset.createTime || new Date() // 如果没有提供创建时间,则使用当前时间
}
if (isEmpty(newPreset.label)) {
throw new Error('预设名称不能为空!')
}
if (isEmpty(newPreset.type)) {
throw new Error('预设类型不能为空!')
}
if (isEmpty(newPreset.id)) {
throw new Error('预设ID不能为空!')
}
// 判断对应的名字和类型是不是存在
const existingPreset = this.realm
.objects('Preset')
.filtered('label = $0 && type = $1', newPreset.label, newPreset.type)
if (existingPreset.length > 0) {
throw new Error('再相同类型下已存在当前的预设名字,请修改后再试!')
}
// 开始写入数据
this.realm.write(() => {
this.realm.create('Preset', newPreset, Realm.UpdateMode.All)
})
// 将数据取出返回
return this.GetPresetById(newPreset.id) as DefinePresetModel.Preset
}
/**
* 修改预设数据
*
* @param {string} id 预设ID
* @param {Partial<PresetModel.Preset>} preset 预设对象
* @throws {Error} 如果预设ID为空,则抛出错误
* @throws {Error} 如果预设对象不存在,则抛出错误
* */
ModifyPreset(id: string, preset: Partial<DefinePresetModel.Preset>): DefinePresetModel.Preset {
if (isEmpty(id)) {
throw new Error('要修改的预设ID不能为空!')
}
delete preset.id // 删除ID属性,避免修改ID
delete preset.createTime // 删除创建时间属性,避免修改创建时间
this.transaction(() => {
let existingPreset = this.realm.objectForPrimaryKey<PresetModel>('Preset', id)
if (existingPreset == null) {
throw new Error('要修改的预设不存在!')
}
// 开始修改
for (let key in preset) {
existingPreset[key] = preset[key]
}
})
// 修改完成后,返回修改后的预设对象
return this.GetPresetById(id) as DefinePresetModel.Preset
}
/**
* 删除预设数据
*
* @param {string} id 预设ID
* @throws {Error} 如果预设ID为空,则抛出错误
* @throws {Error} 如果预设对象不存在,则抛出错误
* */
DeletePreset(id: string): void {
if (isEmpty(id)) {
throw new Error('要删除的预设ID不能为空!')
}
this.transaction(() => {
let existingPreset = this.realm.objectForPrimaryKey('Preset', id)
if (existingPreset == null) {
throw new Error('要删除的预设不存在!')
}
// 开始删除
this.realm.delete(existingPreset)
})
}
}
+117
View File
@@ -0,0 +1,117 @@
// 检测当前环境
const isNode =
typeof process !== 'undefined' && process.versions != null && process.versions.node != null
// 浏览器环境的路径处理函数
function joinPath(...segments: string[]): string {
return segments.join('/').replace(/\/+/g, '/')
}
// 动态导入模块
let appIsPackaged = false
let basePath = ''
// Node环境下的初始化
if (isNode) {
try {
const { app } = require('electron')
appIsPackaged = app.isPackaged
const path = require('path')
basePath = appIsPackaged
? path.join(__dirname, '../../../resources')
: path.join(__dirname, '../../resources')
} catch (e) {
// 可能是纯Node环境,没有Electron
try {
const path = require('path')
basePath = path.join(process.cwd(), 'resources')
} catch (e) {
console.warn('无法确定Node环境路径基础')
}
}
} else {
// 浏览器环境,使用相对路径或API提供的基础路径
// 这里可以根据实际部署环境调整
basePath = '/resources'
}
// 创建配置对象
const define = (() => {
if (isNode) {
const path = require('path')
// Node环境使用文件系统路径
const createPaths = (base: string) => ({
icon: path.join(base, 'icon.ico'),
package_path: path.join(base, 'package'),
db_path: path.join(base, 'scripts/db'),
scripts_path: path.join(base, 'scripts'),
log_folder: path.join(base, 'logger'),
image_path: path.join(base, 'image'),
resources_path: base,
cache_path: path.join(base, 'cache'),
draft_temp_path: path.join(base, 'tmp/jianyingTemp.zip'),
clip_speed_temp_path: path.join(base, 'tmp/Clip/speeds_tmp.json'),
add_canvases_temp_path: path.join(base, 'tmp/Clip/canvases_tmp.json'),
add_sound_channel_mappings_temp_path: path.join(
base,
'tmp/Clip/sound_channel_mappings_tmp.json'
),
add_vocal_separations_temp_path: path.join(base, 'tmp/Clip/vocal_separations_tmp.json'),
add_material_video_temp_path: path.join(base, 'tmp/Clip/videoMaterialTemp.json'),
add_tracks_segments_temp_path: path.join(base, 'tmp/Clip/tracks_segments_tmp.json'),
add_tracks_type_temp_path: path.join(base, 'tmp/Clip/tracks_type_tmp.json'),
add_material_animations_temp_path: path.join(base, 'tmp/Clip/material_animations_tmp.json'),
add_material_text_temp_path: path.join(base, 'tmp/Clip/material_text_temp.json'),
add_track_text_segments_temp_path: path.join(base, 'tmp/Clip/track_text_segments_temp.json'),
add_materials_beats_tmp_path: path.join(base, 'tmp/Clip/materials_beats_tmp.json'),
add_materials_audios_tmp_path: path.join(base, 'tmp/Clip/materials_audios_tmp.json'),
add_tracks_audio_segments_tmp_path: path.join(
base,
'tmp/Clip/tracks_audio_segments_tmp.json'
),
add_keyframe_tmp_path: path.join(base, 'tmp/Clip/keyframe_tmp.json'),
lms_url: 'https://lms.laitool.cn'
})
return createPaths(basePath)
} else {
// 浏览器环境使用URL路径
const createPaths = (base: string) => ({
icon: joinPath(base, 'icon.ico'),
package_path: joinPath(base, 'package'),
db_path: joinPath(base, 'scripts/db'),
scripts_path: joinPath(base, 'scripts'),
log_folder: joinPath(base, 'logger'),
image_path: joinPath(base, 'image'),
resources_path: base,
cache_path: joinPath(base, 'cache'),
draft_temp_path: joinPath(base, 'tmp/jianyingTemp.zip'),
clip_speed_temp_path: joinPath(base, 'tmp/Clip/speeds_tmp.json'),
add_canvases_temp_path: joinPath(base, 'tmp/Clip/canvases_tmp.json'),
add_sound_channel_mappings_temp_path: joinPath(
base,
'tmp/Clip/sound_channel_mappings_tmp.json'
),
add_vocal_separations_temp_path: joinPath(base, 'tmp/Clip/vocal_separations_tmp.json'),
add_material_video_temp_path: joinPath(base, 'tmp/Clip/videoMaterialTemp.json'),
add_tracks_segments_temp_path: joinPath(base, 'tmp/Clip/tracks_segments_tmp.json'),
add_tracks_type_temp_path: joinPath(base, 'tmp/Clip/tracks_type_tmp.json'),
add_material_animations_temp_path: joinPath(base, 'tmp/Clip/material_animations_tmp.json'),
add_material_text_temp_path: joinPath(base, 'tmp/Clip/material_text_temp.json'),
add_track_text_segments_temp_path: joinPath(base, 'tmp/Clip/track_text_segments_temp.json'),
add_materials_beats_tmp_path: joinPath(base, 'tmp/Clip/materials_beats_tmp.json'),
add_materials_audios_tmp_path: joinPath(base, 'tmp/Clip/materials_audios_tmp.json'),
add_tracks_audio_segments_tmp_path: joinPath(base, 'tmp/Clip/tracks_audio_segments_tmp.json'),
add_keyframe_tmp_path: joinPath(base, 'tmp/Clip/keyframe_tmp.json'),
lms_url: 'https://lms.laitool.cn'
})
return createPaths(basePath)
}
})()
// 导出类型定义和实例
export type DefineType = typeof define
export { define }
+624
View File
@@ -0,0 +1,624 @@
import { TaskModal } from '@/define/model/task'
//#region 小说类型
export enum BookType {
// 原创
ORIGINAL = 'original',
// 反推
SD_REVERSE = 'sd_reverse',
// MJ 反推
MJ_REVERSE = 'mj_reverse'
}
/**
* 根据小说类型返回小说类型的label
* @param type 小说类型
* @returns
*/
export function GetBookTypeLabel(type: BookType) {
switch (type) {
case BookType.ORIGINAL:
return '原创'
case BookType.SD_REVERSE:
return 'SD反推'
case BookType.MJ_REVERSE:
return 'MJ反推'
default:
return '未知类型'
}
}
/**
* 获取小说类型的选项列表
* @returns
*/
export function GetBookTypeOptions() {
return [
{ label: '原创', value: BookType.ORIGINAL },
{ label: 'SD反推', value: BookType.SD_REVERSE },
{ label: 'MJ反推', value: BookType.MJ_REVERSE }
]
}
//#endregion
export enum AddBookTaskCopyData {
AFTER_GPT = 'after_gpt', // 文案
OLD_IMAGE = 'old_image', // 抽帧/视频
GPT_PROMPT = 'gpt_prompt', // 反推/GPT提示词
CHARACTER = 'character', // 角色
IMAGE_STYLE = 'image_style', // 风格
PROMPT = 'prompt', // 生图提示词
IMAGE = 'image' // 生图
}
export enum MJCategroy {
// 本地MJ
LOCAL_MJ = 'local_mj',
// 代理MJ
REMOTE_MJ = 'remote_mj',
// 浏览器模式
BROWSER_MJ = 'browser_mj',
// API模式
API_MJ = 'api_mj'
}
export enum MJAction {
// 生图
IMAGINE = 'IMAGINE',
// 反推describe
DESCRIBE = 'DESCRIBE'
}
export enum BookBackTaskType {
// 分镜计算
STORYBOARD = 'storyboard',
// 分割视频
SPLIT = 'split',
// 提取音频
AUDIO = 'audio',
// 识别字幕
RECOGNIZE = 'recognize',
// 抽帧
FRAME = 'frame',
// MJ反推
MJ_REVERSE = BookType.MJ_REVERSE,
// SD反推
SD_REVERSE = BookType.SD_REVERSE,
// MJ生成图片
MJ_IMAGE = 'mj_image',
// SD 生成图片
SD_IMAGE = 'sd_image',
// ComfyUI 生成图片
ComfyUI_IMAGE = 'comfyui_image',
// flux forge 生成图片
FLUX_FORGE_IMAGE = 'flux_forge_image',
// flux api 生成图片
FLUX_API_IMAGE = 'flux_api_image',
// D3 生成图片
D3_IMAGE = 'd3_image',
// 高清
HD = 'hd',
// 合成视频
COMPOSING = 'composing',
// 推理
INFERENCE = 'inference',
// 翻译
TRANSLATE = 'translate',
// ruanway 生成视频
RUNWAY_VIDEO = 'runway_video',
// luma 生成视频
LUMA_VIDEO = 'luma_video',
// kling 生成视频
KLING_VIDEO = 'kling_video'
}
export enum BookBackTaskStatus {
// 等待
WAIT = 'wait',
// 运行中
RUNNING = 'running',
// 暂停
PAUSE = 'pause',
// 完成
DONE = 'done',
// 失败
FAIL = 'fail',
// 重连
RECONNECT = 'reconnect'
}
export enum TaskExecuteType {
// 自动
AUTO = 'auto',
// 手动
OPERATE = 'operate'
}
// 弹窗类型
export enum DialogType {
// 单独弹窗
DIALOG = 'dialog',
// 消息提示
MESSAGE = 'message',
// 右上角通知
NOTIFICATION = 'notification'
}
/**
* 小说任务状态
*/
export enum BookTaskStatus {
// 等待
WAIT = 'wait',
// 分镜中
STORYBOARD = 'storyboard',
// 分镜失败
STORYBOARD_FAIL = 'storyboard_fail',
// 分镜完成,等待分割视频
STORYBOARD_DONE = 'storyboard_done',
// 分割视频中
SPLIT = 'split',
// 分割视频失败
SPLIT_FAIL = 'split_fail',
// 分割视频完成,等待提取音频
SPLIT_DONE = 'split_done',
// 提取音频中
AUDIO = 'audio',
// 提取音频失败
AUDIO_FAIL = 'audio_fail',
// 提取音频完成,等待识别字幕
AUDIO_DONE = 'audio_done',
// 识别字幕中
RECOGNIZE = 'recognize',
// 识别字幕失败
RECOGNIZE_FAIL = 'recognize_fail',
// 识别字幕完成,等待抽帧
RECOGNIZE_DONE = 'recognize_done',
// 抽帧中
FRAME = 'frame',
// 抽帧完成,等待反推
FRAME_DONE = 'frame_done',
// 抽帧失败
FRAME_FAIL = 'frame_fail',
// 反推中
REVERSE = 'reverse',
// 反推失败
REVERSE_FAIL = 'reverse_fail',
// 反推完成,等待生成图片
REVERSE_DONE = 'reverse_done',
// 生成图片中
IMAGE = 'image',
// 图片生成完成,等待高清
IMAGE_DONE = 'image_done',
// 图片生成失败
IMAGE_FAIL = 'image_fail',
// 高清中
HD = 'hd',
// 高清失败
HD_FAIL = 'hd_fail',
// 高清完成,等待合成视频
HD_DONE = 'hd_done',
// 合成视频中
COMPOSING = 'composing',
// 合成视频完成
COMPOSING_DONE = 'composing_done',
// 合成视频失败
COMPOSING_FAIL = 'composing_fail',
/** 添加草稿完成 */
DRAFT_DONE = 'draft_done',
/** 添加草稿失败 */
DRAFT_FAIL = 'draft_fail',
/** 图转视频失败 */
IMAGE_TO_VIDEO_ERROR = 'image_to_video_error',
/** 图转视频成功 */
IMAGE_TO_VIDEO_SUCCESS = 'IMAGE_TO_VIDEO_SUCCESS'
}
export enum TagDefineType {
// 默认风格
DEFAULT_STYLE = 'default_style',
// 角色标签
CHARACTER_MAIN = 'character_main',
// 角色小标签
CHARACTER_SUB = 'min',
// 风格主标签
STYLE_MAIN = 'style_main',
// 场景主标签
SCENE_MAIN = 'scene_main'
}
export enum OperateBookType {
BOOK = 'book', // 这个小说的所有批次
BOOKTASK = 'bookTask', // 整个小说批次分镜合并
BOOKTASKDETAIL = 'bookTaskDetail', // 单个分镜合并
UNDERBOOKTASK = 'underBookTask', // 执行小说批次任务的指定ID以及后面的所有的东西
ASSIGNBOOKTASK = 'assignBookTask' // 指定小说批次任务
}
export enum CopyImageType {
// 所有,包括原图
ALL = 'all',
// 出原图外其他,一个个对应
ONE = 'one',
// 不包含图
NONE = 'none'
}
export enum PromptMergeType {
// mj 合并
MJ_MERGE = 'mj_merge',
// SD 合并
SD_MERGE = 'sd_merge',
// D3 合并
D3_MERGE = 'd3_merge'
}
/**
* 小说数据替换类型
*/
export enum BookRepalceDataType {
// 文案
AFTER_GPT = 'after_gpt',
// GPT提示词
GPT_PROMPT = 'gpt_prompt',
// 提示词
PROMPT = 'prompt'
}
/**
* 小说选择标签的方式类型,可以是下拉和标签
*/
export enum BookTagSelectType {
// 下拉
DROP = 'drop',
// 标签
TAG = 'tag'
}
/**
* 根据Key返回指定的后台任务类型的label
* @param key
* @returns
*/
export function GetBookBackTaskTypeLabel(key: string) {
switch (key) {
case BookBackTaskType.STORYBOARD:
return '分镜计算'
case BookBackTaskType.SPLIT:
return '分割视频'
case BookBackTaskType.AUDIO:
return '提取音频'
case BookBackTaskType.RECOGNIZE:
return '识别字幕'
case BookBackTaskType.FRAME:
return '抽帧'
case BookBackTaskType.MJ_REVERSE:
return 'MJ反推'
case BookBackTaskType.SD_REVERSE:
return 'SD反推'
case BookBackTaskType.MJ_IMAGE:
return 'MJ生成图片'
case BookBackTaskType.SD_IMAGE:
return 'SD生成图片'
case BookBackTaskType.FLUX_FORGE_IMAGE:
return 'flux forge生成图片'
case BookBackTaskType.FLUX_API_IMAGE:
return 'flux api生成图片'
case BookBackTaskType.D3_IMAGE:
return 'D3生成图片'
case BookBackTaskType.HD:
return '高清'
case BookBackTaskType.COMPOSING:
return '合成视频'
case BookBackTaskType.INFERENCE:
return '推理'
case BookBackTaskType.TRANSLATE:
return '翻译'
case BookBackTaskType.RUNWAY_VIDEO:
return 'runway生成视频'
case BookBackTaskType.LUMA_VIDEO:
return 'luma生成视频'
case BookBackTaskType.KLING_VIDEO:
return 'kling生成视频'
default:
return key
}
}
/**
* 根据Key返回指定的后台任务状态的label
* @param key
* @returns
*/
export function GetBookTaskDetailStatusLabel(key: string): TaskModal.TaskStatus {
switch (key) {
case BookTaskStatus.WAIT:
return {
status: BookTaskStatus.WAIT,
label: '等待',
type: 'warning'
}
case BookTaskStatus.STORYBOARD:
return {
status: BookTaskStatus.STORYBOARD,
label: '分镜计算中',
type: 'info'
}
case BookTaskStatus.STORYBOARD_FAIL:
return {
status: BookTaskStatus.STORYBOARD_FAIL,
label: '分镜计算失败',
type: 'error'
}
case BookTaskStatus.STORYBOARD_DONE:
return {
status: BookTaskStatus.STORYBOARD_DONE,
label: '分镜计算完成',
type: 'success'
}
case BookTaskStatus.SPLIT:
return {
status: BookTaskStatus.SPLIT,
label: '分割视频中',
type: 'info'
}
case BookTaskStatus.SPLIT_FAIL:
return {
status: BookTaskStatus.SPLIT_FAIL,
label: '分割视频失败',
type: 'error'
}
case BookTaskStatus.SPLIT_DONE:
return {
status: BookTaskStatus.SPLIT_DONE,
label: '分割视频完成',
type: 'success'
}
case BookTaskStatus.AUDIO:
return {
status: BookTaskStatus.AUDIO,
label: '提取音频中',
type: 'info'
}
case BookTaskStatus.AUDIO_FAIL:
return {
status: BookTaskStatus.AUDIO_FAIL,
label: '提取音频失败',
type: 'error'
}
case BookTaskStatus.AUDIO_DONE:
return {
status: BookTaskStatus.AUDIO_DONE,
label: '提取音频完成',
type: 'success'
}
case BookTaskStatus.RECOGNIZE:
return {
status: BookTaskStatus.RECOGNIZE,
label: '识别字幕中',
type: 'info'
}
case BookTaskStatus.RECOGNIZE_FAIL:
return {
status: BookTaskStatus.RECOGNIZE_FAIL,
label: '识别字幕失败',
type: 'error'
}
case BookTaskStatus.RECOGNIZE_DONE:
return {
status: BookTaskStatus.RECOGNIZE_DONE,
label: '识别字幕完成',
type: 'success'
}
case BookTaskStatus.FRAME:
return {
status: BookTaskStatus.FRAME,
label: '抽帧中',
type: 'info'
}
case BookTaskStatus.FRAME_FAIL:
return {
status: BookTaskStatus.FRAME_FAIL,
label: '抽帧失败',
type: 'error'
}
case BookTaskStatus.FRAME_DONE:
return {
status: BookTaskStatus.FRAME_DONE,
label: '抽帧完成',
type: 'success'
}
case BookTaskStatus.REVERSE:
return {
status: BookTaskStatus.REVERSE,
label: '反推中',
type: 'info'
}
case BookTaskStatus.REVERSE_FAIL:
return {
status: BookTaskStatus.REVERSE_FAIL,
label: '反推失败',
type: 'error'
}
case BookTaskStatus.REVERSE_DONE:
return {
status: BookTaskStatus.REVERSE_DONE,
label: '反推完成',
type: 'success'
}
case BookTaskStatus.IMAGE:
return {
status: BookTaskStatus.IMAGE,
label: '生成图片中',
type: 'info'
}
case BookTaskStatus.IMAGE_FAIL:
return {
status: BookTaskStatus.IMAGE_FAIL,
label: '生成图片失败',
type: 'error'
}
case BookTaskStatus.IMAGE_DONE:
return {
status: BookTaskStatus.IMAGE_DONE,
label: '生成图片完成',
type: 'success'
}
case BookTaskStatus.HD:
return {
status: BookTaskStatus.HD,
label: '高清中',
type: 'info'
}
case BookTaskStatus.HD_FAIL:
return {
status: BookTaskStatus.HD_FAIL,
label: '高清失败',
type: 'error'
}
case BookTaskStatus.HD_DONE:
return {
status: BookTaskStatus.HD_DONE,
label: '高清完成',
type: 'success'
}
case BookTaskStatus.COMPOSING:
return {
status: BookTaskStatus.COMPOSING,
label: '合成视频中',
type: 'info'
}
case BookTaskStatus.COMPOSING_FAIL:
return {
status: BookTaskStatus.COMPOSING_FAIL,
label: '合成视频失败',
type: 'error'
}
case BookTaskStatus.COMPOSING_DONE:
return {
status: BookTaskStatus.COMPOSING_DONE,
label: '合成视频完成',
type: 'success'
}
case BookTaskStatus.DRAFT_DONE:
return {
status: BookTaskStatus.DRAFT_DONE,
label: '添加草稿完成',
type: 'success'
}
case BookTaskStatus.DRAFT_FAIL:
return {
status: BookTaskStatus.DRAFT_FAIL,
label: '添加草稿失败',
type: 'error'
}
case BookTaskStatus.IMAGE_TO_VIDEO_ERROR:
return {
status: BookTaskStatus.IMAGE_TO_VIDEO_ERROR,
label: '图转视频失败',
type: 'error'
}
case BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS:
return {
status: BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS,
label: '图转视频成功',
type: 'success'
}
default:
return {
status: 'UNKNOWN',
label: '未知',
type: 'warning'
}
}
}
/**
* 根据Key返回指定的后台任务状态的label
* @param key
* @returns
*/
export function GetBookBackTaskStatusLabel(key: string): TaskModal.TaskStatus {
switch (key) {
case BookBackTaskStatus.WAIT:
return {
status: BookBackTaskStatus.WAIT,
label: '等待',
type: 'warning'
}
case BookBackTaskStatus.RUNNING:
return {
status: BookBackTaskStatus.RUNNING,
label: '运行中',
type: 'info'
}
case BookBackTaskStatus.PAUSE:
return {
status: BookBackTaskStatus.PAUSE,
label: '暂停',
type: 'warning'
}
case BookBackTaskStatus.DONE:
return {
status: BookBackTaskStatus.DONE,
label: '完成',
type: 'success'
}
case BookBackTaskStatus.FAIL:
return {
status: BookBackTaskStatus.FAIL,
label: '失败',
type: 'error'
}
case BookBackTaskStatus.RECONNECT:
return {
status: BookBackTaskStatus.RECONNECT,
label: '重连',
type: 'warning'
}
default:
return {
status: 'UNKNOWN',
label: '未知',
type: 'warning'
}
}
}
+51
View File
@@ -0,0 +1,51 @@
/**
* 剪映关键帧类型枚举
*
* 定义了剪映视频编辑中可用的各种关键帧类型,用于控制素材的动画效果。
* 这些关键帧类型可用于创建视频素材的移动、缩放等动态效果。
*/
export enum JianyingKeyFrameEnum {
/** 上下关键帧 */
KFTypePositionY = 'KFTypePositionY',
/** 左右关键帧 */
KFTypePositionX = 'KFTypePositionX',
/** 缩放关键帧 */
KFTypeScale = 'KFTypeScale',
/** 随机关键帧 */
KFTypeRandom = 'KFTypeRandom'
}
/**
* 获取剪映关键帧类型的选项列表
*
* 返回格式化后的关键帧类型选项数组,适用于下拉菜单、选择器等UI组件。
* 每个选项包含人类可读的标签和对应的枚举值。
*
* @returns {Array<{label: string, value: JianyingKeyFrameEnum}>} 关键帧类型选项数组
*
* @example
* // 在组件中使用
* const options = getJianyingKeyFrameOption();
* // 返回的数据可直接用于下拉选择组件
* <n-select :options="options" v-model="selectedKeyFrameType" />
*/
export function getJianyingKeyFrameOptions(): Array<{ label: string; value: JianyingKeyFrameEnum }> {
return [
{
label: '上下关键帧',
value: JianyingKeyFrameEnum.KFTypePositionY
},
{
label: '左右关键帧',
value: JianyingKeyFrameEnum.KFTypePositionX
},
{
label: '缩放关键帧',
value: JianyingKeyFrameEnum.KFTypeScale
},
{
label: '随机关键帧',
value: JianyingKeyFrameEnum.KFTypeRandom
}
]
}
+6
View File
@@ -0,0 +1,6 @@
export enum LoggerLevel {
Info = 'info',
Warn = 'warn',
Error = 'error',
Success = 'success'
}
+46
View File
@@ -0,0 +1,46 @@
export enum MJImageType {
//本地MJ
LOCAL_MJ = 'local_mj',
// 代理MJ
REMOTE_MJ = 'remote_mj',
// 浏览器模式
BROWSER_MJ = 'browser_mj',
// API模式
API_MJ = 'api_mj',
// 本地 SD
LOCAL_SD = 'local_sd',
// ComfyUI
ComfyUI = 'comfyui',
// flux-api
FLUX_API = 'flux-api',
// flxu-forge
FLUX_FORGE = 'flux-forge',
// 导入
IMPORT = 'import'
}
export enum MJRespoonseType {
// 创建
CREATED = 'created',
// 更新
UPDATED = 'updated',
// 完成
FINISHED = 'finished',
// 删除
DELETE = 'delete'
}
export enum MJAction {
// 出图
IMAGINE = 'IMAGINE',
// 反推
DESCRIBE = 'DESCRIBE'
}
+80
View File
@@ -0,0 +1,80 @@
/**
* Option Value的数据类型,用于数据的格式化
*/
export enum OptionType {
STRING = 'string',
NUMBER = 'number',
BOOLEAN = 'boolean',
JSON = 'json'
}
/**
* 将字符串转换为指定类型的值
* @param value 要转换的字符串值
* @param type 目标类型 ('string'|'number'|'boolean'|'json')
* @returns 转换后的值
*/
export function getOptionType(type: number): OptionType {
switch (type) {
case 1:
return OptionType.STRING
case 2:
return OptionType.JSON
case 3:
return OptionType.NUMBER
case 4:
return OptionType.BOOLEAN
default:
throw new Error(`Unsupported type: ${type}`)
}
}
/**
* Option Key的名称
*/
export const OptionKeyName = {
Software: {
/** 机器码ID */
MachineId: 'Software_MachineId',
/** 软件授权码 */
AuthorizationCode: 'Software_AuthorizationCode',
/** 外观设置 */
AppearanceSetting: 'Software_AppearanceSetting',
/** 项目文件地址 */
ProjectPath: 'Software_ProjectPath',
/** 常规设置 */
GeneralSetting: 'Software_GeneralSetting',
/** 剪映关键帧设置 */
JianyingKeyFrameSetting: 'Software_JianyingKeyFrameSetting'
},
Midjourney: {
/** MJ基础设置 */
GeneralSetting: 'MJ_GeneralSetting',
/** MJ API设置 */
ApiSetting: 'MJ_ApiSetting'
},
InferenceAI: {
/** InferenceAI设置 人工智能AI推理 */
InferenceSetting: 'InferenceAI_InferenceSetting',
/** 分镜AI模型 */
StoryBoardAIModel: 'InferenceAI_StoryBoardAIModel'
},
SD: {
/** SD基础设置 */
SDImageSetting: 'SD_SDImageSetting',
/** SD修脸修手设置 */
SDADetailerSetting: 'SD_SDADetailerSetting',
/** SDSampler设置 */
SDSamplers: 'SD_SDSamplers',
/** SD的模型 */
SDModels: 'SD_SDModels',
/** SD Lora */
SDLoras: 'SD_Loras'
}
}
+89
View File
@@ -0,0 +1,89 @@
export enum SoftwareThemeType {
// 暗
DARK = 'dark',
// 亮
LIGHT = 'light'
}
export enum ComponentSize {
// 极小
TINY = 'tiny',
// 小
SMALL = 'small',
// 中
MEDIUM = 'medium',
// 大
LARGE = 'large'
}
export enum LoggerType {
// SD反推
SD_REVERSE = 'sd_reverse',
// 原创
ORIGINAL = 'original',
// MJ反推
MJ_REVERSE = 'mj_reverse'
}
export enum LoggerStatus {
// 成功
SUCCESS = 'success',
// 失败
FAIL = 'fail',
// 进行中
DOING = 'doing'
}
export enum OtherData {
// 未知
UNKNOWN = 'unknown',
//默认
DEFAULT = 'default'
}
export enum SoftColor {
// 棕黄色
BROWN_YELLOW = '#e18a3b',
// 橘色
ORANGE = '#ee7959',
// 朱颜酡
ZHUYANTUO = '#f29a76',
// 错误红色
ERROR_RED = '#c8161d'
}
export enum ResponseMessageType {
GET_TEXT = 'getText', // 获取文案
REMOVE_WATERMARK = "REMOVE_WATERMARK",// 删除水印
MJ_REVERSE = 'MJ_REVERSE',// MJ反推,返回反推结果
SD_REVERSE = 'SD_REVERSE',// MJ反推,返回反推结果
REVERSE_PROMPT_TRANSLATE = 'REVERSE_PROMPT_TRANSLATE',// 反推提示词翻译
GPT_PROMPT_TRANSLATE = 'GPT_PROMPT_TRANSLATE', // GPT提示词翻译
MJ_IMAGE = 'MJ_IMAGE',// MJ 生成图片
ComfyUI_IMAGE = 'ComfyUI_IMAGE',// ComfyUI 生成图片
HD_IMAGE = 'HD_IMAGE',// HD 生成图片
RUNWAY_VIDEO = "RUNWAY_VIDEO",// Runway生成视频
LUMA_VIDEO = "LUMA_VIDEO",// Luma生成视频
KLING_VIDEO = "KLING_VIDEO",// Kling生成视频
VIDEO_SUCESS = "VIDEO_SUCESS" //视频生成成功
}
export enum LaiAPIType {
// 主要的
MAIN = "main",
// 香港代理
HK_PROXY = "hk-proxy",
// 备用站点
BAK_MAIN = 'bak-main'
}
// 同步GPTkey的分类
export enum SyncGptKeyType {
// 字幕设置
SUBTITLE_SETTING = 'subtitle_setting',
}
+25
View File
@@ -0,0 +1,25 @@
// 翻译类型(主要用于后端逻辑处理)
export enum TranslateType {
// 反推提示词翻译
REVERSE_PROMPT_TRANSLATE = 'reverse_prompt_translate',
// GPT提示词翻译
GPT_PROMPT_TRANSLATE = 'gpt_prompt_translate'
}
// 翻译API类型
export enum TranslateAPIType {
// 百度翻译
BAIDU = 'baidu',
// 腾讯翻译
TENCENT = 'tencent',
// 火山翻译
VOLCENGINE = 'volcengine',
// 阿里翻译
ALI = 'ali',
// Laitool GPT翻译
LAITOOL = 'laitool'
}
+17
View File
@@ -0,0 +1,17 @@
import { SystemIpc } from './subIpc/systemIpc'
import OptionIpc from './subIpc/optionIpc'
import SettingIpc from './subIpc/settingIpc'
import AxiosIpc from './subIpc/axiosIpc'
import BookIpc from './subIpc/bookIpc'
import PresetIpc from './subIpc/presetIpc'
import TaskIpc from './subIpc/taskIpc'
export function IpcStart() {
SystemIpc()
OptionIpc()
SettingIpc()
AxiosIpc()
BookIpc()
PresetIpc()
TaskIpc()
}
+85
View File
@@ -0,0 +1,85 @@
import { ipcMain } from 'electron'
import axios from 'axios'
import { DEFINE_STRING } from '@/define/ipcDefineString'
function AxiosIpc() {
// 通用 GET 请求
ipcMain.handle(DEFINE_STRING.AXIOS.HTTP_GET, async (_, url, config = {}) => {
try {
const response = await axios.get(url, config)
return {
success: true,
data: response.data,
status: response.status
}
} catch (error: any) {
return {
success: false,
error: error.message,
message: error.message,
status: error.response?.status,
data: error.response?.data
}
}
})
// 通用 POST 请求
ipcMain.handle(DEFINE_STRING.AXIOS.HTTP_POST, async (_, url, data = {}, config = {}) => {
try {
const response = await axios.post(url, data, config)
return {
success: true,
data: response.data,
status: response.status
}
} catch (error: any) {
return {
success: false,
error: error.message,
message: error.message,
status: error.response?.status,
data: error.response?.data
}
}
})
// 通用 PUT 请求
ipcMain.handle(DEFINE_STRING.AXIOS.HTTP_PUT, async (_, url, data = {}, config = {}) => {
try {
const response = await axios.put(url, data, config)
return {
success: true,
data: response.data,
status: response.status
}
} catch (error: any) {
return {
success: false,
error: error.message,
status: error.response?.status,
data: error.response?.data
}
}
})
// 通用 DELETE 请求
ipcMain.handle(DEFINE_STRING.AXIOS.HTTP_DELETE, async (_, url, config = {}) => {
try {
const response = await axios.delete(url, config)
return {
success: true,
data: response.data,
status: response.status
}
} catch (error: any) {
return {
success: false,
error: error.message,
status: error.response?.status,
data: error.response?.data
}
}
})
}
export default AxiosIpc
@@ -0,0 +1,37 @@
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { bookHandle } from '@/main/service/book'
import { ipcMain } from 'electron'
export function bookDataIpc() {
//#region 小说数据相关
/** 添加或修改小说信息 */
ipcMain.handle(
DEFINE_STRING.BOOK.ADD_OR_MODIFY_BOOK,
async (_, book) => await bookHandle.AddOrModifyBook(book)
)
/** 获取小说信息,通过参数查询 */
ipcMain.handle(
DEFINE_STRING.BOOK.GET_BOOK_DATA_CONDITION,
async (_, queryCondition) => await bookHandle.GetBookDataCondition(queryCondition)
)
/** 修改小说数据信息 */
ipcMain.handle(
DEFINE_STRING.BOOK.MODIFY_BOOK_DATA_BY_ID,
async (_, id, bookData) => await bookHandle.ModifyBookDataById(id, bookData)
)
/** 删除小说数据,通过IDs */
ipcMain.handle(
DEFINE_STRING.BOOK.DELETE_BOOK_DATA_INFO_BY_ID,
async (_, id: string) => await bookHandle.DeleteBookDataInfoById(id)
)
/** 重置小说数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.RESET_BOOK_DATA_INFO,
async (_, bookId: string) => await bookHandle.ResetBookDataInfo(bookId)
)
//#endregion
}
@@ -0,0 +1,15 @@
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { bookHandle } from '@/main/service/book'
import { ipcMain } from 'electron'
export function bookExportIpc() {
//#region 导出相关
/** 添加剪映草稿 */
ipcMain.handle(
DEFINE_STRING.BOOK.ADD_JIANYING_DRAFT,
async (_, bookTaskId: string) => await bookHandle.AddJianyingDraft(bookTaskId)
)
//#endregion
}
@@ -0,0 +1,52 @@
import { OperateBookType } from "@/define/enum/bookEnum"
import { DEFINE_STRING } from "@/define/ipcDefineString"
import { bookHandle } from "@/main/service/book"
import { ipcMain } from "electron"
export function bookImageIpc() {
//#region 出图相关
/** 移动图片到主图 */
ipcMain.handle(
DEFINE_STRING.BOOK.MOVE_IMAGE_TO_MAIN_IMAGE,
async (_, bookTaskId: string, bookTaskDetailId: string, sourceImagePath: string) =>
await bookHandle.MoveImageToMainImage(bookTaskId, bookTaskDetailId, sourceImagePath)
)
/** 重置(删除)所有未锁定的分镜图片数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.RESET_GENERATE_IMAGE,
async (_, id: string, operateBookType: OperateBookType) =>
await bookHandle.ResetGenerateImage(id, operateBookType)
)
/** 上传图片到分镜,并且更新出图信息 */
ipcMain.handle(
DEFINE_STRING.BOOK.UPLOAD_IMAGE_TO_BOOK_AND_UPDATE_MESSAGE,
async (_, bookTaskDetailId: string, imageFile: string | string[], option: string) =>
await bookHandle.UpLoadImageToBookAndUpdateMessage(bookTaskDetailId, imageFile, option)
)
/** 高清一个分镜 */
ipcMain.handle(
DEFINE_STRING.BOOK.HD_ONE_IMAGE,
async (_, bookTaskDetailId: string, scale: number) =>
await bookHandle.HDOneImage(bookTaskDetailId, scale)
)
/** 获取Midjourney图片URL并下载应用到分镜 */
ipcMain.handle(
DEFINE_STRING.BOOK.GET_IMAGE_URL_AND_DOWNLOAD,
async (_, id: string, operateBookType: OperateBookType, coverData: boolean) =>
await bookHandle.GetImageUrlAndDownload(id, operateBookType, coverData)
)
/** 下载图片并拆分处理应用到分镜 */
ipcMain.handle(
DEFINE_STRING.BOOK.DOWNLOAD_IMAGE_URL_AND_SPLIT,
async (_, bookTaskDetailId: string, imageUrl: string) =>
await bookHandle.DownloadImageUrlAndSplit(bookTaskDetailId, imageUrl)
)
//#endregion
}
@@ -0,0 +1,52 @@
import { PresetCategory } from '@/define/data/presetData'
import { OperateBookType, PromptMergeType } from '@/define/enum/bookEnum'
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { BookTask } from '@/define/model/book/bookTask'
import { TranslateModel } from '@/define/model/translate'
import { bookHandle } from '@/main/service/book'
import { ipcMain } from 'electron'
export function bookPromptIpc() {
//#region 提示词
/** 原创进行AI推理 */
ipcMain.handle(
DEFINE_STRING.BOOK.ORIGINAL_GET_AI_PROMPT,
async (_, id: string, operateBookType: OperateBookType, coverData: boolean) =>
await bookHandle.OriginalGetAiPrompt(id, operateBookType, coverData)
)
/** AI分镜头合并 */
ipcMain.handle(
DEFINE_STRING.BOOK.AI_STORYBOARD_MERGE,
async (_, bookTaskId: string, type: BookTask.StoryboardMergeType) =>
await bookHandle.AIStoryboardMerge(bookTaskId, type)
)
/** 合并提示词 */
ipcMain.handle(
DEFINE_STRING.BOOK.MERGE_PROMPT,
async (_, id: string, type: PromptMergeType, operateBookType: OperateBookType) =>
await bookHandle.MergePrompt(id, type, operateBookType)
)
/** 自动分析提示词或场景数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.AUTO_ANALYZE_CHARACTER_OR_SCENE,
async (_, bookTaskId: string, type: PresetCategory) =>
await bookHandle.AutoAnalyzeCharacterOrScene(bookTaskId, type)
)
//#endregion
//#region 翻译相关
/** 翻译小说分镜数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.TRANSLATE_BOOK_TASK_DETAIL_PROMPT_NOW_RETURN,
async (_, value: TranslateModel.TranslateNowIPCParams[]) =>
await bookHandle.TranslateBookTaskDetailPromptNowReturn(value)
)
//#endregion
}
@@ -0,0 +1,36 @@
import { DEFINE_STRING } from "@/define/ipcDefineString"
import { bookHandle } from "@/main/service/book"
import { ipcMain } from "electron"
export function bookTaskDetailIpc() {
//#region 小说批次任务详细数据相关
/** 获取小说子任务详细数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.GET_BOOK_TASK_DETAIL_DATA_CONDITION,
async (_, bookTaskDetailCondition) =>
await bookHandle.GetBookTaskDetailDataByCondition(bookTaskDetailCondition)
)
/** 获取小说子任务详细数据,通过小说ID查询 */
ipcMain.handle(
DEFINE_STRING.BOOK.GET_BOOK_TASK_DETAIL_DATA_BY_ID,
async (_, id) => await bookHandle.GetBookTaskDetailDataById(id)
)
/** 修改小说子任务详细数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.MODIFY_BOOK_TASK_DETAIL_BY_ID,
async (_, bookTaskDetailId, updateData) =>
await bookHandle.ModifyBookTaskDetailById(bookTaskDetailId, updateData)
)
/** 保持小说批次数据分镜信息 */
ipcMain.handle(
DEFINE_STRING.BOOK.SAVE_COPYWRITING_INFO,
async (_, bookTaskId, copywritingData, operateBookType) =>
await bookHandle.SaveCopywritingInfo(bookTaskId, copywritingData, operateBookType)
)
//#endregion
}
@@ -0,0 +1,66 @@
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { Book } from '@/define/model/book/book'
import { BookTask } from '@/define/model/book/bookTask'
import { bookHandle } from '@/main/service/book'
import { ipcMain } from 'electron'
export function bookTaskIpc() {
//#region 小说批次任务相关
/** 获取小说子任务数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.GET_BOOK_TASK_DATA_CONDITION,
async (_, bookTaskCondition) => await bookHandle.GetBookTaskDataByCondition(bookTaskCondition)
)
/** 修改小说数据,通过小说ID修改 */
ipcMain.handle(
DEFINE_STRING.BOOK.MODIFY_BOOK_TASK_DATA_BY_ID,
async (_, id, bookData: Book.SelectBookTask) =>
await bookHandle.ModifyBookTaskDataById(id, bookData)
)
/** 删除小说数据,通过小说ID删除 */
ipcMain.handle(
DEFINE_STRING.BOOK.DELETE_BOOK_TASK_BY_IDS,
async (_, ids: string[]) => await bookHandle.DeleteBookTaskByIds(ids)
)
/** 重置小说批次任务数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.RESET_BOOK_TASK_DATA_BY_ID,
async (_, bookTaskId: string) => await bookHandle.ResetBookTaskDataById(bookTaskId)
)
/** 重置小说分镜数据(不包含bootask本身数据),并且初始化全新的分镜信息 */
ipcMain.handle(
DEFINE_STRING.BOOK.RESET_AND_INITIALIZE_BOOK_TASK,
async (_, bookTaskId: string) => await bookHandle.ResetAndInitializeBookTask(bookTaskId)
)
/** 删除小说批次任务数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.DELETE_BOOK_TASK_DATA_BY_ID,
async (_, bookTaskId: string) => await bookHandle.DeleteBookTaskDataById(bookTaskId)
)
/** 添加小说子任务数据 */
ipcMain.handle(
DEFINE_STRING.BOOK.ADD_NEW_BOOK_TASK,
async (_, addData: BookTask.AddBookTaskParam) => await bookHandle.AddNewBookTask(addData)
)
/** 获取小说批次任务的图片生成进度 */
ipcMain.handle(
DEFINE_STRING.BOOK.GET_BOOK_TASK_IMAGE_GENERATE_PROGRESS,
async (_, bookId: string) => await bookHandle.GetBookTaskImageGenerateProgress(bookId)
)
/** 获取小说批次任务的第一张图片路径 */
ipcMain.handle(
DEFINE_STRING.BOOK.GET_BOOK_TASK_FIRST_IMAGE_PATH,
async (_, bookId: string) => await bookHandle.GetBookTaskFirstImagePath(bookId)
)
//#endregion
}
+28
View File
@@ -0,0 +1,28 @@
import { bookDataIpc } from './bookIPC/bookDataIpc'
import { bookTaskIpc } from './bookIPC/bookTaskIpc'
import { bookTaskDetailIpc } from './bookIPC/bookTaskDetailIpc'
import { bookPromptIpc } from './bookIPC/bookPromptIpc'
import { bookImageIpc } from './bookIPC/bookImageIpc'
import { bookExportIpc } from './bookIPC/bookExportIpc'
function BookIpc() {
// 小说数据相关
bookDataIpc()
//小说批次任务相关
bookTaskIpc()
// 小说任务详情相关
bookTaskDetailIpc()
// 小说提示词相关
bookPromptIpc()
// 小说图像相关
bookImageIpc()
// 小说导出相关
bookExportIpc()
}
export default BookIpc
+21
View File
@@ -0,0 +1,21 @@
import OptionService from '@/main/service/option/index'
import { DEFINE_STRING } from '../../ipcDefineString'
import { OptionType } from '../../enum/option'
import { ipcMain } from 'electron'
function OptionIpc() {
/** 获取指定的Option,通过key,不存在返回null */
ipcMain.handle(
DEFINE_STRING.OPTION.GET_OPTION_BY_KEY,
async (_, key: string) => await OptionService.GetOptionByKey(key)
)
/** 修改指定的Option,通过key,不存在则创建 */
ipcMain.handle(
DEFINE_STRING.OPTION.MODIFY_OPTION_BY_KEY,
async (_, key: string, value: string, type: OptionType) =>
await OptionService.ModifyOptionByKey(key, value, type)
)
}
export default OptionIpc
+40
View File
@@ -0,0 +1,40 @@
import { DEFINE_STRING } from '../../ipcDefineString'
import { ipcMain } from 'electron'
import { presetHandle } from '@/main/service/preset'
import { PresetModel } from '@/define/model/preset'
function PresetIpc() {
/** 通过条件获取预设 */
ipcMain.handle(
DEFINE_STRING.PRESET.GET_PRESET_BY_CONDITION,
async (_, queryCondition: PresetModel.Preset) =>
await presetHandle.GetPresetByCondition(queryCondition)
)
/** 通过ID获取预设 */
ipcMain.handle(
DEFINE_STRING.PRESET.GET_PRESET_BY_ID,
async (_, id: string) => await presetHandle.GetPresetById(id)
)
/** 添加预设 */
ipcMain.handle(
DEFINE_STRING.PRESET.ADD_PRESET,
async (_, preset: PresetModel.Preset) => await presetHandle.AddPreset(preset)
)
/** 修改预设 */
ipcMain.handle(
DEFINE_STRING.PRESET.MODIFY_PRESET,
async (_, id: string, preset: Partial<PresetModel.Preset>) =>
await presetHandle.ModifyPreset(id, preset)
)
/** 删除预设 */
ipcMain.handle(
DEFINE_STRING.PRESET.DELETE_PRESET,
async (_, id: string) => await presetHandle.DeletePreset(id)
)
}
export default PresetIpc
+13
View File
@@ -0,0 +1,13 @@
import { DEFINE_STRING } from '../../ipcDefineString'
import { ipcMain } from 'electron'
import SettingHandle from '@/main/service/setting/index'
function SettingIpc() {
/** 获取默认的剪映草稿地址 */
ipcMain.handle(
DEFINE_STRING.SETTING.GET_DEFAULT_JIANYING_DRAFT_PATH,
async () => await SettingHandle.GetDefaultJianyingDraftPath()
)
}
export default SettingIpc
+133
View File
@@ -0,0 +1,133 @@
import { app, ipcMain, BrowserWindow } from 'electron'
import { DEFINE_STRING } from '../../ipcDefineString'
import ElectronInterface, { OpenFolderParams } from '@/main/service/system/electronInterface'
import { UserSoftware } from '@/main/service/system/userSoftware'
import JianyingService from '@/main/service/jianying/jianyingService'
const electronInterface = new ElectronInterface()
const userSoftware = new UserSoftware()
const jianyingService = new JianyingService()
function SystemIpc() {
//#region 窗口相关
/**
* 设置当前窗口的标题
*/
ipcMain.on(DEFINE_STRING.SYSTEM.SET_TITLE, (e, data) => {
const webContents = e.sender
const wins = BrowserWindow.fromWebContents(webContents)
if (wins) {
wins.setTitle(data)
}
})
/**
* 检查当前的窗口是不是最大化
*/
ipcMain.handle(DEFINE_STRING.SYSTEM.IS_MAXIMIZED, (_) => {
const win = BrowserWindow.getFocusedWindow()
return win?.isMaximized()
})
/**
* 最小化当前窗口
*/
ipcMain.on(DEFINE_STRING.SYSTEM.MIN_WINDOW, (_) => {
const win = BrowserWindow.getFocusedWindow()
win?.minimize()
})
/**
* 退出当前应用
*/
ipcMain.on(DEFINE_STRING.SYSTEM.QUIT_APP, (_) => {
app.quit()
})
/**
* 打开开发者工具
*/
ipcMain.on(DEFINE_STRING.SYSTEM.OPEN_DEV_TOOLS, (_) => {
const win = BrowserWindow.getFocusedWindow()
win?.webContents.openDevTools()
})
//#endregion
/** 打开指定的URL */
ipcMain.handle(DEFINE_STRING.SYSTEM.OPEN_URL, async (_, url: string) => {
electronInterface.OpenUrl(url)
})
//#region 系统相关
/** * 切换主题 */
ipcMain.handle(
DEFINE_STRING.SYSTEM.TOGGLE_THEME,
async (_, theme: string) => await electronInterface.ToggleTheme(theme)
)
/** 同步授权信 */
ipcMain.on(
DEFINE_STRING.SYSTEM.SYNC_AUTHORIZATION,
async (_, am: SoftwareModal.SoftwareAuthorizationMessage) =>
await userSoftware.SyncAuthorization(am)
)
/** 获取剪映草稿文件列表 */
ipcMain.handle(
DEFINE_STRING.SYSTEM.GET_JIANYING_DRAFT_FILE_LIST,
async () => await jianyingService.GetJianyingDraftFileList()
)
//#endregion
//#region 文件相关
/** 选择多个文件,指定文件后缀 */
ipcMain.handle(
DEFINE_STRING.SYSTEM.SELECT_MULTIPLE_FILE,
async (_, extensions: string[]) => await electronInterface.SelectMultipleFile(extensions)
)
/**
* 选择单个文件,指定文件后缀
*/
ipcMain.handle(
DEFINE_STRING.SYSTEM.SELECT_SINGLE_FILE,
async (_, extensions: string[]) => await electronInterface.SelectSingleFile(extensions)
)
/** 选择单个文件夹 */
ipcMain.handle(DEFINE_STRING.SYSTEM.SELECT_SINGLE_FOLDER, async (_, defaultPath: string) => {
return await electronInterface.SelectSingleFolder(defaultPath)
})
/** 打开指定的文件夹 */
ipcMain.handle(
DEFINE_STRING.SYSTEM.OPEN_FOLDER,
async (_, params: OpenFolderParams) => await electronInterface.OpenFolder(params)
)
/** 打开指定的文件 */
ipcMain.handle(
DEFINE_STRING.SYSTEM.OPEN_FILE,
async (_, filePath: string) => await electronInterface.OpenFile(filePath)
)
/** 复制指定的文件夹中的所有内容到另一个文件夹 */
ipcMain.handle(
DEFINE_STRING.SYSTEM.COPY_FOLDER_CONTENTS,
async (_, { source, destination }) =>
await electronInterface.CopyFolderContents(source, destination)
)
/** 选择文件夹或指定后缀的文件 */
ipcMain.handle(
DEFINE_STRING.SYSTEM.SELECT_FOLDER_OR_FILE,
async (_, extensions?: string[]) => await electronInterface.SelectFolderOrFile(extensions)
)
//#endregion
}
export { SystemIpc }
+62
View File
@@ -0,0 +1,62 @@
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { ipcMain } from 'electron'
import { TaskHandle } from '@/main/service/task'
import { BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
let taskHandle: TaskHandle = new TaskHandle()
function TaskIpc() {
/** 启动后台任务 */
ipcMain.handle(
DEFINE_STRING.TASK.START_TASK_QUEUE,
async (_, isGiveUp) => await taskHandle.StartTaskQueue(isGiveUp)
)
/** 添加单个个任务 */
ipcMain.handle(
DEFINE_STRING.TASK.ADD_ONE_TASK,
async (
_,
bookId: string,
taskType: BookBackTaskType,
executeType: TaskExecuteType = TaskExecuteType.AUTO,
bookTaskId: string | undefined = undefined,
bookTaskDetailId: string | undefined = undefined,
responseMessageName?: string
) =>
await taskHandle.AddOneTask(
bookId,
taskType,
executeType,
bookTaskId,
bookTaskDetailId,
responseMessageName
)
)
/** 添加多个任务 */
ipcMain.handle(
DEFINE_STRING.TASK.ADD_MULTI_TASK,
async (_, tasks) => await taskHandle.AddMultiTask(tasks)
)
/** 获取指定的状态的任务 */
ipcMain.handle(
DEFINE_STRING.TASK.GET_ASSIGN_STATUS_TASK_COUNT,
async (_, status) => await taskHandle.GetAssignStatusTaskCount(status)
)
/** 获取任务集合 */
ipcMain.handle(
DEFINE_STRING.TASK.GET_TASK_COLLECTION,
async (_, queryTaskCondition) => await taskHandle.GetTaskCollection(queryTaskCondition)
)
/** 更新后台任务状态 */
ipcMain.handle(
DEFINE_STRING.TASK.UPDATE_TASK_STATUS,
async (_, bookBackTask) => await taskHandle.UpdateTaskStatus(bookBackTask)
)
}
export default TaskIpc
+19
View File
@@ -0,0 +1,19 @@
import LOGGER from './subDefineString/loggerDefineString'
import OPTION from './subDefineString/optionDefineString'
import SYSTEM from './subDefineString/systemDefineString'
import SETTING from './subDefineString/settingDefineString'
import AXIOS from './subDefineString/axiosDefineString'
import BOOK from './subDefineString/bookDefineString'
import PRESET from './subDefineString/presetDefineString'
import TASK from './subDefineString/taskDefineString'
export const DEFINE_STRING = {
OPTION: OPTION,
SYSTEM: SYSTEM,
LOGGER: LOGGER,
SETTING: SETTING,
AXIOS: AXIOS,
BOOK: BOOK,
PRESET: PRESET,
TASK: TASK
}
@@ -0,0 +1,12 @@
const AXIOS = {
/** http的get请求 */
HTTP_GET: 'http:get',
/** http的post请求 */
HTTP_POST: 'http:post',
/** http的put请求 */
HTTP_PUT: 'http:put',
/** http的delete请求 */
HTTP_DELETE: 'http:delete'
}
export default AXIOS
@@ -0,0 +1,145 @@
const BOOK = {
//#region 小说数据相关
/** 新增或者是修改小说数据 */
ADD_OR_MODIFY_BOOK: 'ADD_OR_MODIFY_BOOK',
/** 获取小说数据,通过参数查询 */
GET_BOOK_DATA_CONDITION: 'GET_BOOK_DATA_CONDITION',
/** 修改小说数据信息 */
MODIFY_BOOK_DATA_BY_ID: 'MODIFY_BOOK_DATA_BY_ID',
/** 删除小说数据,通过ID */
DELETE_BOOK_DATA_INFO_BY_ID: 'DELETE_BOOK_DATA_INFO_BY_ID',
/** 重置小说数据 */
RESET_BOOK_DATA_INFO: 'RESET_BOOK_DATA_INFO',
//#endregion
//#region 小说批次任务相关
/** 获取小说批次任务数据,通过参数查询 */
GET_BOOK_TASK_DATA_CONDITION: 'GET_BOOK_TASK_DATA_CONDITION',
/** 修改小说批次数据,通过ID */
MODIFY_BOOK_TASK_DATA_BY_ID: 'MODIFY_BOOK_TASK_DATA_BY_ID',
/** 删除指定ID得小说子任务 */
DELETE_BOOK_TASK_BY_IDS: 'DELETE_BOOK_TASK_BY_IDS',
/** 重置小说批次任务数据 */
RESET_BOOK_TASK_DATA_BY_ID: 'RESET_BOOK_TASK_DATA_BY_ID',
/** 重置小说分镜数据(不包含bootask本身数据),并且初始化全新的分镜信息 */
RESET_AND_INITIALIZE_BOOK_TASK: 'RESET_AND_INITIALIZE_BOOK_TASK',
/** 删除小说批次任务数据 */
DELETE_BOOK_TASK_DATA_BY_ID: 'DELETE_BOOK_TASK_DATA_BY_ID',
/** 添加小说子任务数据 */
ADD_NEW_BOOK_TASK: 'ADD_NEW_BOOK_TASK',
/** 获取小说批次任务的图片生成进度 */
GET_BOOK_TASK_IMAGE_GENERATE_PROGRESS: 'GET_BOOK_TASK_IMAGE_GENERATE_PROGRESS',
/** 获取小说批次任务的第一张图片路径 */
GET_BOOK_TASK_FIRST_IMAGE_PATH: 'GET_BOOK_TASK_FIRST_IMAGE_PATH',
//#endregion
//#region 小说批次任务详细数据相关
/** 获取小说子任务详细数据 */
GET_BOOK_TASK_DETAIL_DATA_CONDITION: 'GET_BOOK_TASK_DETAIL_DATA_CONDITION',
/** 获取小说子任务详细数据,通过小说ID查询 */
GET_BOOK_TASK_DETAIL_DATA_BY_ID: 'GET_BOOK_TASK_DETAIL_DATA_BY_ID',
/** 修改小说批次任务状态 */
MODIFY_BOOK_TASK_DETAIL_BY_ID: 'MODIFY_BOOK_TASK_DETAIL_BY_ID',
/** 保持小说批次数据分镜信息 */
SAVE_COPYWRITING_INFO: 'SAVE_COPYWRITING_INFO',
//#endregion
//#region 提示词
/** 原创进行AI推理 */
ORIGINAL_GET_AI_PROMPT: 'ORIGINAL_GET_AI_PROMPT',
/** AI分镜头合并 */
AI_STORYBOARD_MERGE: 'AI_STORYBOARD_MERGE',
/** 原创获取AI提示词返回 */
ORIGINAL_GET_AI_PROMPT_RETURN: 'ORIGINAL_GET_AI_PROMPT_RETURN',
/** 合并提示词 */
MERGE_PROMPT: 'MERGE_PROMPT',
/** 自动分析提示词或场景数据 */
AUTO_ANALYZE_CHARACTER_OR_SCENE: 'AUTO_ANALYZE_CHARACTER_OR_SCENE',
//#endregion
//#region 翻译相关
/** 翻译小说详细任务提示词数据 */
TRANSLATE_BOOK_TASK_DETAIL_PROMPT_NOW_RETURN: 'TRANSLATE_BOOK_TASK_DETAIL_PROMPT_NOW_RETURN',
/** 翻译小说详细任务提示词数据监听 */
TRANSLATE_BOOK_TASK_DETAIL_PROMPT_NOW_RETURN_Listen:
'TRANSLATE_BOOK_TASK_DETAIL_PROMPT_NOW_RETURN_Listen',
//#endregion
//#region 出图相关
/** MJ生图返回数据 */
MJ_IMAGE_GENERATE_RETURN: 'MJ_IMAGE_GENERATE_RETURN',
/** SD生图返回数据 */
SD_IMAGE_GENERATE_RETURN: 'SD_IMAGE_GENERATE_RETURN',
/** ComfyUI 生图返回数据 */
ComfyUI_IMAGE_GENERATE_RETURN: 'ComfyUI_IMAGE_GENERATE_RETURN',
/** D3 生图返回数据 */
D3_IMAGE_GENERATE_RETURN: 'D3_IMAGE_GENERATE_RETURN',
/** Flux API 生图返回数据 */
FLUX_API_IMAGE_GENERATE_RETURN: 'FLUX_API_IMAGE_GENERATE_RETURN',
/** Flux Forge 生图数据返回 */
FLUX_FORGE_IMAGE_GENERATE_RETURN: 'FLUX_FORGE_IMAGE_GENERATE_RETURN',
/** 移动图片到主图 */
MOVE_IMAGE_TO_MAIN_IMAGE: 'MOVE_IMAGE_TO_MAIN_IMAGE',
/** 重置生成图片数据 */
RESET_GENERATE_IMAGE: 'RESET_GENERATE_IMAGE',
/** 上传图片到分镜,并且更新出图信息 */
UPLOAD_IMAGE_TO_BOOK_AND_UPDATE_MESSAGE: 'UPLOAD_IMAGE_TO_BOOK_AND_UPDATE_MESSAGE',
/** 高清一个分镜 */
HD_ONE_IMAGE: 'HD_ONE_IMAGE',
/**获取Midjourney图片URL并下载应用到分镜 */
GET_IMAGE_URL_AND_DOWNLOAD: 'GET_IMAGE_URL_AND_DOWNLOAD',
/** 下载图片并拆分处理应用到分镜 */
DOWNLOAD_IMAGE_URL_AND_SPLIT: 'DOWNLOAD_IMAGE_URL_AND_SPLIT',
//#endregion
//#region 视频相关
/** 添加剪映草稿 */
ADD_JIANYING_DRAFT: 'ADD_JIANYING_DRAFT'
//#endregion
}
export default BOOK
@@ -0,0 +1,6 @@
const LOGGER = {
/** 添加日志消息 */
ADD_LOGGER: 'ADD_LOGGER'
}
export default LOGGER
@@ -0,0 +1,9 @@
const OPTION = {
/** 获取指定的Option,通过key,不存在返回null */
GET_OPTION_BY_KEY: 'GET_OPTION_BY_KEY',
/** 修改指定的Option,通过key,不存在则创建 */
MODIFY_OPTION_BY_KEY: 'MODIFY_OPTION_BY_KEY'
}
export default OPTION
@@ -0,0 +1,14 @@
const PRESET = {
/** 通过ID获取预设 */
GET_PRESET_BY_ID: 'GET_PRESET_BY_ID',
/** 获取预设通过条件 */
GET_PRESET_BY_CONDITION: 'GET_PRESET_BY_CONDITION',
/** 添加预设 */
ADD_PRESET: 'ADD_PRESET',
/** 修改预设 */
MODIFY_PRESET: 'MODIFY_PRESET',
/** 删除预设 */
DELETE_PRESET: 'DELETE_PRESET'
}
export default PRESET
@@ -0,0 +1,6 @@
const SETTING = {
/** 获取默认的剪映草稿地址 */
GET_DEFAULT_JIANYING_DRAFT_PATH: 'GET_DEFAULT_JIANYING_DRAFT_PATH'
}
export default SETTING
@@ -0,0 +1,67 @@
const SYSTEM = {
//#region 系统相关
/** 设置窗口的标题 */
SET_TITLE: 'SET_TITLE',
/** 检查当前窗口是不是最大化 */
IS_MAXIMIZED: 'IS_MAXIMIZED',
/** 最小化当前 */
MIN_WINDOW: 'MIN_WINDOW',
/** 退出应用 */
QUIT_APP: 'QUIT_APP',
/** 打开开发者工具 */
OPEN_DEV_TOOLS: 'OPEN_DEV_TOOLS',
//#endregion
//#region 系统相关
/** 切换主题 */
TOGGLE_THEME: 'TOGGLE_THEME',
/** 同步授权信息 */
SYNC_AUTHORIZATION: 'SYNC_AUTHORIZATION',
/** 获取剪映草稿文件列表 */
GET_JIANYING_DRAFT_FILE_LIST: 'GET_JIANYING_DRAFT_FILE_LIST',
//#endregion
//#region 文件相关
/** 选择多个文件,指定文件后缀 */
SELECT_MULTIPLE_FILE: 'SELECT_MULTIPLE_FILE',
/** 选择单个文件,指定文件后缀 */
SELECT_SINGLE_FILE: 'SELECT_SINGLE_FILE',
/** 选择单个文件夹 */
SELECT_SINGLE_FOLDER: 'SELECT_SINGLE_FOLDER',
/** 打开指定的文件夹 */
OPEN_FOLDER: 'OPEN_FOLDER',
/** 打开指定的文件 */
OPEN_FILE: 'OPEN_FILE',
/** 复制指定的文件夹中的所有内容到另一个文件夹 */
COPY_FOLDER_CONTENTS: 'COPY_FOLDER_CONTENTS',
/** 选择文件夹或指定后缀的文件 */
SELECT_FOLDER_OR_FILE: 'SELECT_FOLDER_OR_FILE',
//#endregion
/** 打开指定的url */
OPEN_URL: 'OPEN_URL',
//#region 通知相关
/** 显示全局的 message */
SHOW_MESSAGE_DIALOG: 'SHOW_MESSAGE_DIALOG',
/** 显示全局的 NOTIFICATION */
SHOW_MAIN_NOTIFICATION: 'SHOW_MAIN_NOTIFICATION'
//#endregion
}
export default SYSTEM
@@ -0,0 +1,20 @@
const TASK = {
/* 启动任务队列 */
START_TASK_QUEUE: 'START_TASK_QUEUE',
/* 添加单个个任务 */
ADD_ONE_TASK: 'ADD_ONE_TASK',
/** 添加多个任务 */
ADD_MULTI_TASK: 'ADD_MULTI_TASK',
/** 获取指定的状态的任务 */
GET_ASSIGN_STATUS_TASK_COUNT: 'GET_ASSIGN_STATUS_TASK_COUNT',
/** 获取任务集合 */
GET_TASK_COLLECTION: 'GET_TASK_COLLECTION',
/** 更新后台任务状态 */
UPDATE_TASK_STATUS: 'UPDATE_TASK_STATUS'
}
export default TASK
View File
+44
View File
@@ -0,0 +1,44 @@
declare namespace OpenAIRequest {
/**
* AI聊天消息接口
*
* 定义AI聊天中单条消息的结构
*/
interface RequestMessage {
/** 消息角色类型 */
role: 'system' | 'user' | 'assistant'
/** 消息内容 */
content: string
}
/**
* AI词语合并配置接口
*
* 定义AI分镜师进行文案分镜头合并时的完整配置参数
* 用于文案的智能分镜处理
*/
interface Request {
/** AI模型名称,指定使用的AI模型 */
model?: string
/** 是否启用流式输出 */
stream: boolean
/** 温度参数,控制生成内容的随机性 (0-1) */
temperature: number
/** 消息列表,包含系统提示和用户输入 */
messages: AIMessage[]
}
/**
* AI配置基础接口
*
* 定义所有AI相关配置的通用属性
*/
interface RequestBase {
/** AI模型名称 */
model: string
/** 是否启用流式输出 */
stream: boolean
/** 温度参数 (0-1) */
temperature: number
}
}
+343
View File
@@ -0,0 +1,343 @@
import { BookTaskDetailModel } from '@/define/db/model/bookTaskDetail'
import {
BookBackTaskStatus,
BookBackTaskType,
BookTaskStatus,
BookType,
TaskExecuteType,
BookRepalceDataType
} from '../../enum/bookEnum'
import { MJAction } from '../../enum/bookEnum'
import { BookTaskDetail } from './bookTaskDetail'
import { ImageCategory } from '@/define/data/imageData'
import { ImageGenerateMode } from '@/define/data/mjData'
declare namespace Book {
//#region 小说相关
/**
* 小说数据类型
*/
type SelectBook = {
id?: string
no?: number
name?: string
bookFolderPath?: string
type?: BookType
oldVideoPath?: string
srtPath?: string
audioPath?: string
draftDepend?: string // 草稿依赖
imageFolder?: string
draftSrtStyle?: string | null // 草稿字幕样式
backgroundMusic?: string | null // 背景音乐ID
friendlyReminder?: string | null // 友情提示
subtitlePosition?: string
watermarkPosition?: string
updateTime?: Date
createTime?: Date
version?: string
imageStyle?: string[] | null // 软件内置的样式
autoAnalyzeCharacter?: string | null // 自动分析角色设置
customizeImageStyle?: string[] | null // 自定义的样式
videoConfig?: string | null // 合成视频设置
prefixPrompt?: string | null // 前缀
suffixPrompt?: string | null // 后缀
}
/**
* 查询小说的条件
*/
type QueryBookCondition = {
id?: string
name?: string
page?: number
pageSize?: number
type?: BookType
}
/**
* 查询小说的返回类型
*/
type QueryBookConditionResponse = {
resultBooks: SelectBook[]
bookLength: number
}
//#endregion
//#region 小说任务相关
/**
* 小说批次任务类型
*/
type SelectBookTask = {
no?: number
id?: string
bookId?: string
name?: string
generateVideoPath?: string
srtPath?: string
audioPath?: string
draftDepend?: string
draftSrtStyle?: string | null // 草稿字幕样式
backgroundMusic?: string | null // 背景音乐ID
friendlyReminder?: string | null // 友情提示
imageFolder?: string
customizeImageStyle?: string[]
cacheImageList?: string[]
prefixPrompt?: string
suffixPrompt?: string
styleList?: BookStyle[] | DefineBookStyle[] // 样式列表,
status?: BookTaskStatus
errorMsg?: string
version?: string
imageCategory?: ImageCategory // 出图方式
imageStyle?: string[] | null // 软件内置的样式
autoAnalyzeCharacter?: string | null // 自动分析角色设置
customizeImageStyle?: string[] | null // 自定义的样式
videoConfig?: string | null // 合成视频设置
prefixPrompt?: string | null // 前缀
suffixPrompt?: string | null // 后缀
isAuto?: boolean // 是否标记全自动
subImageFolder?: string[] | null // 子图片文件夹地址,多个
openVideoGenerate?: boolean // 是否开启视频生成
createTime?: Date
updateTime?: Date
}
/**
* 查询小说批次任务的条件
*/
type QueryBookTaskCondition = {
id?: string
no?: number
name?: string
bookId?: string
bookTaskId?: string
page?: number
pageSize?: number
}
/**
* 查询小说人物的返回数据类型
*/
type QueryBookTaskConditionResponse = {
bookTasks: SelectBookTask[]
bookTaskLength: number
}
//#endregion
//#region 小说任务详细数据相关
type SelectBookTaskDetail = {
id?: string
no?: number
name?: string
bookId?: string
bookTaskId?: string
videoPath?: string // 视频地址
generateVideoPath?: string // 生成的视频地址
audioPath?: string // 音频地址
draftDepend?: string // 草稿依赖
word?: string // 文案
oldImage?: string // 旧图片(用于SD的图生图)
afterGpt?: string // GPT生成的文案
startTime?: number // 开始时间
endTime?: number // 结束时间
timeLimit?: string // 事件实现(0 -- 3000
subValue?: BookTaskDetail.CopywritingSubValue[] | string // 包含的字幕数据
characterTags?: string[] // 角色标签
sceneTags?: string[] // 场景标签
styleTags?: string[] // 风格标签
promptSort?: string // 提示词排序
gptPrompt?: string // GPT提示词
mjMessage?: MJMessage // MJ消息
videoMessage?: BookTaskDetail.VideoMessage // 视频消息
outImagePath?: string // 输出图片地址
subImagePath?: string[] // 子图片地址
imageLock?: boolean // 图片锁
reversePrompt?: ReversePrompt[] // 反推的提示词数据
prompt?: string // 提示
adetailer?: boolean // 是否开启修脸
sdConifg?: SDConfig // SD配置
subtitlePosition?: string // 字幕位置
status?: BookTaskStatus
createTime?: Date
updateTime?: Date
}
type QueryBookTaskDetailCondition = {
id?: string
name?: string
bookId?: string
bookTaskId?: string
page?: number
pageSize?: number
}
//#endregion
// 添加批次任务
type AddBookTask = {
copyBookTask: boolean // 是否复制旧的小说任务
count: number // 批次数量
prefixPrompt?: string // 前缀提示
suffixPrompt?: string // 后缀提示词
selectBookTask?: string // 选择的旧的小说任务
selectTaskDataCategory?: string[] // 选择复制的数据类型
selectBookId: string // 选择的小说ID
}
// 返回的MJ消息
type MJMessage = {
id?: string
mjApiUrl?: string
progress: number
category: ImageGenerateMode
imageClick?: string // 图片点击(显示的小的)
imageShow?: string // 图片实际的地址
messageId?: string // 消息ID(可以是MJ中的,也可以是API中的)
action?: MJAction // 动作(生图,反推之类)
status: string // 状态
message?: string // 消息
}
type WebuiConfig = {
sampler_name: string // 采样器名称
negative_prompt: string // 负面提示
batch_size: number // 批次大小
steps: number // 步数
cfg_scale: number // 提示词相关性
denoising_strength: number // 降噪强度
width: number // 宽度
height: number // 高度
seed: number // 种子
init_images: string // 初始图片(垫图的图片地址)
id: string
}
type SDConfig = {
checkpoints: string // 大模型
api: string // api地址
model: string // 生图方式
webuiConfig: WebuiConfig
id: string
}
type ReversePrompt = {
id?: string
bookTaskDetailId?: string
prompt?: string
promptCN?: string
isSelect?: boolean
}
// 查询小说任务的
type QueryBookBackTaskCondition = {
id: string
bookId: string
bookTaskId: string
name: string
type: string
status: string
executeType: string
count: number
}
// 更新小说任务状态
type UpdateBookTaskListStatus = {
id: string
status: BookBackTaskStatus
errorMessage?: string
startTime?: number
endTime?: number
}
// 小说人物
type BookTask = {
id: string
bookId: string
bookTaskId: string
bookTaskDetailId: string
name: string // 任务名称,小说名+批次名+分镜名
type: BookBackTaskType
status: BookBackTaskStatus
errorMessage?: string
executeType: TaskExecuteType // 任务执行类型,手动还是自动
createTime: Date
updateTime: Date
startTime?: number
endTime?: number
}
// 获取小说文案的参数
type GetVideoFrameTextParams = {
id: string
type: SubtitleSavePositionType
videoPath: string
subtitlePosition?: string
}
// 小说风格
type BookStyle = {
id?: string
label?: string
key?: string
value?: string
children?: string
type?: string
prompt?: string
image_url?: string
cref_cw?: number
lora?: string
chinese_prompt?: string
lora_weight?: number
show_image?: string
isShow?: boolean
}
type DefineBookStyle = {
chinese_style?: string
english_style?: string
id?: string
image?: string
}
/**
* 裁剪小视频的返回数据类型
*/
type BookFrameShortClip = {
startTime: number
endTime: number
videoPath: string
duration: number
}
type DeleteBookTaskDetailCondition = {
id?: string
bookId?: string
bookTaskId?: string
name?: string
}
//#region 替换小说数据
type BookReplaceData = {
before: string
after: string
replaceAll?: boolean
type: BookRepalceDataType
}
type ReplaceDataRes = {
bookTaskDetailId: string
newData: string | BookTaskDetail.CopywritingSubValue[]
type?: 'reversePrompt' | 'gptPrompt' | 'afterGpt' | 'prompt' | 'subValue'
reversePromptType?: 'prompt' | 'promptCN'
reversePromptId?: string
}
//#endregion
}
+88
View File
@@ -0,0 +1,88 @@
import { PresetCategory } from '@/define/data/presetData'
declare namespace BookTask {
type BookTaskImageCacheImageList = {
bookTaskName: string
bookTaskId: string
bookId: string
imagePaths: string[]
}
/**
* 书籍任务中的角色或场景定义
*
* 该类型用于定义书籍项目中的角色或场景信息,包括序号、名称和相关提示词。
* 在图像生成过程中,这些信息会被用于构建完整的提示词,以确保角色和场景的一致性。
*
* @property {number} no - 角色或场景的序号/编号,用于排序和引用
* @property {string} name - 角色或场景的名称,如"主角张三"或"现代都市街景"
* @property {string} prompt - 描述角色或场景的提示词,用于AI图像生成
*/
type BookTaskCharacterAndScene = {
[PresetCategory.Character]: BookTaskCharacterAndSceneObject[]
[PresetCategory.Scene]: BookTaskCharacterAndSceneObject[]
}
type BookTaskCharacterAndSceneObject = {
no: number
name: string
prompt: string
}
/**
* 添加小说批次任务的参数
*/
interface AddBookTaskParam {
/** 批量生成的数量 */
count: number
/** 是否复制小说数据 */
copyBookTask: boolean
/** 选中的要复制的小说子批次 */
selectBookTask?: string
/** 需要复制的数据类别 */
selectTaskDataCategory: Array<string>
/** 通用前缀,目前不需要 */
prefixPrompt?: string
/** 通用后缀,目前不需要 */
suffixPrompt?: string
/** 当前小说ID */
selectBookId: string
}
/**
* 进度数据接口
*
* 用于跟踪和显示小说任务的生成进度信息,包括图像和视频的生成进度统计
*
* @property {number} imageProgress - 图像生成进度,已完成的图像数量
* @property {number} videoProgress - 视频生成进度,已完成的视频数量
* @property {number} totalCount - 总任务数量,需要生成的总数
* @property {number} imageRate - 图像生成进度百分比 (0-100)
* @property {number} videoRate - 视频生成进度百分比 (0-100)
*/
interface ProgressData {
imageProgress: number
videoProgress: number
totalCount: number
imageRate: number
videoRate: number
}
/**
* 小说任务进度记录类型
*
* 以任务ID为键,进度数据为值的记录映射,用于管理多个小说任务的进度状态
* 键为小说任务的唯一标识符,值为对应的进度数据
*/
type BookTaskProgressRecord = Record<string, ProgressData>
/**
* 分镜合并类型
*
* 定义AI分镜头合并的模式类型
*
* @type {'long'} long - 长镜头模式,生成较长的分镜序列
* @type {'short'} short - 短镜头模式,生成较短的分镜序列
*/
type StoryboardMergeType = 'long' | 'short'
}
+111
View File
@@ -0,0 +1,111 @@
import {
ImageToVideoModels,
KlingMode,
RunawayModel,
RunwaySeconds,
VideoModel,
VideoStatus
} from '@/define/enum/video'
declare namespace BookTaskDetail {
//#region 图生视频相关
/** VideoMessage Model */
type VideoMessage = {
id?: string
msg?: string
videoType?: ImageToVideoModels
prompt?: string
style?: string
imageUrl?: string
model?: VideoModel
bookTaskDetailId?: string
status?: VideoStatus
videoUrl?: string
taskId?: string
runwayOptions?: string
lumaOptions?: string
klingOptions?: string
messageData?: string
}
/** runway 合成视频的参数(逆向) */
type RunwayOption = {
callback_url: string // 回调地址
image: string // 图片地址
style?: string // 风格
model: RunawayModel // 模型
prompt: string // 提示词
options: {
// 参数
motion_vector?: {
// 镜头控制,目前仅支持GEN2
isUse?: boolean // 是否使用镜头控制
x?: number // x轴 -10.0到10.0之间
y?: number //垂直 -10.0到10.0之间
z?: number //缩放 -10.0到10.0之间
r?: number //旋转 -10.0到10.0之间
bg_x_pan?: number //左右方向倾斜 -10.0到10.0之间
bg_y_pan?: number //上下方向倾斜 -10.0到10.0之间
}
seconds?: RunwaySeconds // 生成视频的时长
image_as_end_frame?: boolean // 目前仅支持gen-3,设置为true后,此图片将作为生成视频的结尾参考,默认为false,即将参考图作为首帧参考进行视频生成
flip?: boolean // 默认为宽屏,即16:9
}
last_image?: string // 尾帧
}
type lumaOptions = {
user_prompt: string // 用户提示词
aspect_ratio: string // 宽高比
expand_prompt?: boolean // 是否扩展提示词
loop: boolean // 循环一张参考图
image_url?: string // 图片地址(参考图,支持第三方图片地址、Base64)
image_end_url?: string // 尾帧图片地址(支持第三方图片地址、Base64)
notify_hook?: string // 回调地址
request_model?: string // 请求的模型,快速还是慢速
}
type klingOptions = {
model?: string // 模型(kling-v1)
image: string // 图片地址,必须,支持Base64编码或图片URL,支持.jpg / .jpeg / .png格式,大小不能超过10MB,分辨率不小于300*300px
image_tail?: string // 尾帧图片地址,支持Base64编码或图片URL,支持.jpg / .jpeg / .png格式,大小不能超过10MB,分辨率不小于300*300px
prompt?: string // 提示词,正向文本提示, 可选,不能超过500个字符
negative_prompt?: string // 负面提示,负向文本提示,可选,不能超过200个字符
cfg_scale?: number // 提示词相关性,可选,范围0-1
mode?: KlingMode // 生成视频的模式,可选,枚举值:std(高性能)或 pro(高表现)
duration?: RunwaySeconds // 生成视频时长,单位秒,可选,枚举值:5,10(包含尾帧的请求仅支持5秒)
callback_url?: string // 回调地址,可选,生成视频完成后,会向该地址发送通知
}
//#endregion
//#region 小说文案相关
/**
* 文案数据包含的字幕数据
*/
type CopywritingSubValue = {
end_time: number
start_time: number
id: string
srt_value: string
}
/**
* 保存文案数据类型
*/
type SaveCopywritingData = {
id: string
lastId?: string
no: number
afterGpt: string
startTime: number
endTime: number
subValue: CopywritingSubValue[]
timeLimit: string
word: string
}
//#endregion
}
+52
View File
@@ -0,0 +1,52 @@
import { TranslateModel } from "./translate"
import { DialogType } from "../enum/bookEnum"
import { ResponseMessageType } from "../enum/softwareEnum"
import { MJ } from "./mj"
declare namespace GeneralResponse {
type SuccessItem = {
code: number
message?: string
data: any
}
type ErrorItem = {
code: number
message: string
data?: any
}
type ProgressResponse = {
total: number, // 总数
current: number, // 当前进度
}
type SubtitleProgressResponse = {
content: string, // 文案内容
progress: ProgressResponse
}
// 主线程主动返回前端的消息类
type MessageResponse = {
code: number,
id: string, // 消息的唯一标识(可以和前端相关联)
type?: ResponseMessageType,
dialogType?: DialogType = DialogType.MESSAGE,
message?: string,
data?: MJ.MJResponseToFront | Buffer | string | TranslateModel.TranslateResponseMessageModel | ProgressResponse | SubtitleProgressResponse
}
}
export type SuccessItem = {
code: number
message?: string
data: any
}
export type ErrorItem = {
code: number
message: string
data?: any
}
+10
View File
@@ -0,0 +1,10 @@
import { LoggerLevel } from '../enum/logger'
declare namespace LoggerModel {
interface LogItem {
id: string
timestamp: string
level: LoggerLevel
message: string
}
}
+30
View File
@@ -0,0 +1,30 @@
import { MJRespoonseType } from "../define/enum/mjEnum"
declare namespace MJ {
// MJ的API进行反推的参数
type APIDescribeParams = {
image: string // 图片的地址(可以是网络,也可以是本地)
taskId: string // 任务ID
}
type MJResponseToFront = {
code: number, // 返回前端的码 0/1
id: string, // 对应分镜的ID
type: MJRespoonseType, // 返回前端的操作类型
mjType: MJAction, // 执行MJ的类型
category: MJImageType, // 调用MJ分类
messageId?: string, // 返回消息的id,就是任务ID
imageClick?: string, // 预览的图片(再使用浏览器模式的时候需要,其他都是null)
imageShow?: string, // 实际下载的图片的地址
imagePath?: string, //实际下载的图片的地址
prompt?: string, // 提示词消息
progress: number, // 实现的进程
message?: string // 消息
status: string
mjApiUrl?: string // 请求的MJ地址
outImagePath?: string // 输出的图片地址
subImagePath?: string[] // 子图片地址
}
}
+42
View File
@@ -0,0 +1,42 @@
declare namespace OptionModel {
type OptionModel = {
key?: string
value?: string
type?: string | number
createTime?: Date
updateTime?: Date
}
/** Option类型 */
type OptionType = {
label: string
path: string
value: string
}
/** 背景音乐Option Model */
type BackgroundMusicOption = {
id: string // 唯一ID
musicName: string // 音乐名称
musicPath: string // 音乐路径
}
/** 图片 Option Model */
type ImageOption = {
id: string // 唯一ID
imagePath: string // 图片路径
imageName: string // 图片名称
}
/**
* 用户的登录信息
*/
type UserInformation = {
userId: string
userName: string
token: string
expirationTime: Date // 到期时间
isForver: boolean // 是否永久有效
machineId?: string
}
}
+88
View File
@@ -0,0 +1,88 @@
import { PresetCategory } from '../data/presetData'
declare namespace PresetModel {
/**
* 预设类型定义
*/
interface Preset {
/** 预设唯一标识符 */
id?: string
/** 预设显示名称/标签 */
label: string
/** 预设类型 */
type: string
/** 预设展示图片 */
showImage?: string[]
/** 预设提示词 */
prompt: string
/** 中文提示词 */
chinesePrompt?: string
/** 图片URL */
imageUrl?: string
/** 参数 srefSw */
srefSw?: number
/** 参数 crefCw */
crefCw?: number
/** LoRA模型 */
lora?: string | null
/** LoRA权重 */
loraWeight?: number
/** 是否显示 */
isShow: boolean
/** 子预设列表 */
aliases?: string[]
/** 创建时间 */
createTime?: Date
/** 显示的图片 */
coverImage?: string
/** 是否被选中 */
checked?: boolean
}
/**
* Represents the condition parameters for querying presets.
* 表示查询预设的条件参数。预设是否应该显示。
*/
type QueryPresetCondition = {
/** 预设的唯一标识符。 */
id?: string
/** 预设的显示名称或标签。 */
label?: string
/** 预设的类别或类型。 */
type?: string | null
/** 预设是否应该显示。 */
isShow?: boolean
/** 页码 */
page?: number
/** 每页数量 */
pageSize?: number
}
/**
* 预设分类
* PresetCategory 是一个枚举类型,表示预设的分类。
*/
type PresetCategoryCollection = {
/** 人物预设集合 */
[PresetCategory.Character]: Preset[]
/** 风格预设集合 */
[PresetCategory.Style]: Preset[]
/** 场景预设集合 */
[PresetCategory.Scene]: Preset[]
}
}
+207
View File
@@ -0,0 +1,207 @@
import { ImageGenerateMode, MJRobotType, MJSpeed } from '../data/mjData'
import { JianyingKeyFrameEnum } from '../enum/jianyingEnum'
declare namespace SettingModal {
//#region 基础设置
/**
* 通用设置接口
* 定义应用程序的常规设置项
*/
export interface GeneralSettings {
/** 剪映草稿箱路径 */
draftPath: string
/** 项目保存路径 */
projectPath: string
/** 系统并发数 (1-16) */
concurrency: number
/** 默认生图方式 */
defaultImgGenMethod: ImageCategory
/** 高清倍数 (1-4) */
hdScale: number
/** 默认图转视频方式 */
defaultImg2Video: ImageToVideoCategory
/** 系统语言 */
language: string
}
//#endregion
//#region MJ设置
/**
* Midjourney 通用设置接口
*/
interface MJGeneralSettings {
/** 出图模式 */
outputMode: ImageGenerateMode
/** 生图机器人类型 */
robot: MJRobotType
/** 机器人模型ID */
model: string
/** 生图尺寸ID */
size: string
/** 命令后缀 */
commandSuffix: string
/** 生图任务量 (1-10) */
taskCount: number
/** 间隔时间(秒) */
interval: number
}
/**
* Midjourney API 设置接口
* 定义了与 MJ API 相关的配置参数
*/
interface MJApiSettings {
/** API 服务地址 */
apiUrl: string
/** API 密钥/授权令牌 */
apiKey: string
/** 出图速度设置
* 可选值通常为"fast"(快速)或"relaxed"(慢速)
*/
apiSpeed: MJSpeed
}
//#endregion
//#region AI推理设置
/**
* AI 推理设置接口
* 定义了用于 AI 推理和翻译的配置参数
*/
interface InferenceAISettings {
/** API 服务提供商标识 */
apiProvider: string
/** API 访问令牌/密钥 */
apiToken: string
/** 推理模型名称
* 例如: 'deepseek-chat', 'gpt-4', 'claude-3-opus' 等
*/
inferenceModel: string
/** AI 提示词值 - 用于生成图像的提示内容 */
aiPromptValue: string
/** 翻译模型名称
* 例如: 'Doubao-lite-32k', 'gpt-3.5-turbo' 等
*/
translationModel: string
}
//#endregion
//#region SD设置
/**
* Stable Diffusion 设置接口
* 定义了 SD 生成图像所需的所有参数
*/
interface SDSettings {
/** SD API服务器请求地址 */
requestUrl: string
/** 出图方式: 'txt2img' (文生图) 或 'img2img' (图生图) */
generationMethod: string
/** 正向提示词 - 描述你希望生成的图像内容 */
positivePrompt: string
/** 反向提示词 - 描述你希望避免的图像内容 */
negativePrompt: string
/** CFG Scale - 提示词相关性权重 (1-20) */
cfgScale: number
/** 单次出图张数 (1-6) */
batchCount: number
/**
* 种子值
* -1表示随机种子,指定具体数值可复现相同结果
*/
seed: number
/** 重绘幅度 (0-1) - 在图生图模式下影响保留原图程度 */
denoisingStrength: number
/** 采样方式 - 如 'DPM++ SDE Karras', 'Euler a' 等 */
sampler: string
/** 迭代步数 (1-150) - 影响生成质量和时间 */
steps: number
/** 生成图片宽度 (像素) */
width: number
/** 生成图片高度 (像素) */
height: number
/** 是否启用修脸/修手功能 */
faceHandFix: boolean
}
/**
* ADetailer 模型接口
* 定义了 ADetailer 插件使用的模型及其设置
*/
interface SDADetailerModel {
/** 模型唯一标识符 */
id: string
/** 模型文件名称
* 例如: 'face_yolov8n.pt', 'hand_yolov8n.pt' 等
*/
model: string
/** 识别信任度
* 范围通常为 10-100 的整数值,表示百分比
* 数值越高,模型识别越严格
*/
threshold: number
}
//#endregion
//#region 剪映关键帧设置
/**
* 剪映关键帧设置接口
* 定义了剪映视频编辑中关键帧的设置项
*/
interface JianyingKeyFrameSettings {
keyFrame: JianyingKeyFrameEnum
isFixedSpeed: boolean
keyFrameTime: number
upDownKeyFrame: {
defaultScale: number
startPosition: number
endPosition: number
}
leftRightKeyFrame: {
defaultScale: number
startPosition: number
endPosition: number
}
scaleKeyFrame: {
defaultScale: number
startPosition: number
endPosition: number
}
}
//#endregion
}
+79
View File
@@ -0,0 +1,79 @@
declare namespace SoftwareModal {
/**
* 软件授权信息
*/
interface SoftwareAuthorizationMessage {
/** 机器码 */
machineID: string
/** 授权软件类型 都是0 */
type: number
/** 使用类型 1 基础 2 赞助*/
useType: number
/** 授权天数 */
expiryTime: number
/** 授权时间 */
authorizedDate: Date
/** 到期时间 */
expiryDate: Date
/** 授权码 */
authorizationCode: string
}
//#region 更新信息和版本信息
/**
* 更新变更类型
*/
export type ChangeType = 'bugfix' | 'add' | 'improvement' | 'remove' | 'security' | 'performance'
/**
* 单个变更项
*/
export interface ChangeItem {
/** 变更类型 */
type: ChangeType
/** 变更描述 */
description: string
}
/**
* 版本更新信息
*/
export interface SubUpdateInfo {
/** 版本号 */
version: string
/** 更新日期 */
updateDate: string
/** 变更列表 */
changes: ChangeItem[]
}
/**
* 完整的更新信息结构
*/
export interface UpdateInfo {
/** 最新版本号 */
latestVersion: string
/** 最新更新日期 */
updateDate: string
/** 所有版本的更新信息 */
updateInfo: SubUpdateInfo[]
}
/**
* 系统更新信息
*/
interface VersionInfo {
/** 当前版本 */
currentVersion: string
/** 最新版本 */
latestVersion: string
/** 更新信息 */
updateInfo: UpdateInfo
/** 是否可以更新 */
canUpdate: boolean
}
//#endregion
}
+60
View File
@@ -0,0 +1,60 @@
import {
BookBackTaskStatus,
BookBackTaskType,
BookTaskStatus,
TaskExecuteType
} from '@/define/enum/bookEnum'
declare namespace TaskModal {
interface Task {
id?: string
bookId?: string
bookTaskId?: string
bookTaskDetailId?: string
name?: string // 任务名称,小说名+批次名+分镜名
type?: BookBackTaskType
status?: BookBackTaskStatus
errorMessage?: string | null
executeType?: TaskExecuteType // 任务执行类型,手动还是自动
createTime?: Date
updateTime?: Date
startTime?: number
endTime?: number
messageName?: string
}
interface TaskCondition {
bookName?: string
bookTaskName?: string
bookTaskDetailName?: string
taskName?: string
taskType?: BookBackTaskType
taskStatus?: BookBackTaskStatus
taskErrorMessage?: string
}
interface QueryTaskCondition extends TaskCondition {
page: number
pageSize: number
}
interface BackTaskCollection extends Task {
bookName?: string
bookTaskName?: string
bookTaskDetailName?: string
}
type TaskCollection = {
page: number
pageSize: number
count: number
data: BackTaskCollection[]
}
type TaskStatus = {
status: BookBackTaskStatus | 'UNKNOWN' | BookTaskStatus | string
label: string
type?: string
color?: string
} & ({ type: string } | { color: string })
}
+56
View File
@@ -0,0 +1,56 @@
import { TranslateType, TranslateAPIType } from '../enum/translate'
declare namespace TranslateModel {
type TranslateNowReturnParams = {
text: string // 需要翻译的文本
from: string // 源语言
to: string // 目标语言
isSplit: false // 是否需要分割
}
type TranslateNowIPCParams = {
text: string // 需要翻译的文本
from: string // 源语言
to: string // 目标语言
isSplit: false // 是否需要分割
bookTaskDetailId: string // 小说分镜ID
reversePromptId: string // 反推提示词ID
windowId?: number // 窗口ID
type: TranslateType // 翻译类型
responseMessgeName: string // 返回的消息名称(用作自定义)
}
type TranslateResponseMessageModel = {
bookTaskDetailId: string // 分镜ID
reversePromptId: string // 反推提示词ID
from: string // 源语言
to: string // 目标语言 to 是 zh 值重新保存到 promptCN 中,to 是 en ,需要重新覆盖prompt和prom
prompt: string // 英文数据
promptCN: string // 中文翻译数据(但是也可以存储英文)
progress: number // 索引(翻译到了哪,用于进度条)
total: number // 总数
}
type TranslateReturnDataModel = {
src: string // 字符串
dst: string // 翻译结果
}
type TranslateReturnModel = {
to: string // 目标语言
data: TranslateReturnDataModel[] // 翻译结果
}
type subTranslateModel = {
name: TranslateAPIType // 翻译API名称
translation_business: string // 翻译业务
translation_app_id: string // 翻译APP ID
translation_secret: string // 翻译密钥
}
type TranslateModel = {
selectModel: TranslateAPIType // 选择的模型
translation_auto: boolean // 是否自动翻译
translates: subTranslateModel[] // 可用的翻译API
}
}
+391
View File
@@ -0,0 +1,391 @@
import { DEFINE_STRING } from './ipcDefineString'
import { TimeDelay } from './Tools/time'
// 要清理的批次ID
let GlobalClearBatchId: string[] = []
export class AsyncQueue {
global: any
tasks: any[]
concurrencyLimit: number
manualMode: boolean
currentConcurrency: number
batchCompletion: any
taskDeadline: string
showEndTime: boolean
currentCreateItem: any
taskProgress: any[]
constructor(global, concurrencyLimit = 1, manualMode = false) {
this.global = global
this.tasks = []
this.concurrencyLimit = concurrencyLimit
this.manualMode = manualMode
this.currentConcurrency = 0
// 扩展批次完成状态对象以支持子批次
this.batchCompletion = {}
this.taskDeadline = global.endTime
this.showEndTime = true
this.currentCreateItem = null
// 只有在手动模式下才会使用。要设置当前正在执行的任务,将正在执行的任务添加到里面,需要在任务完成之后手动移除
this.taskProgress = []
}
async enqueue(
task: Function,
taskId: string,
batchId: string,
subBatchId: string = 'default',
errorFunc: Function | null = null
) {
// if (batchId && batchId != DEFINE_STRING.QUEUE_BATCH.IMAGE_SAVE_TO_OTHER_FOLDER) {
// // 判断当前的任务是否已经存在,存在则不添加
// let index = this.tasks.findIndex(
// (item) =>
// item.taskId === taskId && item.batchId === batchId && item.subBatchId === subBatchId
// )
// if (index != -1) {
// throw new Error(`Task ${taskId} in batch ${batchId} already exists.`)
// }
// }
if (!this.batchCompletion[batchId]) {
this.batchCompletion[batchId] = {
remaining: 0,
subBatches: {},
callback: null,
failedTasks: []
}
}
if (!this.batchCompletion[batchId].subBatches[subBatchId]) {
this.batchCompletion[batchId].subBatches[subBatchId] = { remaining: 0, failedTasks: [] }
}
this.batchCompletion[batchId].remaining++
this.batchCompletion[batchId].subBatches[subBatchId].remaining++
this.tasks.push({ task, taskId, batchId, subBatchId, errorFunc })
if (!this.manualMode) {
await this.process()
}
}
setBatchCompletionCallback(batchId, callback) {
if (this.batchCompletion[batchId]) {
this.batchCompletion[batchId].callback = callback
}
}
setSubBatchCompletionCallback(batchId, subBatchId, callback) {
if (this.batchCompletion[batchId]) {
this.batchCompletion[batchId].subBatches[subBatchId].callback = callback
}
}
setTaskDeadline(deadline) {
this.taskDeadline = deadline
}
async process(task_count = 0) {
// 判断是不是有机器码检测的标识
// if (!this.global.CheckMachineId) {
// // throw new Error("Machine ID not detected, please check the machine ID.");
// this.global.wins[0].webContents.send(DEFINE_STRING.SYSTEM.SHOW_MESSAGE_DIALOG, {
// code: 0,
// message: '请联系管理员激活'
// })
// return
// }
if (this.global.endTime && new Date(Date.now()).toISOString() > this.global.endTime) {
this.tasks = []
if (this.showEndTime) {
this.global.wins[0].webContents.send(DEFINE_STRING.SYSTEM.SHOW_MESSAGE_DIALOG, {
code: 0,
message: '试用时间已到,请联系客服'
})
}
this.showEndTime = false
return
}
while (
this.tasks.length > 0 &&
(this.manualMode
? this.taskProgress.length < task_count
: this.currentConcurrency < this.concurrencyLimit)
) {
const { task, taskId, batchId, subBatchId, errorFunc } = this.tasks.shift()
this.currentConcurrency++
Promise.resolve(task())
.then(() => {
this.currentConcurrency--
this.handleTaskCompletion(batchId, subBatchId, null)
if (!this.manualMode) {
this.process()
}
})
.catch((error) => {
// 判断是不是网络请求,不是网络请求直接报错
let retryCount = 0
const maxRetryCount = 5
let retryTask = async () => {
if (retryCount < maxRetryCount) {
retryCount++
await TimeDelay(2000)
this.global.wins[0].webContents.send(DEFINE_STRING.SYSTEM.SHOW_MAIN_NOTIFICATION, {
code: 0,
message: `生图失败,共5次尝试,目前第${retryCount}`
})
task()
.then(() => {
this.currentConcurrency--
this.handleTaskCompletion(batchId, subBatchId, null)
if (!this.manualMode) {
this.process()
}
})
.catch((_error) => {
retryTask()
})
} else {
// console.error(`Task ${taskId} failed after ${maxRetryCount} retries`);
this.currentConcurrency--
this.handleTaskCompletion(batchId, subBatchId, { taskId, error })
// 执行传入的错误后执行方法,一般用于修改后台任务或者是批量提示
if (errorFunc) {
errorFunc(
batchId,
`任务 ${taskId} 执行失败,错误信息:${error.toString()},开始清理整个批次,批次ID${batchId}`
)
}
if (!this.manualMode) {
this.process()
}
}
}
if (GlobalClearBatchId.includes(batchId)) {
retryTask()
} else {
console.error('An error occurred in task ' + taskId + ':', error)
// 出现报错。直接停掉当前对应的的子批次
this.currentConcurrency--
this.handleTaskCompletion(batchId, subBatchId, { taskId, error })
// 执行传入的错误后执行方法,一般用于修改后台任务或者是批量提示
if (errorFunc) {
errorFunc(
batchId,
`任务 ${taskId} 执行失败,错误信息:${error.toString()},开始清理整个批次,批次ID${batchId}`
)
}
if (!this.manualMode) {
this.process()
} else {
// 将当前的设置删除,将历史人物删除
this.setCurrentCreateItem(null)
this.taskProgress = []
}
}
})
}
}
// 任务完成执行的函数,用于处理任务完成后的状态更新等
handleTaskCompletion(batchId, subBatchId, taskError) {
const batch = this.batchCompletion[batchId]
const subBatch = this.batchCompletion[batchId].subBatches[subBatchId]
if (taskError) {
batch.failedTasks.push(taskError)
subBatch.failedTasks.push(taskError)
// 删除整个子批次
this.removeTask(batchId, null, subBatchId)
}
this.batchCompletion[batchId].remaining--
this.batchCompletion[batchId].subBatches[subBatchId].remaining--
// 子批次完成
if (this.batchCompletion[batchId].subBatches[subBatchId].remaining === 0) {
console.log(`Sub-batch ${subBatchId} in batch ${batchId} completed.`)
const callback = this.batchCompletion[batchId].subBatches[subBatchId].callback
if (callback) {
callback(this.batchCompletion[batchId].subBatches[subBatchId].failedTasks)
}
this.batchCompletion[batchId].subBatches[subBatchId] = {
remaining: 0,
callback: null,
failedTasks: []
}
}
if (this.batchCompletion[batchId].remaining === 0) {
const callback = this.batchCompletion[batchId].callback
if (callback) {
callback(this.batchCompletion[batchId].failedTasks)
}
this.batchCompletion[batchId] = {
remaining: 0,
subBatches: {},
callback: null,
failedTasks: []
}
}
}
// 移除任务
removeTask(batchId, taskId, subBatchId = 'default') {
try {
if (!this.batchCompletion[batchId]) {
throw new Error(`Batch ${batchId} does not exist.`)
}
// 获取当前批次的所有的任务
let initialCount = this.tasks.filter((item) => item.batchId === batchId).length
let finalCount = initialCount
// 首先,判断是否指定了taskId来决定是移除单个任务还是整个批次
if (taskId == null) {
// 删除整个批次
this.tasks = this.tasks.filter((item) => item.batchId !== batchId)
finalCount = this.tasks.filter((item) => item.batchId === batchId).length
} else if (taskId == 'all') {
// 删除所有的指定的子批次
this.tasks = this.tasks.filter(
(item) => !(item.subBatchId == subBatchId && item.batchId == batchId)
)
finalCount = this.tasks.filter(
(item) => item.subBatchId == subBatchId && item.batchId == batchId
).length
} else {
// 删除指定的任务,需要考虑subBatchId
this.tasks = this.tasks.filter(
(item) =>
!(item.taskId === taskId && item.batchId === batchId && item.subBatchId === subBatchId)
)
finalCount = this.tasks.filter(
(item) =>
item.taskId === taskId && item.batchId === batchId && item.subBatchId === subBatchId
).length
}
// 在移除后再次计算该批次的任务数量
// const finalCount = this.tasks.length;
// 使用两次计数的差值更新 remaining
const removedTasks = initialCount - finalCount
if (removedTasks > 0) {
this.batchCompletion[batchId].remaining -= removedTasks
if (this.batchCompletion[batchId].subBatches[subBatchId]) {
this.batchCompletion[batchId].subBatches[subBatchId].remaining -= removedTasks
this.batchCompletion[batchId].subBatches[subBatchId].callback = null
}
}
// 检查并处理完整批次和小批次的完成状态
const batch = this.batchCompletion[batchId]
// 判断子批次的数量
const subBatch = this.batchCompletion[batchId].subBatches[subBatchId]
// 子批次完成
if (subBatch && subBatch.remaining == 0 && subBatch.callback) {
subBatch.callback(subBatch.failedTasks)
delete this.batchCompletion[batchId].subBatches[subBatchId] // 清理批次完成状态
}
// 总批次完成
if (batch && batch.remaining === 0 && batch.callback) {
const callback = batch.callback
callback(batch.failedTasks)
delete this.batchCompletion[batchId] // 清理批次完成状态
} else {
// 遍历小批次,看是否有需要清理的
for (let subBatchId in batch.subBatches) {
const subBatch = batch.subBatches[subBatchId]
if (subBatch.remaining === 0) {
// 这里可以进行小批次完成后的处理,比如调用小批次的特定回调
console.log(`Sub-batch ${subBatchId} in batch ${batchId} completed.`)
// 可以选择删除空的小批次状态,如果不需要保留它的完成记录
delete batch.subBatches[subBatchId]
}
}
}
return {
code: 1
}
} catch (error: any) {
return {
code: 0,
message: error.toString()
}
}
}
// 获取指定batch的任务列表
getTasks(batchId) {
return this.tasks.filter((item) => item.batchId === batchId)
}
// 获取失败的任务
getFailedTasks(batchId, subBatchId = 'default') {
if (this.batchCompletion[batchId] && this.batchCompletion[batchId].subBatches[subBatchId]) {
return this.batchCompletion[batchId].subBatches[subBatchId].failedTasks
}
return []
}
// 手动开启下一个任务
async startNextTask(taskCount = 3) {
// 判断当前是不是有任务正在执行
if (this.currentCreateItem) {
return
}
console.log('调用开始下一个任务', this.taskProgress)
if (this.manualMode && this.tasks.length > 0 && this.taskProgress.length < taskCount) {
this.process(taskCount)
}
}
// 修改currentItem
setCurrentCreateItem(item) {
this.currentCreateItem = item
if (item) {
// 判断相同的ID的任务是否存在,存在则不添加
let index = this.taskProgress.findIndex((task) => task.id === item.id)
if (index == -1) {
this.taskProgress.push(item)
} else {
// 直接修改
this.taskProgress[index] = item
}
}
}
// 获取currentItem
getCurrentCreateItem() {
return this.currentCreateItem
}
// 获取当前正在执行的任务数量
getCurrentConcurrency() {
return this.currentConcurrency
}
// 获取等待中的队列
getWaitingQueue() {
return this.tasks.length
}
// 获取当前任务的限制任务数量
getConcurrencyLimit() {
return this.concurrencyLimit
}
// 修改任务数据
// 获取当前正在执行的任务对象()
getTaskProgress() {
return this.taskProgress
}
// 删除自动设置的任务
removeTaskProgress(callback) {
this.taskProgress = callback(this.taskProgress)
console.log('删除对应消息的任务', this.taskProgress)
}
}
+25
View File
@@ -0,0 +1,25 @@
import { ValidateJson } from "../Tools/validate";
/**
* 获取转发请求的数据,序列化后返回
* @param response
* @returns
*/
function GetForwardResponseData(response: any) {
if (response.status != 200) {
throw new Error(response.message)
}
if (response.data.code != 1) {
throw new Error(response.data.message)
}
if (!ValidateJson(response.data.data)) {
throw new Error(response.data.data)
}
return JSON.parse(response.data.data);
}
let ForwardResponse = {
GetForwardResponseData,
}
export default ForwardResponse;
+95
View File
@@ -0,0 +1,95 @@
export type OpenAISuccessResponse = {
id: string
object: string
created: number
model: string
choices: [
{
index: number
message: {
role: string
content: string
}
finish_reason: string
}
]
usage: {
prompt_tokens: number
completion_tokens: number
total_tokens: number
}
}
type RixApiErrorResponse = {
error: {
message: string // 错误信息
type: string
param: string
code: string
}
}
type KimiErrorResponse = {
error: {
message: string
type: string
}
}
type DoubaoErrorResponse = {
error: {
code: string
message: string
param: string
type: string
}
}
/**
* 处理OpenAI系列返回的成功response
* @param response OpenAI返回的response
* @returns 处理后的返回的数据
*/
export function GetOpenAISuccessResponse(response: string | OpenAISuccessResponse): string {
if (typeof response === 'string') {
response = JSON.parse(response) as OpenAISuccessResponse
}
// 开始处理response
return response.choices[0].message.content
}
/**
* 处理RixApi系列返回的错误response
* @param response RixApi返回的response
* @returns 处理后的错误信息
*/
export function GetRixApiErrorResponse(response: string | RixApiErrorResponse): string {
if (typeof response === 'string') {
response = JSON.parse(response) as RixApiErrorResponse
}
return response.error.message
}
/**
* 处理kimi的错误信息返回
* @param response
* @returns
*/
export function GetKimiErrorResponse(response: string | KimiErrorResponse): string {
if (typeof response === 'string') {
response = JSON.parse(response) as KimiErrorResponse
}
return response.error.message
}
/**
* 获取豆包的错误返回信息
* @param response
* @returns
*/
export function GetDoubaoErrorResponse(response: string | DoubaoErrorResponse): string {
if (typeof response === 'string') {
response = JSON.parse(response) as DoubaoErrorResponse
}
return response.error.message
}
+153
View File
@@ -0,0 +1,153 @@
import path from 'path'
import { CheckFolderExistsOrCreate } from '../Tools/file'
import fs from 'fs'
import { define } from '../define'
import { errorMessage, successMessage } from '@/public/generalTools'
import { machineId } from 'node-machine-id'
import { OptionRealmService } from '../db/service/optionService'
import { OptionKeyName, OptionType } from '../enum/option'
import { isEmpty } from 'lodash'
import { execSync } from 'child_process'
import os from 'os'
import crypto from 'crypto'
export class InitFunc {
constructor() {}
/**
* 初始化文件夹或者是文件,判断是不是要新建
*/
async InitConfigFolderOrFile() {
let folderData = [
{
path: 'config',
default: null,
folder: true,
fileType: 'json'
}
]
for (let i = 0; i < folderData.length; i++) {
const element = folderData[i]
// 判断是不是文件夹
if (element.folder) {
// 判断对应的文件夹位置是不是存在,不纯在创建
await CheckFolderExistsOrCreate(element.path)
} else {
switch (element.fileType) {
case 'json':
// 判断文件是不是存在,不存在创建
await CheckFolderExistsOrCreate(path.dirname(element.path))
fs.promises.writeFile(element.path, JSON.stringify(element.default), 'utf-8')
break
case 'txt':
// 判断文件是不是存在,不存在创建
await CheckFolderExistsOrCreate(path.dirname(element.path))
fs.promises.writeFile(element.path, element.default ?? '', 'utf-8')
break
default:
break
}
}
}
}
async InitConfig() {
try {
let sp = path.resolve(define.resources_path, 'config', 'init.txt')
let res = await fs.promises.readFile(sp, 'utf-8')
// 取第一行
let arr = res.split('\n')
let data = arr[0] ?? ''
global.softwareId = data
return true
} catch (error) {
errorMessage('读取初始化配置文件失败 init', 'InitFunc_InitConfig')
// 确保创建必要的目录
await CheckFolderExistsOrCreate(path.resolve(define.resources_path, 'config'))
// 创建一个默认的init.txt文件
await fs.promises.writeFile(
path.resolve(define.resources_path, 'config', 'init.txt'),
'default-software-id',
'utf-8'
)
global.softwareId = 'default-software-id'
return true
}
}
/**
* 获取电脑机器码,每次电脑启动进行更新
* @returns
*/
async InitMachineId() {
try {
let baseId = await machineId(true)
let hardwareInfo = ''
try {
if (process.platform === 'win32') {
// Windows: 获取BIOS和磁盘序列号
hardwareInfo = execSync(
'wmic bios get serialnumber && wmic diskdrive get serialnumber'
).toString()
} else if (process.platform === 'darwin') {
// macOS: 获取硬件UUID
hardwareInfo = execSync(
'system_profiler SPHardwareDataType | grep "Hardware UUID"'
).toString()
} else {
// Linux: 获取主板序列号
hardwareInfo = execSync(
'cat /sys/class/dmi/id/board_serial 2>/dev/null || echo "unknown"'
).toString()
}
} catch (e) {
hardwareInfo = 'exec-failed'
}
// 方法2: 用户和系统信息
const userInfo = os.userInfo().username + '-' + os.homedir()
// 组合所有信息
const combinedInfo = `${baseId}|${hardwareInfo}|${userInfo}`
// 生成最终ID
let id = crypto.createHash('sha256').update(combinedInfo).digest('hex')
const optionRealmService = await OptionRealmService.getInstance()
optionRealmService.ModifyOptionByKey(OptionKeyName.Software.MachineId, id, OptionType.STRING)
successMessage(id, '重新获取机器码成功!', 'InitFunc_InitMachineId')
return true
} catch (error) {
errorMessage('初始设置机器码失败', 'InitFunc_InitMachineId')
return false
}
}
/**
* 初始化项目地址
*/
async InitProjectPath() {
try {
let projectPath = path.resolve(define.resources_path, 'project')
const optionRealmService = await OptionRealmService.getInstance()
let projectPathOption = optionRealmService.GetOptionByKey(OptionKeyName.Software.ProjectPath)
if (
projectPathOption == null ||
projectPathOption.value == null ||
isEmpty(projectPathOption.value)
) {
// 如果没有设置过项目地址,则创建一个默认的项目地址
await CheckFolderExistsOrCreate(projectPath)
optionRealmService.ModifyOptionByKey(
OptionKeyName.Software.ProjectPath,
projectPath,
OptionType.STRING
)
successMessage(projectPath, '项目地址初始化成功', 'InitFunc_InitProjectPath')
}
} catch (error: any) {
errorMessage('项目地址初始化失败,' + error.message, 'InitFunc_InitMachineId')
}
}
}
+250
View File
@@ -0,0 +1,250 @@
import { app, BrowserWindow } from 'electron'
const { join } = require('path')
import { is } from '@electron-toolkit/utils'
import { InitFunc } from './initFunc'
import { IpcStart } from '../ipc/index'
import { define } from '../define'
type DefaultConfig = {
id: null | number // 窗口唯一id
background: string // 背景色
route: string // 路由地址url
title: string // 标题
data: any // 传入数据参数
width: number | string // 窗口宽度
height: number | string // 窗口高度
minWidth: number | string // 窗口最小宽度
minHeight: number | string // 窗口最小高度
x: number | string // 窗口相对于屏幕左侧坐标
y: number | string // 窗口相对于屏幕顶端坐标
resize: boolean // 是否支持缩放
maximize: boolean // 最大化窗口
parent: string | null // 父窗口(需传入父窗口id
modal: boolean // 模态窗口(模态窗口是浮于父窗口上,禁用父窗口)
alwaysOnTop: boolean // 置顶窗口
outUrl: string | null // 外部链接
openDevTools: boolean // 是否打开开发者工具
theme: string // 主题
}
// 配置参数
const defaultConfig: DefaultConfig = {
id: null, // 窗口唯一id
background: '#fff', // 背景色
route: '', // 路由地址url
title: '', // 标题
data: null, // 传入数据参数
width: '', // 窗口宽度
height: '', // 窗口高度
minWidth: '', // 窗口最小宽度
minHeight: '', // 窗口最小高度
x: '', // 窗口相对于屏幕左侧坐标
y: '', // 窗口相对于屏幕顶端坐标
resize: true, // 是否支持缩放
maximize: false, // 最大化窗口
parent: '', // 父窗口(需传入父窗口id
modal: false, // 模态窗口(模态窗口是浮于父窗口上,禁用父窗口)
alwaysOnTop: false, // 置顶窗口
outUrl: null, // 外部链接
openDevTools: false, // 是否打开开发者工具
theme: 'light' // 主题
}
export class CreatWindow {
mainWin: BrowserWindow | null
wins: any
initFunc: InitFunc
constructor() {
this.mainWin = null
this.wins = {}
this.initFunc = new InitFunc()
}
/**
* 返回创建窗口的配置
* @returns
*/
winOpts(): Electron.BrowserWindowConstructorOptions {
let icon = define.icon
return {
// 窗口图标
icon: icon,
autoHideMenuBar: true,
width: 900,
height: 640,
minimizable: true,
maximizable: true,
show: false,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
contextIsolation: true, // 启用上下文隔离(为了安全性)(默认true)
nodeIntegration: false, // 启用Node集成(默认false
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegrationInWorker: false,
webSecurity: false,
allowRunningInsecureContent: true,
partition: 'persist:my-partition'
}
}
}
// 创建新窗口
createWin(name, options) {
const args: any = Object.assign({}, defaultConfig, options)
console.log(args)
// 判断窗口是否存在
for (let i in this.wins) {
// 判断当前名字的窗口在不在在的话,就聚焦
if (this.getWin(i) && this.wins[i].route === args.route) {
const win = this.getWin(i)
if (win) {
win.focus()
}
return
}
}
let opt: Electron.BrowserWindowConstructorOptions = this.winOpts()
if (args.parent) {
const parentWin = this.getWin(args.parent)
if (parentWin) {
opt.parent = parentWin
}
}
if (typeof args.modal === 'boolean') opt.modal = args.modal
if (typeof args.resize === 'boolean') opt.resizable = args.resize
if (typeof args.alwaysOnTop === 'boolean') opt.alwaysOnTop = args.alwaysOnTop
if (args.background) opt.backgroundColor = args.background
if (args.width) opt.width = args.width
if (args.height) opt.height = args.height
if (args.minWidth) opt.minWidth = args.minWidth
if (args.minHeight) opt.minHeight = args.minHeight
if (args.x) opt.x = args.x
if (args.y) opt.y = args.y
console.log(opt)
// 创建窗口对象
let win = new BrowserWindow(opt)
// 是否最大化
if (args.maximize && args.resize) {
win.maximize()
}
args.id = win.id
// 加载页面
let $url
if (args.outUrl) {
$url = args.outUrl
} else {
// 判断是不是开发环境
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
if (args.route != '') {
$url = process.env['ELECTRON_RENDERER_URL'] + '/#/' + args.route
} else {
$url = process.env['ELECTRON_RENDERER_URL']
}
if (args.openDevTools) {
win.webContents.openDevTools()
}
} else {
if (args.route != '') {
$url = `file://${join(__dirname, '../renderer/index.html')}#/${args.route}`
} else {
$url = join(__dirname, '../renderer/index.html')
}
}
}
if (is.dev) {
win.loadURL($url)
} else {
win.loadFile($url)
}
win.once('ready-to-show', () => {
win.show()
})
win.on('close', () => win.setOpacity(0))
// 初始化渲染进程
win.webContents.on('did-finish-load', () => {
// win.webContents.send('win-loaded', '加载完成~')
win.webContents.send('win-loaded', args)
})
this.wins[name] = {
route: args.route,
id: win.id
}
return win
}
// 获取全部窗口
getAllWin() {
return BrowserWindow.getAllWindows()
}
// 获取窗口
getWin(name) {
let id = this.wins[name].id
return BrowserWindow.fromId(Number(id))
}
// 关闭全部窗口
closeAllWin() {
try {
for (let i in this.wins) {
if (this.getWin(i)) {
const win = this.getWin(i)
if (win) {
win.close()
}
} else {
app.quit()
}
}
} catch (error: any) {
throw new Error(error)
}
}
// 关闭指定的窗口
closeWin(name) {
try {
if (this.getWin(name)) {
const win = this.getWin(name)
if (win) {
win.close()
}
}
} catch (error: any) {
throw new Error(error)
}
}
// 下面的方法创建初始条件和文件
async initConfigFolderOrFile() {
await this.initFunc.InitConfigFolderOrFile()
}
// 初始化配置文件
async InitConfig() {
try {
await this.initFunc.InitConfig()
await this.initFunc.InitMachineId()
await this.initFunc.InitProjectPath()
} catch (error) {
console.error('配置初始化失败:', error)
// 这里可以添加额外的错误处理
}
}
// 开启主进程监听
ipcMainListen() {
IpcStart()
}
}
+61
View File
@@ -0,0 +1,61 @@
import { app, BrowserWindow } from 'electron'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { CreatWindow } from '../define/window/windowManager'
import { Logger } from '../define/Tools/logger'
import { successMessage } from '@/public/generalTools'
// import { machineId } from 'node-machine-id'
// 在文件顶部添加这段代码
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', promise, '原因:', reason)
// 可以选择记录到日志文件或者向用户显示错误
})
// 在应用启动前,设置正确的编码
process.env.LANG = 'zh_CN.UTF-8'
async function createWindow() {
try {
// 创建主窗口
global.wm = new CreatWindow()
let win = global.wm.createWin('main', { openDevTools: false, minWidth: 1000, minHeight: 750 })
global.wins = [win]
global.wm.ipcMainListen()
// 先初始化日志
global.logger = new Logger()
// 再初始化文件夹和配置
await global.wm.initConfigFolderOrFile()
await global.wm.InitConfig()
successMessage(
null,
'软件初始化成功,创建窗体成功,初始化配置,初始化日志成功',
'SoftWare.Init'
)
} catch (error: any) {
console.error('应用初始化失败:', error)
// 移除throw error,改为返回一个自定义错误消息
throw error
}
}
app.whenReady().then(async () => {
electronApp.setAppUserModelId('com.electron')
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
await createWindow()
app.on('activate', async function () {
if (BrowserWindow.getAllWindows().length === 0) await createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
+84
View File
@@ -0,0 +1,84 @@
import { BookTask } from '@/define/model/book/bookTask'
import { BookBasicHandle } from '../book/subBookHandle/bookBasicHandle'
import { AIWordMergeLong } from '@/define/data/aiData/aiPrompt/aiWordMerge/aiWordMergeLong'
import { AIWordMergeShort } from '@/define/data/aiData/aiPrompt/aiWordMerge/aiWordMergeShort'
import { AiReasonCommon } from '../aiReason/aiReasonCommon'
import { groupWordsByCharCount } from '@/define/Tools/write'
import { cloneDeep, isEmpty } from 'lodash'
import { OptionKeyName } from '@/define/enum/option'
export class AIStoryboard extends BookBasicHandle {
aiReasonCommon: AiReasonCommon
constructor() {
super()
this.aiReasonCommon = new AiReasonCommon()
}
AIStoryboardMerge = async (bookTaskId: string, type: BookTask.StoryboardMergeType) => {
try {
await this.InitBookBasicHandle()
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
if (!bookTask) throw new Error('小说批次任务未找到')
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTask.id
})
if (!bookTaskDetails || bookTaskDetails.length === 0) throw new Error('小说分镜信息未找到')
let word = bookTaskDetails.map((item) => item.afterGpt)
if (word == undefined || word.length === 0) throw new Error('分镜内容为空')
let groupWord = groupWordsByCharCount(word as string[], 500)
// 开始处理文案
await this.aiReasonCommon.InitAiReasonCommon()
// 获取推理设置
await this.aiReasonCommon.GetAISetting()
let result: string[] = []
for (let i = 0; i < groupWord.length; i++) {
// 开始做数据
let request: OpenAIRequest.Request
if (type === 'long') {
request = cloneDeep(AIWordMergeLong)
} else if (type === 'short') {
request = cloneDeep(AIWordMergeShort)
} else {
throw new Error('不支持的分镜合并类型')
}
const element = groupWord[i]
let newWord = element.map((item) => item).join('\n')
for (let j = 0; j < request.messages.length; j++) {
const messageItem = request.messages[j]
messageItem.content = this.aiReasonCommon.replaceObject(messageItem.content, {
textContent: newWord
})
}
// 判断模型是不是有设置值
let modelRes = this.optionRealmService.GetOptionByKey(
OptionKeyName.InferenceAI.StoryBoardAIModel
)
if (modelRes == null || isEmpty(modelRes.value)) {
delete request.model
} else {
request.model = modelRes.value as string
}
let res = await this.aiReasonCommon.FetchGpt(request.messages, {
...request
})
// console.log('res:', res)
result.push(res)
}
console.log('分镜合并结果:', result)
return result
} catch (error) {
throw error
}
}
}
+11
View File
@@ -0,0 +1,11 @@
import { mixin } from '@/main/utils/mixin'
import { AIStoryboard } from './aiStoryboard'
interface AIHandle extends AIStoryboard {}
@mixin(AIStoryboard)
class AIHandle {
constructor() {}
}
export const aiHandle = new AIHandle() as AIHandle
+301
View File
@@ -0,0 +1,301 @@
import { OptionRealmService } from '@/define/db/service/optionService'
import { OptionKeyName } from '@/define/enum/option'
import { optionSerialization } from '../option/optionSerialization'
import { SettingModal } from '@/define/model/setting'
import { isEmpty } from 'lodash'
import { GetOpenAISuccessResponse } from '@/define/response/openAIResponse'
import { GetApiDefineDataById } from '@/define/data/apiData'
import axios from 'axios'
import { RetryWithBackoff } from '@/define/Tools/common'
import { Book } from '@/define/model/book/book'
import { AiInferenceModelModel, GetAIPromptOptionByValue } from '@/define/data/aiData/aiData'
/**
* AI推理通用工具类
*
* AI推理相关的各种通用功能:
* -
* - API提供商和模型信息
* -
* -
* -
* -
*
* AI推理流程
*
*
* @class AiReasonCommon
* @example
* const aiReason = new AiReasonCommon();
* await aiReason.GetAISetting();
* const result = await aiReason.OriginalInferencePrompt(taskDetail, allDetails, 2, characterData);
*/
export class AiReasonCommon {
optionRealmService!: OptionRealmService
aiReasonSetting!: SettingModal.InferenceAISettings
/**
* * AiReasonCommon
* @returns {Promise<void>} - Promise
*/
async InitAiReasonCommon(): Promise<void> {
if (!this.optionRealmService) {
this.optionRealmService = await OptionRealmService.getInstance()
}
}
/**
*
* @returns {Promise<void>} - Promise
* @throws {Error} -
*/
async GetAISetting(): Promise<void> {
await this.InitAiReasonCommon()
let res = this.optionRealmService.GetOptionByKey(OptionKeyName.InferenceAI.InferenceSetting)
let aiReasonSetting = optionSerialization<SettingModal.InferenceAISettings>(
res,
'‘设置-> 推理设置’'
)
if (
isEmpty(aiReasonSetting.apiProvider) ||
isEmpty(aiReasonSetting.apiToken) ||
isEmpty(aiReasonSetting.inferenceModel) ||
isEmpty(aiReasonSetting.aiPromptValue)
) {
throw new Error(
'请检查 ‘设置-> 推理设置’ 的API提供商、API令牌、推理模型、推理模式等是不是存在!'
)
}
this.aiReasonSetting = aiReasonSetting
}
/**
* API提供商信息
* @returns
*/
GetAPIProviderMessage() {
let apiProviders = GetApiDefineDataById(this.aiReasonSetting.apiProvider)
return apiProviders
}
/**
*
* @returns {any} -
* @throws {Error} -
*/
GetInferenceModelMessage(): AiInferenceModelModel {
let selectInferenceModel = GetAIPromptOptionByValue(this.aiReasonSetting.aiPromptValue)
if (isEmpty(selectInferenceModel)) {
throw new Error('请检查推理模型是否存在!')
}
return selectInferenceModel
}
/**
*
*
* {key}
* replacements
*
* @param {string} content -
* @param {Record<string, string>} replacements -
* @returns {string}
*
* @example
* // 返回 "你好,张三,今天是星期一"
* replaceObject("你好,{name},今天是{day}", { name: "张三", day: "星期一" })
*/
replaceObject(content: string, replacements: Record<string, string>): string {
let result = content
for (let key in replacements) {
result = result.replaceAll(`{${key}}`, replacements[key])
}
return result
}
/**
*
* @param currentBookTaskDetail
* @param bookTaskDetails
* @param contextCount
*/
GetBookTaskDetailContextData(
currentBookTaskDetail: Book.SelectBookTaskDetail,
bookTaskDetails: Book.SelectBookTaskDetail[],
contextCount: number
): string {
let prefix = ''
// 拼接一个word
let i = (currentBookTaskDetail.no as number) - 1
if (i <= contextCount) {
prefix = bookTaskDetails
.filter((_item, index) => index < i)
.map((item) => item.afterGpt)
.join('\r\n')
} else if (i > contextCount) {
prefix = bookTaskDetails
.filter((_item, index) => i - index <= contextCount && i - index > 0)
.map((item) => item.afterGpt)
.join('\r\n')
}
let suffix = ''
let o_i = bookTaskDetails.length - i
if (o_i <= contextCount) {
suffix = bookTaskDetails
.filter((_item, index) => index > i)
.map((item) => item.afterGpt)
.join('\r\n')
} else if (o_i > contextCount) {
suffix = bookTaskDetails
.filter((_item, index) => index - i <= contextCount && index - i > 0)
.map((item) => item.afterGpt)
.join('\r\n')
}
return `${prefix}\r\n${currentBookTaskDetail.afterGpt}\r\n${suffix}`
}
/**
* message
* @param currentBookTaskDetail
* @param contextData
* @param autoAnalyzeCharacter
* @returns
*/
GetGPTRequestMessage(
currentBookTaskDetail: Book.SelectBookTaskDetail,
contextData: string,
autoAnalyzeCharacter: string,
selectInferenceModel: AiInferenceModelModel
): any[] {
let message: any = []
if (selectInferenceModel.hasExample) {
// // 有返回案例的
// message = gptDefine.GetExamplePromptMessage(global.config.gpt_auto_inference)
// // 加当前提问的
// message.push({
// role: 'user',
// content: currentBookTaskDetail.afterGpt
// })
} else {
// 直接返回,没有案例的
message = [
{
role: 'system',
content: this.replaceObject(selectInferenceModel.systemContent, {
textContent: contextData,
characterContent: autoAnalyzeCharacter
})
},
{
role: 'user',
content: this.replaceObject(selectInferenceModel.userContent, {
contextContent: contextData,
textContent: currentBookTaskDetail.afterGpt ?? '',
characterContent: autoAnalyzeCharacter,
wordCount: '40'
})
}
]
}
return message
}
/**
*
* @description
*
* @param {OpenAISuccessResponse} message -
* @returns {Promise<string>} - Promise
* @throws {Error} -
* @throws {Error} -
* @throws {Error} -
*
*/
async FetchGpt(message: any, option: any = {}): Promise<string> {
try {
let data = {
model: this.aiReasonSetting.inferenceModel,
messages: message,
...option
}
let apiProvider = this.GetAPIProviderMessage()
let config = {
method: 'post',
maxBodyLength: Infinity,
url: apiProvider.gpt_url,
headers: {
Authorization: `Bearer ${this.aiReasonSetting.apiToken}`,
'Content-Type': 'application/json'
},
data: JSON.stringify(data)
}
let res = await RetryWithBackoff(
async () => {
return await axios.request(config)
},
5,
2000
)
let content = GetOpenAISuccessResponse(res.data)
// this.GetResponseContent(res, this.gptUrl)
return content
} catch (error) {
throw error
}
}
/**
*
* @param currentBookTaskDetail
* @param bookTaskDetails
* @param contextCount
* @param autoAnalyzeCharacter
*/
async OriginalInferencePrompt(
currentBookTaskDetail: Book.SelectBookTaskDetail,
bookTaskDetails: Book.SelectBookTaskDetail[],
contextCount: number,
autoAnalyzeCharacter: string
) {
await this.GetAISetting()
// 获取当前的推理模式信息
let selectInferenceModel = this.GetInferenceModelMessage()
// 内置模式
let context = this.GetBookTaskDetailContextData(
currentBookTaskDetail,
bookTaskDetails,
contextCount
)
if (isEmpty(autoAnalyzeCharacter) && selectInferenceModel.mustCharacter) {
throw new Error('当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!')
}
let message = this.GetGPTRequestMessage(
currentBookTaskDetail,
context,
autoAnalyzeCharacter,
selectInferenceModel
)
// 开始请求
let res = await this.FetchGpt(message)
if (res) {
// 处理返回的数据,删除部分数据
res = res
.replace(/\)\s*\(/g, ', ')
.replace(/^\(/, '')
.replace(/\)$/, '')
.replaceAll('*', '')
.replaceAll('--', ' ')
}
return res
}
}
@@ -0,0 +1,41 @@
import { Book } from '@/define/model/book/book'
import { BookServiceHandle } from '../subBookHandle/bookServiceHandle'
/**
*
*/
export class BookDataEntrance {
bookServiceHandle: BookServiceHandle
constructor() {
this.bookServiceHandle = new BookServiceHandle()
}
//#region 小说数据相关
/** 添加或修改小说信息 */
AddOrModifyBook = async (book: Book.SelectBook) =>
await this.bookServiceHandle.AddOrModifyBook(book)
/** 获取小说信息,通过参数查询 */
GetBookDataCondition = async (queryCondition: Book.QueryBookCondition) =>
await this.bookServiceHandle.GetBookDataCondition(queryCondition)
/** 获取小说数据,通过小说ID查询 */
GetBookDataById = async (id: string) => await this.bookServiceHandle.GetBookDataById(id)
/** 修改小说数据,通过小说ID修改 */
ModifyBookDataById = async (id: string, bookData: Book.SelectBook) =>
await this.bookServiceHandle.ModifyBookDataById(id, bookData)
/** 重置小说数据 */
async ResetBookDataInfo(bookId: string) {
return await this.bookServiceHandle.ResetBookDataInfo(bookId)
}
/** 删除小说数据,通过小说ID删除 */
async DeleteBookDataInfoById(id: string) {
return await this.bookServiceHandle.DeleteBookDataInfoById(id)
}
//#endregion
}
@@ -0,0 +1,22 @@
/**
*
*
*
*
*/
import { BookVideoHandle } from "../subBookHandle/bookVideoHandle"
export class BookExportEntrance {
bookVideoHandle: BookVideoHandle
constructor() {
this.bookVideoHandle = new BookVideoHandle()
}
//#region 视频相关
/** 添加剪映草稿 */
AddJianyingDraft = async (bookTaskId: string) =>
await this.bookVideoHandle.AddJianyingDraft(bookTaskId)
//#endregion
}
@@ -0,0 +1,61 @@
/**
*
*
*
* BookImageHandle处理具体业务逻辑
*
* @class BookImageEntrance
*/
import { OperateBookType } from "@/define/enum/bookEnum"
import { BookImageHandle } from "../subBookHandle/bookImageHandle"
export class BookImageEntrance {
bookImageHandle: BookImageHandle
constructor() {
this.bookImageHandle = new BookImageHandle()
}
//#region 出图相关
/** 将指定图片设置为分镜的主图片 */
MoveImageToMainImage = async (
bookTaskId: string,
bookTaskDetailId: string,
sourceImagePath: string
) =>
await this.bookImageHandle.MoveImageToMainImage(bookTaskId, bookTaskDetailId, sourceImagePath)
/** 重置(删除)所有未锁定的分镜图片数据 */
ResetGenerateImage = async (id: string, operateBookType: OperateBookType) =>
await this.bookImageHandle.ResetGenerateImage(id, operateBookType)
/** 上传图片到分镜并更新相关信息 */
UpLoadImageToBookAndUpdateMessage = async (
bookTaskDetailId: string,
imageFile: string | string[],
option: string
) =>
await this.bookImageHandle.UpLoadImageToBookAndUpdateMessage(
bookTaskDetailId,
imageFile,
option
)
/** 对分镜图片进行高清处理 */
HDOneImage = async (bookTaskDetailId: string, scale: number) =>
await this.bookImageHandle.HDOneImage(bookTaskDetailId, scale)
/** 获取Midjourney图片URL并下载应用到分镜 */
GetImageUrlAndDownload = async (
id: string,
operateBookType: OperateBookType,
coverData: boolean
) => await this.bookImageHandle.GetImageUrlAndDownload(id, operateBookType, coverData)
/** 下载图片并拆分处理应用到分镜 */
DownloadImageUrlAndSplit = async (bookTaskDetailId: string, imageUrl: string) =>
await this.bookImageHandle.DownloadImageUrlAndSplit(bookTaskDetailId, imageUrl)
//#endregion
}
@@ -0,0 +1,51 @@
/**
*
*
* AI提示词生成
* BookPromptHandle和TranslateHandle来提供完整的书籍处理服务
*
* @class BookPromptEntrance
*/
import { PresetCategory } from '@/define/data/presetData'
import { OperateBookType, PromptMergeType } from '@/define/enum/bookEnum'
import { TranslateModel } from '@/define/model/translate'
import { BookPromptHandle } from '../subBookHandle/bookPromptHandle'
import { TranslateHandle } from '../../translate'
import { BookTask } from '@/define/model/book/bookTask'
export class BookPromptEntrance {
bookPromptHandle: BookPromptHandle
translateHandle: TranslateHandle
constructor() {
this.bookPromptHandle = new BookPromptHandle()
this.translateHandle = new TranslateHandle()
}
//#region 提示词
/** 为小说分镜生成AI提示词 */
OriginalGetAiPrompt = async (id: string, operateBookType: OperateBookType, coverData: boolean) =>
await this.bookPromptHandle.OriginalGetAiPrompt(id, operateBookType, coverData)
/** AI分镜头合并 */
AIStoryboardMerge = async (bookTaskId: string, type: BookTask.StoryboardMergeType) =>
await this.bookPromptHandle.AIStoryboardMerge(bookTaskId, type)
/** 合并提示词 */
MergePrompt = async (id: string, type: PromptMergeType, operateBookType: OperateBookType) =>
await this.bookPromptHandle.MergePrompt(id, type, operateBookType)
/** 自动分析提示词或场景数据 */
AutoAnalyzeCharacterOrScene = async (bookTaskId: string, type: PresetCategory) =>
await this.bookPromptHandle.AutoAnalyzeCharacterOrScene(bookTaskId, type)
//#endregion
//#region 翻译相关
/** 翻译小说分镜数据 */
TranslateBookTaskDetailPromptNowReturn = async (value: TranslateModel.TranslateNowIPCParams[]) =>
await this.translateHandle.TranslateBookTaskDetailPromptNowReturn(value)
//#endregion
}
@@ -0,0 +1,50 @@
/**
*
*
*
* BookTaskDetailServiceHandle的调用
*/
import { OperateBookType } from '@/define/enum/bookEnum'
import { Book } from '@/define/model/book/book'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import { BookTaskDetailServiceHandle } from '../subBookHandle/bookTaskDetailServiceHandle'
export class BookTaskDetailEntrance {
bookTaskDetailServiceHandle: BookTaskDetailServiceHandle
constructor() {
this.bookTaskDetailServiceHandle = new BookTaskDetailServiceHandle()
}
//#region 小说批次任务详细数据相关
/** 获取小说子任务详细数据 */
GetBookTaskDetailDataByCondition = async (
bookTaskDetailCondition: Book.QueryBookTaskDetailCondition
) =>
await this.bookTaskDetailServiceHandle.GetBookTaskDetailDataByCondition(bookTaskDetailCondition)
/** 获取小说子任务详细数据,通过小说ID查询 */
GetBookTaskDetailDataById = async (id: string) =>
await this.bookTaskDetailServiceHandle.GetBookTaskDetailDataById(id)
/** 修改小说子任务详细数据 */
ModifyBookTaskDetailById = async (
bookTaskDetailId: string,
updateData: Book.SelectBookTaskDetail
) => await this.bookTaskDetailServiceHandle.ModifyBookTaskDetailById(bookTaskDetailId, updateData)
/** 保存小说批次数据分镜信息 */
SaveCopywritingInfo = async (
bookTaskId: string,
copywritingData: BookTaskDetail.SaveCopywritingData[],
operateBookType: OperateBookType
) =>
await this.bookTaskDetailServiceHandle.SaveCopywritingInfo(
bookTaskId,
copywritingData,
operateBookType
)
//#endregion
}
@@ -0,0 +1,80 @@
import { BookTaskStatus } from '@/define/enum/bookEnum'
import { Book } from '@/define/model/book/book'
import { BookTask } from '@/define/model/book/bookTask'
import { BookTaskServiceHandle } from '../subBookHandle/bookTaskServiceHandle'
/**
*
*
*/
export class BookTaskEntrance {
bookTaskServiceHandle: BookTaskServiceHandle
constructor() {
this.bookTaskServiceHandle = new BookTaskServiceHandle()
}
//#region 小说子任务数据相关
/** 获取小说子任务数据 */
async GetBookTaskDataByCondition(bookTaskCondition: Book.QueryBookTaskCondition) {
return await this.bookTaskServiceHandle.GetBookTaskDataByCondition(bookTaskCondition)
}
/** 获取小说子任务数据,通过小说ID查询 */
async GetBookTaskDataById(id: string) {
return await this.bookTaskServiceHandle.GetBookTaskDataById(id)
}
/** 更新小说批次任务状态 */
async ModifyBookTaskStatus(bookTaskId: string, status: BookTaskStatus, errorMsg?: string) {
return await this.bookTaskServiceHandle.ModifyBookTaskStatus(bookTaskId, status, errorMsg)
}
/** 修改小说批次任务数据,通过小说ID修改 */
async ModifyBookTaskDataById(bookTaskId: string, data: Book.SelectBookTask) {
return await this.bookTaskServiceHandle.ModifyBookTaskDataById(bookTaskId, data)
}
/** 删除小说批次任务数据,通过小说ID删除 */
async DeleteBookTaskByIds(ids: string[]) {
return await this.bookTaskServiceHandle.DeleteBookTaskByIds(ids)
}
/** 添加小说任务 */
async AddNewBookTask(addData: BookTask.AddBookTaskParam) {
return await this.bookTaskServiceHandle.AddNewBookTask(addData)
}
/** 获取小说批次任务的图片生成进度 */
async GetBookTaskImageGenerateProgress(bookId: string) {
return await this.bookTaskServiceHandle.GetBookTaskImageGenerateProgress(bookId)
}
/** 获取小说批次任务的第一张图片路径 */
async GetBookTaskFirstImagePath(bookId: string) {
return await this.bookTaskServiceHandle.GetBookTaskFirstImagePath(bookId)
}
/** 添加小说子任务数据 */
async AddBookTask(bookTask: Book.SelectBookTask) {
return await this.bookTaskServiceHandle.AddBookTask(bookTask)
}
/** 重置小说子任务数据 */
async ResetBookTaskDataById(bookTaskId: string) {
return await this.bookTaskServiceHandle.ResetBookTaskDataById(bookTaskId)
}
/** 重置小说分镜数据(不包含bootask本身数据),并且初始化全新的分镜信息 */
async ResetAndInitializeBookTask(bookTaskId: string) {
return await this.bookTaskServiceHandle.ResetAndInitializeBookTask(bookTaskId)
}
/** 删除小说批次任务数据 */
async DeleteBookTaskDataById(bookTaskId: string) {
return await this.bookTaskServiceHandle.DeleteBookTaskDataById(bookTaskId)
}
//#endregion
}
+33
View File
@@ -0,0 +1,33 @@
import { BookDataEntrance } from './bookIndex/bookDataEntrance'
import { BookTaskEntrance } from './bookIndex/bookTaskEmtrance'
import { BookTaskDetailEntrance } from './bookIndex/bookTaskDetailEntrance'
import { BookPromptEntrance } from './bookIndex/bookPromptEntrance'
import { BookImageEntrance } from './bookIndex/bookImageEntrance'
import { BookExportEntrance } from './bookIndex/bookExportEntrance'
import { mixin } from '@/main/utils/mixin'
// 使用装饰器实现多重继承
// 类型接口声明,让TypeScript知道合并后的类型
interface BookHandle
extends BookDataEntrance,
BookTaskEntrance,
BookTaskDetailEntrance,
BookPromptEntrance,
BookImageEntrance,
BookExportEntrance {}
@mixin(
BookTaskEntrance,
BookTaskDetailEntrance,
BookPromptEntrance,
BookImageEntrance,
BookExportEntrance
)
class BookHandle extends BookDataEntrance {
constructor() {
super()
}
}
// debugger
export const bookHandle = new BookHandle() as BookHandle
@@ -0,0 +1,38 @@
import { BookTaskDetailService } from '@/define/db/service/book/bookTaskDetailService'
import { BookTaskService } from '@/define/db/service/book/bookTaskService'
import { OptionRealmService } from '@/define/db/service/optionService'
import { BookService } from '@/define/db/service/book/bookService'
export class BookBasicHandle {
bookTaskDetailService!: BookTaskDetailService
bookTaskService!: BookTaskService
optionRealmService!: OptionRealmService
bookService!: BookService
constructor() {
// 初始化
}
async InitBookBasicHandle() {
// 如果 bookTaskDetailService 已经初始化,则直接返回
if (!this.bookTaskDetailService) {
this.bookTaskDetailService = await BookTaskDetailService.getInstance()
}
if (!this.bookTaskService) {
this.bookTaskService = await BookTaskService.getInstance()
}
if (!this.optionRealmService) {
this.optionRealmService = await OptionRealmService.getInstance()
}
if (!this.bookService) {
this.bookService = await BookService.getInstance()
}
}
async transaction(callback: (realm: any) => void) {
await this.InitBookBasicHandle()
this.bookService.transaction(() => {
callback(this.bookService.realm)
})
}
}
@@ -0,0 +1,724 @@
import path from 'path'
import fs from 'fs'
import { BookBasicHandle } from './bookBasicHandle'
import {
CheckFileOrDirExist,
CheckFolderExistsOrCreate,
CopyFileOrFolder,
DownloadImageFromUrl
} from '@/define/Tools/file'
import { getProjectPath } from '../../option/optionCommonService'
import { errorMessage, successMessage } from '@/public/generalTools'
import { GeneralResponse } from '@/define/model/generalResponse'
import { BookType, MJAction, OperateBookType } from '@/define/enum/bookEnum'
import { Book } from '@/define/model/book/book'
import { isEmpty } from 'lodash'
import { ImageGenerateMode } from '@/define/data/mjData'
import { define } from '@/define/define'
import util from 'util'
import { exec } from 'child_process'
import { ImageCategory } from '@/define/data/imageData'
import { OptionKeyName } from '@/define/enum/option'
import { optionSerialization } from '../../option/optionSerialization'
import { SettingModal } from '@/define/model/setting'
const execAsync = util.promisify(exec)
import { MJApiService } from '../../mj/mjApiService'
import { ImageSplit } from '@/define/Tools/image'
export class BookImageHandle extends BookBasicHandle {
mjApiService: MJApiService
constructor() {
// 初始化
super()
this.mjApiService = new MJApiService()
}
/**
*
*
* ID
*
*
*
* @param {string} bookTaskId - ID
* @param {string} bookTaskDetailId - ID
* @param {string} sourceImagePath - 可以是本地路径或file:协议的URL
* @returns {Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem>}
*
*
* @throws {Error}
*
* @example
* // 设置ID为"detail-123"的分镜主图
* const result = await bookImageHandle.MoveImageToMainImage(
* "task-456",
* "detail-123",
* "D:/temp/selected_image.png"
* );
* if (result.code === 1) {
* console.log("设置成功,新图片路径:", result.data);
* }
*/
public async MoveImageToMainImage(
bookTaskId: string,
bookTaskDetailId: string,
sourceImagePath: string
): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
try {
await this.InitBookBasicHandle()
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
let bookTaskDetail =
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到对应的小说子任务数据,请检查ID是否正确')
}
// 获取小说项目的地址
let imageFolder = bookTask.imageFolder
if (imageFolder == null) {
throw new Error('没有找到对应的小说项目的图片地址,请检查')
}
let currentImagePath = path.join(imageFolder, `${bookTaskDetail.name}.png`)
// 判断地址对应的文件是不是存在
if (await CheckFileOrDirExist(currentImagePath)) {
await fs.promises.unlink(currentImagePath)
}
sourceImagePath = sourceImagePath.split('?t=')[0]
sourceImagePath = sourceImagePath.split('?time=')[0]
if (sourceImagePath.startsWith('file:/')) {
sourceImagePath = sourceImagePath.replace('file:///', '')
sourceImagePath = sourceImagePath.replace('file://', '')
sourceImagePath = sourceImagePath.replace('file:/', '')
}
sourceImagePath = path.resolve(sourceImagePath)
if (!(await CheckFileOrDirExist(sourceImagePath))) {
throw new Error(`图片文件 ${sourceImagePath} 不存在,请检查`)
}
// 复制文件,判断父文件夹是不是存在
await CopyFileOrFolder(sourceImagePath, currentImagePath, true)
let projectPath = await getProjectPath()
// 修改数据库数据
await this.bookTaskDetailService.ModifyBookTaskDetailById(bookTaskDetailId, {
outImagePath: path.relative(projectPath, currentImagePath)
})
// 返回数据
return successMessage(
currentImagePath + `?t=${Date.now()}`,
'将指定的图片放到主图中成功',
'BookImage_MoveImageToMainImage'
)
} catch (error: any) {
return errorMessage(
'将指定的图片放到主图中失败,错误信息如下:' + error.message,
'BookImage_MoveImageToMainImage'
)
}
}
/**
*
*
*
*
* 1.
* 2.
*
* "Reset"
*
* @param {string} id - IDID或分镜IDoperateBookType参数
* @param {OperateBookType} operateBookType - id参数代表的是书籍任务还是单个分镜
* - BOOKTASK: 删除整个书籍任务下的所有未锁定分镜图片
* - BOOKTASKDETAIL: 仅删除单个指定分镜的图片
*
* @returns {Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem>}
*
*
* @throws {Error} ID的数据
*
* @example
* // 删除整个书籍任务的图片数据
* const result = await bookImageHandle.ResetGenerateImage(
* "task-123",
* OperateBookType.BOOKTASK
* );
*
* // 删除单个分镜的图片数据
* const result = await bookImageHandle.ResetGenerateImage(
* "detail-456",
* OperateBookType.BOOKTASKDETAIL
* );
*/
async ResetGenerateImage(
id: string,
operateBookType: OperateBookType
): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
try {
await this.InitBookBasicHandle()
let bookTaskDetails: Book.SelectBookTaskDetail[] = []
if (operateBookType == OperateBookType.BOOKTASK) {
bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: id
})
} else if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
let tempBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (tempBookTaskDetail == null) {
throw new Error('没有找到要删除的分镜数据,请检查ID是否正确')
}
bookTaskDetails = [tempBookTaskDetail]
} else {
throw new Error('不支持的操作类型,请检查')
}
//这边过滤被锁定的数据
bookTaskDetails = bookTaskDetails.filter((item) => !item.imageLock)
if (bookTaskDetails.length <= 0) {
throw new Error('没有要删除的分镜数据,请检查')
}
// 开始删除数据,要删除图片数据和出图的信息
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i]
this.bookTaskDetailService.DeleteBoookTaskDetailGenerateImage(element.id as string)
// 上面的信息重置完毕之后,开始删除文件信息
let outImage = element.outImagePath
if (await CheckFileOrDirExist(outImage)) {
await fs.promises.unlink(outImage as string)
}
}
return successMessage(
bookTaskDetails,
'删除所有的生图数据成功',
'BookImage_ResetGenerateImage'
)
} catch (error: any) {
return errorMessage(
'删除所有的图片数据失败,失败信息如下:' + error.toString(),
'BookImage_ResetGenerateImage'
)
}
}
/**
*
*
*
*
*
* :
* 1.
* 2.
* 3.
* 4.
* 5.
*
* @param {string} bookTaskDetailId - ID
* @param {string} imageFile -
* @param {string} option - :
* - 'outImagePath':
* - 'subImagePath':
*
* @returns {Promise<string>} ()
* @throws {Error}
*
* @private 使
*/
private async UploadOne(
bookTaskDetailId: string,
imageFile: string,
option: string
): Promise<string> {
console.log('开始上传图片', bookTaskDetailId, imageFile, option)
if (!(await CheckFileOrDirExist(path.resolve(imageFile)))) {
throw new Error(`图片文件 ${imageFile} 不存在,请检查`)
}
// 修改数据库数据
let bookTaskDetail =
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到对应的小说子任务数据,请检查ID是否正确')
}
let projectPath = await getProjectPath()
// 将图片复制到对应的文件夹中
// 生成一个0-10的随机数
let random = Math.floor(Math.random() * 10)
let newImagePath = path.join(
projectPath,
`${bookTaskDetail.bookId}/data/Upload/${bookTaskDetail.name}_${Date.now()}_${random}.png`
)
await CheckFolderExistsOrCreate(path.dirname(newImagePath))
await CopyFileOrFolder(imageFile, newImagePath)
if (option == 'outImagePath') {
let outImagePath = bookTaskDetail.outImagePath
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
)
if (isEmpty(bookTaskDetail.outImagePath)) {
outImagePath = path.join(
projectPath,
`${bookTaskDetail.bookId}/tmp/${bookTask.name}/${bookTaskDetail.name}.png`
)
}
await CheckFolderExistsOrCreate(path.dirname(outImagePath as string))
await CopyFileOrFolder(newImagePath, outImagePath)
await this.bookTaskDetailService.ModifyBookTaskDetailById(bookTaskDetailId, {
outImagePath: path.relative(projectPath, outImagePath as string)
})
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(bookTaskDetailId, {
progress: 100,
status: 'success',
category: ImageGenerateMode.MJ_API,
messageId: '',
action: MJAction.IMAGINE
})
return outImagePath + '?t=' + new Date().getTime()
} else if (option == 'subImagePath') {
// 修改数据库数据
let subImagePath = bookTaskDetail.subImagePath ?? []
subImagePath.push(newImagePath)
await this.bookTaskDetailService.ModifyBookTaskDetailById(bookTaskDetailId, {
subImagePath: subImagePath.map((item) => path.relative(projectPath, item))
})
return newImagePath + '?t=' + new Date().getTime()
} else {
throw new Error('无效的操作类型,请检查')
}
}
/**
*
*
*
*
*
*
* @param {string} bookTaskDetailId - ID
* @param {string | string[]} imageFile -
* @param {string} option -
* - 'outImagePath':
* - 'subImagePath':
*
* @returns {Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem>}
* ()
* @throws {Error}
*
* @example
* // 上传主图
* const mainResult = await bookImageHandle.UpLoadImageToBookAndUpdateMessage(
* "detail-123",
* "D:/images/main.png",
* "outImagePath"
* );
*
* // 上传多个子图
* const subResult = await bookImageHandle.UpLoadImageToBookAndUpdateMessage(
* "detail-123",
* ["D:/images/sub1.png", "D:/images/sub2.png"],
* "subImagePath"
* );
*/
public async UpLoadImageToBookAndUpdateMessage(
bookTaskDetailId: string,
imageFile: string | string[],
option: string
): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
try {
await this.InitBookBasicHandle()
if (option == 'outImagePath') {
let res = await this.UploadOne(bookTaskDetailId, imageFile as string, option)
return successMessage(
res,
'上传图片,并修改小说信息成功',
'BookImage_UpLoadImageToBookAndUpdateMessage'
)
} else if (option == 'subImagePath') {
let subImage = [] as string[]
for (let i = 0; i < (imageFile as string[]).length; i++) {
let res = await this.UploadOne(bookTaskDetailId, imageFile[i], option)
subImage.push(res)
}
return successMessage(
subImage as string[],
'上传图片,并修改小说信息成功',
'BookImage_UpLoadImageToBookAndUpdateMessage'
)
} else {
throw new Error('无效的操作类型,请检查')
}
} catch (error: any) {
return errorMessage(
'上传图片到小说中,并修改小说信息失败,错误信息如下:' + error.message,
'BookImage_UpLoadImageToBookAndUpdateMessage'
)
}
}
/**
*
*
*
*
*
*
* :
* 1.
* 2.
* 3.
* 4. (rnv.exe)
* 5.
*
* @param {string} bookTaskDetailId - ID
* @param {number} scale - 2
* @returns {Promise<string>}
* @throws {Error}
*
* @private 使
*/
private async HDOneImagePrivate(bookTaskDetailId: string, scale: number = 2): Promise<string> {
let bookTaskDetail =
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到要高清的分镜数据,请检查ID是否正确')
}
let outImagePath = bookTaskDetail.outImagePath as string
if (isEmpty(outImagePath)) {
throw new Error('没有找到要高清的分镜数据,请检查ID是否正确')
}
let imageExist = await CheckFileOrDirExist(outImagePath as string)
if (!imageExist) {
throw new Error('没有找到要高清的图片,请检查图片是否存在')
}
let newImagePath = path.join(
path.dirname(outImagePath as string),
`${path.basename(outImagePath as string, '.png')}_hd.png`
)
// 开始高清
await CopyFileOrFolder(outImagePath as string, newImagePath, true)
let command = `"${path.join(define.package_path, 'Improve/rnv.exe')}" -i "${newImagePath}" -o "${outImagePath}" -s ${scale}`
await execAsync(command, { maxBuffer: 1024 * 1024 * 10, encoding: 'utf-8' })
// 高清完成之后删除临时文件
await fs.promises.unlink(newImagePath)
return outImagePath
}
/**
*
*
* 使AI超分辨率技术对分镜的主图进行放大增强处理
* RealESRGAN-ncnn-vulkan工具执行实际的超分辨率算法
*
*
* @param {string} bookTaskDetailId - ID
* @param {number} scale - 2
* @returns {Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem>}
* ()
*
* @example
* // 将ID为"detail-123"的分镜图片放大2倍
* const result = await bookImageHandle.HDOneImage("detail-123");
* if (result.code === 1) {
* console.log("高清处理成功,图片路径:", result.data);
* }
*
* // 将ID为"detail-456"的分镜图片放大4倍
* const result = await bookImageHandle.HDOneImage("detail-456", 4);
*/
public async HDOneImage(
bookTaskDetailId: string,
scale: number = 2
): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
try {
await this.InitBookBasicHandle()
let outImagePath = await this.HDOneImagePrivate(bookTaskDetailId, scale)
// 高清完成
return successMessage(
outImagePath + '?t=' + new Date().getTime(),
'高清分镜成功',
'BookImage_HDOneImage'
)
} catch (error: any) {
return errorMessage('高清分镜失败,失败信息如下:' + error.toString(), 'BookImage_HDOneImage')
}
}
/**
*
*
* URL下载图片或复制本地图片文件
* Midjourney图片
*
*
* :
* 1.
* 2. (URL或本地文件)
* 3. (MJ图片拆分使)
* 4.
* 5.
*
* @param {string} bookTaskDetailId - ID
* @param {string} imageUrl - URL或本地文件路径
*
* @returns {Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem>}
*
*
* @throws {Error} MJ图片拆分失败时抛出错误
*
* @example
* // 从网络URL下载图片并应用到分镜
* const result = await bookImageHandle.DownloadImageUrlAndSplit(
* "detail-123",
* "https://example.com/image.png"
* );
*
* // 从本地路径复制图片并应用到分镜
* const result = await bookImageHandle.DownloadImageUrlAndSplit(
* "detail-123",
* "D:/images/local_image.png"
* );
*/
async DownloadImageUrlAndSplit(
bookTaskDetailId: string,
imageUrl: string
): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
await this.InitBookBasicHandle()
let bookTaskDetail =
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('获取到的数据分镜为空,无法执行操作')
}
let book = await this.bookService.GetBookDataById(bookTaskDetail.bookId as string)
if (book == null) {
throw new Error('获取到的小说为空,无法执行操作')
}
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
)
if (bookTask == null) {
throw new Error('获取到的任务为空,无法执行操作')
}
let imagePath = path.join(
book.bookFolderPath as string,
`data\\MJOriginalImage\\${bookTaskDetail.id}_${new Date().getTime()}.png`
)
// 判断是不是一个链接
const urlRegex = /^(http|https):\/\/[^ "]+$/
if (!urlRegex.test(imageUrl)) {
// 本地图片,直接复制
await CopyFileOrFolder(imageUrl, imagePath)
} else {
// 网络图片下载
await DownloadImageFromUrl(imageUrl, imagePath)
}
let imageCategory = bookTask.imageCategory
let out_file: string
let imageRes: string[] = []
if (imageCategory == ImageCategory.Midjourney) {
// 只有MJ需要裁剪,其他的不需要
// 进行图片裁剪
imageRes = await ImageSplit(
imagePath,
bookTaskDetail.name as string,
path.join(book.bookFolderPath as string, 'data\\MJOriginalImage')
)
if (imageRes && imageRes.length < 4) {
throw new Error('图片裁剪失败')
}
// 修改数据
// 修改数据库数据,将图片保存到对应的文件夹中
let firstImage = imageRes[0]
if (book.type == BookType.ORIGINAL) {
await CopyFileOrFolder(
firstImage,
path.join(
book.bookFolderPath as string,
`data\\import\\${bookTaskDetail.name}_${new Date().getTime()}.png`
)
)
}
out_file = path.join(bookTask.imageFolder as string, `${bookTaskDetail.name}.png`)
await CopyFileOrFolder(firstImage, out_file)
} else {
// 其他的导入,每次只能导入一张图
out_file = path.join(bookTask.imageFolder as string, `${bookTaskDetail.name}.png`)
if (book.type == BookType.ORIGINAL) {
await CopyFileOrFolder(
imagePath,
path.join(
book.bookFolderPath as string,
`data\\import\\${bookTaskDetail.name}_${new Date().getTime()}.png`
)
)
}
await CopyFileOrFolder(imagePath, out_file)
imageRes = [out_file]
}
let projectPath = await getProjectPath()
// 修改分镜的数据
await this.bookTaskDetailService.ModifyBookTaskDetailById(bookTaskDetailId, {
outImagePath: path.relative(projectPath, out_file),
subImagePath: imageRes.map((item) => path.relative(projectPath, item))
})
let mjMessage = {
status: 'success',
progress: 100,
category: ImageGenerateMode.MJ_API,
messageId: '',
action: MJAction.IMAGINE
}
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(bookTaskDetailId, mjMessage)
return successMessage(
{
outImagePath: out_file + '?time=' + new Date().getTime(),
subImagePath: imageRes.map((item) => item + '?time=' + new Date().getTime()),
mjMessage: mjMessage
},
'下载指定的图片地址并且分割成功',
'BookImage_DownloadImageUrlAndSplit'
)
} catch (error: any) {
return {
code: 0,
message: '下载指定的图片地址并且分割错误,错误信息如下:' + error.message
}
}
}
/**
* Midjourney图片URL并下载应用到分镜
*
* MJ API获取已生成任务的图片URL
*
* MJ图片链接
*
* MJ API模式下有效
*
* @param {string} id - IDID或分镜IDoperateBookType参数
* @param {OperateBookType} operateBookType - id参数代表的是书籍任务还是单个分镜
* - BOOKTASK: 处理整个书籍任务下的所有分镜
* - BOOKTASKDETAIL: 仅处理单个指定分镜
* @param {boolean} coverData -
* - true:
* - false:
*
* @returns {Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem>}
*
*
* @throws {Error} MJ模式MJ API模式
*
*
* @example
* // 下载并应用整个书籍任务的所有分镜图片(不覆盖已有图片)
* const result = await bookImageHandle.GetImageUrlAndDownload(
* "task-123",
* OperateBookType.BOOKTASK,
* false
* );
*
* // 下载并应用单个分镜图片(覆盖已有图片)
* const result = await bookImageHandle.GetImageUrlAndDownload(
* "detail-456",
* OperateBookType.BOOKTASKDETAIL,
* true
* );
*/
async GetImageUrlAndDownload(
id: string,
operateBookType: OperateBookType,
coverData: boolean
): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
try {
console.log('GetImageUrlAndDownload', id, operateBookType, coverData)
await this.InitBookBasicHandle()
let bookTaskDetail: Book.SelectBookTaskDetail[] = []
let bookTask: Book.SelectBookTask
if (operateBookType == OperateBookType.BOOKTASK) {
bookTask = await this.bookTaskService.GetBookTaskDataById(id)
bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTask.id
})
// 这边过滤出图成功的数据
if (!coverData) {
bookTaskDetail = bookTaskDetail.filter((item) => !item.outImagePath)
}
} else if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
let currentBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (currentBookTaskDetail == null) {
throw new Error('没有找到要采集的分镜数据,请检查ID是否正确')
}
bookTask = await this.bookTaskService.GetBookTaskDataById(
currentBookTaskDetail.bookTaskId as string
)
bookTaskDetail = [currentBookTaskDetail]
} else {
throw new Error('不支持的操作类型')
}
// 这边再做个详细的筛选
if (bookTaskDetail.length < 0) {
throw new Error('没有找到需要采集的数据')
}
if (bookTask.imageCategory != ImageCategory.Midjourney) {
throw new Error('只有MJ模式下才能使用这个功能')
}
let mjGeneralSettingOption = this.optionRealmService.GetOptionByKey(
OptionKeyName.Midjourney.GeneralSetting
)
let mjGeneralSetting = optionSerialization<SettingModal.MJGeneralSettings>(
mjGeneralSettingOption,
'‘设置 -> MJ设置’'
)
if (mjGeneralSetting.outputMode != ImageGenerateMode.MJ_API) {
throw new Error('只有MJ API模式下才能使用这个功能')
}
let result: any[] = []
for (let i = 0; i < bookTaskDetail.length; i++) {
const element = bookTaskDetail[i]
if (!element.mjMessage) continue
if (element.mjMessage.status == 'error') continue
if (isEmpty(element.mjMessage.messageId)) continue
// 这边开始采集
let res = await this.mjApiService.GetMJAPITaskById(
element.mjMessage.messageId as string,
''
)
if (isEmpty(res.imagePath)) {
throw new Error('获取图片地址链接为空')
}
// 开始下载
let dr = await this.DownloadImageUrlAndSplit(element.id as string, res.imagePath as string)
if (dr.code == 0) {
throw new Error(dr.message)
}
result.push({
id: element.id,
data: dr.data
})
}
if (result.length <= 0) {
throw new Error('没有找到需要采集的数据')
}
return successMessage(result, '获取图片链接并且下载成功', 'BookImage_GetImageUrlAndDownload')
} catch (error: any) {
return errorMessage(
'获取图片链接并且下载失败,错误信息如下:' + error.message,
'BookImage_GetImageUrlAndDownload'
)
}
}
}
@@ -0,0 +1,379 @@
import { OperateBookType, PromptMergeType } from '@/define/enum/bookEnum'
import { Book } from '@/define/model/book/book'
import { errorMessage, SendReturnMessage, successMessage } from '@/public/generalTools'
import { isEmpty } from 'lodash'
import { AiReasonCommon } from '../../aiReason/aiReasonCommon'
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { GeneralResponse } from '@/define/model/generalResponse'
import { ExecuteConcurrently } from '@/define/Tools/common'
import { OptionKeyName } from '@/define/enum/option'
import { optionSerialization } from '../../option/optionSerialization'
import { SettingModal } from '@/define/model/setting'
import { MJServiceHandle } from '@/main/service/mj/mjServiceHandle'
import { BookBasicHandle } from './bookBasicHandle'
import { PresetCategory } from '@/define/data/presetData'
import { aiPrompts } from '@/define/data/aiData/aiPrompt'
import { ValidateJsonAndParse } from '@/define/Tools/validate'
import { BookTask } from '@/define/model/book/bookTask'
import { SDServiceHandle } from '../../sd/sdServiceHandle'
import { aiHandle } from '../../ai'
export class BookPromptHandle extends BookBasicHandle {
aiReasonCommon: AiReasonCommon
mjServiceHandle: MJServiceHandle
sdServiceHandle: SDServiceHandle
constructor() {
super()
this.aiReasonCommon = new AiReasonCommon()
this.mjServiceHandle = new MJServiceHandle()
this.sdServiceHandle = new SDServiceHandle()
}
/**
* AI提示词
*
* 使AI推理为每个分镜生成提示词
*
* 1. BOOKTASK
* 2. BOOKTASKDETAIL
* 3. UNDERBOOKTASK
*
*
*
* @param {string} id - operateBookType不同ID或分镜ID
* @param {OperateBookType} operateBookType -
* @param {boolean} coverData -
* true-, false-
* @returns {Promise<SuccessItem | ErrorItem>}
*
* @throws
*
* @example
* // 为整个小说任务生成提示词,不覆盖已有数据
* const result = await bookPromptHandle.OriginalGetAiPrompt(
* "task-123",
* OperateBookType.BOOKTASK,
* false
* );
*/
OriginalGetAiPrompt = async (
id: string,
operateBookType: OperateBookType,
coverData: boolean
): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> => {
try {
let bookTask: Book.SelectBookTask = {} as Book.SelectBookTask
let bookTaskDetails: Book.SelectBookTaskDetail[] = []
let allBookTaskDetails: Book.SelectBookTaskDetail[] = []
await this.InitBookBasicHandle()
if (operateBookType == OperateBookType.BOOKTASK) {
bookTask = await this.bookTaskService.GetBookTaskDataById(id)
allBookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: id
})
if (!coverData) {
// 不覆盖数据,只推理空白提示词
bookTaskDetails = allBookTaskDetails.filter((item) => isEmpty(item.gptPrompt))
} else {
// 不覆盖,就是全部
bookTaskDetails = allBookTaskDetails
}
} else if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
let singleBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (singleBookTaskDetail == null) {
throw new Error('没有找到要推理的分镜数据,请检查ID是否正确')
}
bookTask = await this.bookTaskService.GetBookTaskDataById(
singleBookTaskDetail.bookTaskId as string
)
bookTaskDetails = [singleBookTaskDetail]
} else if (operateBookType == OperateBookType.UNDERBOOKTASK) {
let singleBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (singleBookTaskDetail == null) {
throw new Error('没有找到要推理的分镜数据,请检查ID是否正确')
}
bookTask = await this.bookTaskService.GetBookTaskDataById(
singleBookTaskDetail.bookTaskId as string
)
// 获取全部的分镜数据
allBookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTask.id
})
// 只要当前行往下的数据
for (let i = 0; i < allBookTaskDetails.length; i++) {
const element = allBookTaskDetails[i]
if (bookTaskDetails == undefined) {
bookTaskDetails = []
}
if (i + 1 >= (singleBookTaskDetail.no as number)) {
bookTaskDetails.push(element)
}
}
} else {
throw new Error('未知的操作类型')
}
if (!allBookTaskDetails || allBookTaskDetails.length <= 0) {
allBookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTask.id
})
}
if (bookTaskDetails.length <= 0) {
throw new Error('没有找到要推理的分镜数据')
}
let generalSettingOption = this.optionRealmService.GetOptionByKey(
OptionKeyName.Software.GeneralSetting
)
let generalSetting = optionSerialization<SettingModal.GeneralSettings>(
generalSettingOption,
'‘设置 -> 通用设置’'
)
let tasks = [] as Array<() => Promise<any>>
// 获取当前task的角色和场景数据
let autoAnalyzeCharacter = bookTask.autoAnalyzeCharacter ?? '{}'
let autoAnalyzeCharacterData =
ValidateJsonAndParse<BookTask.BookTaskCharacterAndScene>(autoAnalyzeCharacter)
let characterData = autoAnalyzeCharacterData[PresetCategory.Character] ?? []
let sceneData = autoAnalyzeCharacterData[PresetCategory.Scene] ?? []
let characterString = ''
let sceneString = ''
let characterAndScene = ''
if (characterData.length > 0) {
characterString = characterData.map((item) => item.name + '' + item.prompt).join('\n')
characterAndScene = '角色设定:' + '\n' + characterString
}
if (sceneData.length > 0) {
sceneString = sceneData.map((item) => item.name + '' + item.prompt).join('\n')
characterAndScene = characterAndScene + '\n' + '场景设定:' + '\n' + sceneString
}
// 添加异步任务
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i]
tasks.push(async () => {
let content = await this.aiReasonCommon.OriginalInferencePrompt(
element,
allBookTaskDetails,
15, // 上下文关联行数
characterAndScene
)
// 修改推理出来的数据
await this.bookTaskDetailService.ModifyBookTaskDetailById(element.id as string, {
gptPrompt: content
})
// 每次完成,都要向前端返回信息
SendReturnMessage(
{
code: 1,
id: element.id as string,
data: {
content: content,
progress: {
current: i,
total: bookTaskDetails.length
} as GeneralResponse.ProgressResponse
}
},
DEFINE_STRING.BOOK.ORIGINAL_GET_AI_PROMPT_RETURN
)
})
}
// 分批次执行异步任务
await ExecuteConcurrently(tasks, global.am.isPro ? (generalSetting.concurrency ?? 1) : 1)
// 执行完毕
return successMessage(null, '推理所有数据完成', 'BookPrompt_OriginalGetPrompt')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务详细数据失败,失败原因如下:${error.message}`,
'BookPromptHandle_OriginalGetAiPrompt'
)
}
}
AIStoryboardMerge = async (bookTaskId: string, type: BookTask.StoryboardMergeType) => {
try {
let res = await aiHandle.AIStoryboardMerge(bookTaskId, type)
return successMessage(res, 'AI分镜头合并成功', 'BookPromptHandle_AIStoryboardMerge')
} catch (error: any) {
return errorMessage(
`AI分镜头合并失败,失败原因如下:${error.message}`,
'BookPromptHandle_AIStoryboardMerge'
)
}
}
/**
*
*
* (MJ或SD)
*
* - MJ_MERGE: 使用MidJourney规则合并提示词
* - SD_MERGE: 使用Stable Diffusion规则合并提示词
*
* operateBookType参数决定
* - BOOKTASK: 处理整个小说任务中的所有分镜
* - BOOKTASKDETAIL: 仅处理单个指定分镜
* - UNDERBOOKTASK: 处理指定分镜及其后续所有分镜
*
* @param {string} id - operateBookType不同ID或分镜ID
* @param {PromptMergeType} type - 使
* @param {OperateBookType} operateBookType -
* @returns {Promise<SuccessItem | ErrorItem>}
*
* @throws {Error}
*
* @example
* // 使用MidJourney规则合并单个分镜的提示词
* const result = await bookPromptHandle.MergePrompt(
* "detail-123",
* PromptMergeType.MJ_MERGE,
* OperateBookType.BOOKTASKDETAIL
* );
*/
MergePrompt = async (
id: string,
type: PromptMergeType,
operateBookType: OperateBookType
): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> => {
try {
if (type == PromptMergeType.MJ_MERGE) {
return await this.mjServiceHandle.MergeMJPrompt(id, operateBookType)
} else if (type == PromptMergeType.SD_MERGE) {
return await this.sdServiceHandle.MergeSDPrompt(id, operateBookType)
} else {
throw new Error('未知的合并模式,请检查')
}
} catch (error: any) {
return errorMessage(
'合并提示词失败,错误信息如下:' + error.message,
'ReverseBook_MergePrompt'
)
}
}
/**
*
*
* 使AI技术分析书籍任务中的所有分镜文本
* AI分析
* autoAnalyzeCharacter字段中JSON格式存储
*
* @param {string} bookTaskId - ID
* @param {PresetCategory} type - PresetCategory.Character()PresetCategory.Scene()
*
* @returns {Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem>}
*
*
* @throws {Error}
*
* @example
* // 分析书籍任务中的角色
* const characterResult = await bookPromptHandle.AutoAnalyzeCharacterOrScene(
* "task-123",
* PresetCategory.Character
* );
*
* // 分析书籍任务中的场景
* const sceneResult = await bookPromptHandle.AutoAnalyzeCharacterOrScene(
* "task-123",
* PresetCategory.Scene
* );
*/
AutoAnalyzeCharacterOrScene = async (bookTaskId: string, type: PresetCategory) => {
try {
if (type != PresetCategory.Character && type != PresetCategory.Scene) {
throw new Error('分析的类型只能是角色或场景,请检查')
}
await this.InitBookBasicHandle()
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTaskId
})
if (bookTaskDetails.length <= 0) {
throw new Error('没有找到要分析的分镜数据,请先导入文案或者时srt!')
}
let words = bookTaskDetails
.map((item) => {
return item.afterGpt
})
.join('\r\n')
let systemContent = ''
let userContent = ''
if (type == PresetCategory.Character) {
systemContent = aiPrompts.NanFengCharacterSystemContent
userContent = aiPrompts.NanFengCharacterUserContent
} else if (type == PresetCategory.Scene) {
systemContent = aiPrompts.NanFengSceneSystemContent
userContent = aiPrompts.NanFengSceneUserContent
}
let message = [
{
role: 'system',
content: this.aiReasonCommon.replaceObject(systemContent, {
textContent: words
})
},
{
role: 'user',
content: this.aiReasonCommon.replaceObject(userContent, {
textContent: words
})
}
]
await this.aiReasonCommon.GetAISetting()
let content = await this.aiReasonCommon.FetchGpt(message)
let autoAnalyzeCharacter = bookTask.autoAnalyzeCharacter ?? '{}'
let autoAnalyzeCharacterData =
ValidateJsonAndParse<BookTask.BookTaskCharacterAndScene>(autoAnalyzeCharacter)
let returnData = content.split('\n').filter((item) => !isEmpty(item))
let newData: BookTask.BookTaskCharacterAndSceneObject[] = []
for (let i = 0; i < returnData.length; i++) {
const element = returnData[i]
let splitData = element.split('.')
if (splitData.length < 3) {
continue
}
let tempData = {
no: Number(splitData[0]),
id: crypto.randomUUID(),
name: splitData[1],
prompt: splitData[2]
} as BookTask.BookTaskCharacterAndSceneObject
newData.push(tempData)
}
if (type == PresetCategory.Character) {
autoAnalyzeCharacterData[PresetCategory.Character] = newData
} else if (type == PresetCategory.Scene) {
// 场景数据
autoAnalyzeCharacterData[PresetCategory.Scene] = newData
}
// 重新写入
await this.bookTaskService.ModifyBookTaskDataById(bookTaskId, {
autoAnalyzeCharacter: JSON.stringify(autoAnalyzeCharacterData)
})
return successMessage(
autoAnalyzeCharacterData,
'自动分析角色或场景成功',
'ReverseBook_AutoAnalyzeCharacterOrScene'
)
} catch (error: any) {
return errorMessage(
'自动分析角色或场景失败,错误信息如下:' + error.message,
'ReverseBook_AutoAnalyzeCharacterOrScene'
)
}
}
}
@@ -0,0 +1,218 @@
import { Book } from '@/define/model/book/book'
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
import { errorMessage, successMessage } from '@/public/generalTools'
import { BookBasicHandle } from './bookBasicHandle'
import { CheckFileOrDirExist, DeleteFolderAllFile } from '@/define/Tools/file'
import path from 'path'
import { isEmpty } from 'lodash'
/**
*
*
* BookService
*
* BookService
*
*
* @class
*/
export class BookServiceHandle extends BookBasicHandle {
constructor() {
super()
}
/**
*
*
* BookService AddOrModifyBook
*
* @param {Book.SelectBook} book -
* @returns {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async AddOrModifyBook(book: Book.SelectBook): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let res = await this.bookService.AddOrModifyBook(book)
return successMessage(res, '添加/修改小说信息成功!', 'BookServiceHandle_AddOrModifyBook')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`添加/修改小说信息失败,失败原因如下:${error.message}`,
'BookServiceHandle_AddOrModifyBook'
)
}
}
/**
*
*
* BookService GetBookDataCondition
*
* @param {Book.QueryBookCondition} queryCondition -
* @returns {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async GetBookDataCondition(
queryCondition: Book.QueryBookCondition
): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let res = await this.bookService.GetBookDataCondition(queryCondition)
return successMessage(res, '获取小说数据成功!', 'BookServiceHandle_GetBookDataCondition')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说数据失败,失败原因如下:${error.message}`,
'BookServiceHandle_GetBookDataCondition'
)
}
}
/**
* ID查询
*
* BookService GetBookDataById ID
*
* * @param {string} id - ID
* @returns {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async GetBookDataById(id: string): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let res = await this.bookService.GetBookDataById(id)
return successMessage(res, '获取小说数据成功!', 'BookServiceHandle_GetBookDataById')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说数据失败,失败原因如下:${error.message}`,
'BookServiceHandle_GetBookDataById'
)
}
}
/**
* ID修改
*
* BookService ModifyBookDataById ID和小说数据对象
*
* @param {string} id - ID
* @param {Book.SelectBook} bookData -
* @returns {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async ModifyBookDataById(
id: string,
bookData: Book.SelectBook
): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let res = await this.bookService.ModifyBookDataById(id, bookData)
return successMessage(res, '修改小说数据成功!', 'BookServiceHandle_ModifyBookDataById')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`修改小说数据失败,失败原因如下:${error.message}`,
'BookServiceHandle_ModifyBookDataById'
)
}
}
// 重置小说批次任务
async ResetBookDataInfo(bookId: string) {
try {
await this.InitBookBasicHandle()
let book = await this.bookService.GetBookDataById(bookId)
if (book == null) {
return errorMessage(
'未找到小说数据,重置小说数据失败!',
'BookServiceHandle_ResetBookDataInfo'
)
}
let bookDataPath = path.resolve(book.bookFolderPath as string, 'data')
let bookScriptPath = path.resolve(book.bookFolderPath as string, 'script')
let bookTmpPath = path.resolve(book.bookFolderPath as string, 'tmp')
// 开始执行重置操作,获取所有的批次任务,然后删除全部
let bookTasks = await this.bookTaskService.GetBookTaskDataByCondition({ bookId: book.id })
if (bookTasks.bookTasks.length > 0) {
// 有批次任务才删除
for (let i = 0; i < bookTasks.bookTasks.length; i++) {
const element = bookTasks.bookTasks[i]
this.bookTaskService.DeleteBookTaskDataById(element.id as string)
}
}
// 删除完成之后,删除文件夹
if (!isEmpty(bookDataPath) && (await CheckFileOrDirExist(bookDataPath))) {
await DeleteFolderAllFile(bookDataPath, false)
}
if (!isEmpty(bookScriptPath) && (await CheckFileOrDirExist(bookScriptPath))) {
await DeleteFolderAllFile(bookScriptPath, false)
}
if (!isEmpty(bookTmpPath) && (await CheckFileOrDirExist(bookTmpPath))) {
await DeleteFolderAllFile(bookTmpPath, false)
}
// 删除完毕 返回
return successMessage(null, '重置小说数据成功!', 'BookServiceHandle_ResetBookDataInfo')
} catch (error: any) {
return errorMessage(
`重置小说数据失败,失败原因如下:${error.message}`,
'BookServiceHandle_ResetBookDataInfo'
)
}
}
/**
* ID删除
*
* BookService DeleteBookDataById ID
*
* @param {string} id - ID
* @returns {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async DeleteBookDataInfoById(id: string): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
// 先调用重置方法 再调用删除
let resetRes = await this.ResetBookDataInfo(id)
if (resetRes.code != 1) {
return errorMessage(
`删除小说失败,失败原因如下:${resetRes.message}`,
'BookServiceHandle_DeleteBookDataInfoById'
)
}
let book = await this.bookService.GetBookDataById(id)
if (book == null) {
return errorMessage(
'未找到小说数据,删除小说失败!',
'BookServiceHandle_DeleteBookDataInfoById'
)
}
let projectPath = book.bookFolderPath as string
this.bookService.DeleteBookDataById(id)
// 删除成功 删除对应的文件
if (!isEmpty(projectPath) && (await CheckFileOrDirExist(projectPath))) {
await DeleteFolderAllFile(projectPath, true)
}
return successMessage(null, '删除小说数据成功!', 'BookServiceHandle_DeleteBookDataById')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`删除小说数据失败,失败原因如下:${error.message}`,
'BookServiceHandle_DeleteBookDataById'
)
}
}
}
@@ -0,0 +1,248 @@
import { BookTaskDetailService } from '@/define/db/service/book/bookTaskDetailService'
import { BookTaskStatus, OperateBookType } from '@/define/enum/bookEnum'
import { Book } from '@/define/model/book/book'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import { errorMessage, successMessage } from '@/public/generalTools'
import { BookTaskServiceHandle } from './bookTaskServiceHandle'
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
export class BookTaskDetailServiceHandle {
private bookTaskDetailService!: BookTaskDetailService
private bookTaskServiceHandle: BookTaskServiceHandle
constructor() {
this.bookTaskServiceHandle = new BookTaskServiceHandle()
}
private async InitBookTaskDetailServiceHandle() {
// 如果 bookTaskDetailService 已经初始化,则直接返回
if (this.bookTaskDetailService) return
this.bookTaskDetailService = await BookTaskDetailService.getInstance()
}
/**
*
*
* BookTaskDetailService GetBookTaskDetailDataByCondition
*
* @param {Book.QueryBookTaskDetailCondition} bookTaskDetailCondition -
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async GetBookTaskDetailDataByCondition(
bookTaskDetailCondition: Book.QueryBookTaskDetailCondition
): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookTaskDetailServiceHandle()
let res =
await this.bookTaskDetailService.GetBookTaskDetailDataByCondition(bookTaskDetailCondition)
return successMessage(
res,
'获取小说子任务详细数据成功!',
'BookTaskDetailServiceHandle_GetBookTaskDetailData'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务详细数据失败,失败原因如下:${error.message}`,
'BookTaskDetailServiceHandle_GetBookTaskDetailData'
)
}
}
/**
* ID
*
* BookTaskDetailService GetBookTaskDetailDataById ID
*
* @param {string} id - ID
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async GetBookTaskDetailDataById(id: string): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookTaskDetailServiceHandle()
let res = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
return successMessage(
res,
'获取小说子任务详细数据成功!',
'BookTaskDetailServiceHandle_GetBookTaskDetailDataById'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务详细数据失败,失败原因如下:${error.message}`,
'BookTaskDetailServiceHandle_GetBookTaskDetailDataById'
)
}
}
/**
*
*
* BookTaskDetailService ModifyBookTaskDetailById ID
*
* @param {string} bookTaskDetailId - ID
* @param {Book.SelectBookTaskDetail} updateData -
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async ModifyBookTaskDetailById(
bookTaskDetailId: string,
updateData: Book.SelectBookTaskDetail
): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookTaskDetailServiceHandle()
let res = await this.bookTaskDetailService.ModifyBookTaskDetailById(
bookTaskDetailId,
updateData
)
return successMessage(
res,
'修改小说子任务详细数据成功!',
'BookTaskDetailServiceHandle_ModifyBookTaskDetailById'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`修改小说子任务详细数据失败,失败原因如下:${error.message}`,
'BookTaskDetailServiceHandle_ModifyBookTaskDetailById'
)
}
}
/**
*
*
* BookTaskDetailService SaveCopywritingInfo ID
*
* @param {string} bookTaskId - ID
* @param {BookTaskDetail.SaveCopywritingData[]} copywritingData -
* @param {Book.OperateBookType} operateBookType -
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async SaveCopywritingInfo(
bookTaskId: string,
copywritingData: BookTaskDetail.SaveCopywritingData[],
operateBookType: OperateBookType
): Promise<SuccessItem | ErrorItem> {
try {
if (operateBookType != OperateBookType.BOOKTASK) {
throw new Error('目前只支持对小说任务的文案保存')
}
await this.bookTaskServiceHandle.InitBookBasicHandle()
let bookTask =
await this.bookTaskServiceHandle.bookTaskService.GetBookTaskDataById(bookTaskId)
await this.InitBookTaskDetailServiceHandle()
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTaskId
})
// 判断有没有小说批次任务 没有的话全部新增,有的话判断是不是当前的分镜是不是比旧的多,比旧的多就新增,否则就修改,然后将多余的旧的删除
if (bookTaskDetails.length > copywritingData.length) {
// 旧的分镜比新的多,删除旧的分镜
// 先修改分镜数据
// 开始修改。修改使用事务吧
this.bookTaskDetailService.transaction(() => {
for (let i = 0; i < copywritingData.length; i++) {
const element = copywritingData[i]
let btd = this.bookTaskDetailService.realm.objectForPrimaryKey(
'BookTaskDetail',
bookTaskDetails[i].id
)
if (btd == null) {
throw new Error('未找到对应的分镜数据,请检查')
}
// 开始修改
btd.startTime = element.startTime
btd.endTime = element.endTime
btd.word = element.word
btd.afterGpt = element.afterGpt
btd.subValue = JSON.stringify(element.subValue)
btd.timeLimit = `${element.startTime} - ${element.endTime}`
}
// 删除多余的分镜数据
let ids: string[] = []
for (let i = copywritingData.length; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i]
ids.push(element.id as string)
}
// 删除数据
for (let i = 0; i < ids.length; i++) {
const element = ids[i]
let btd = this.bookTaskDetailService.realm.objectForPrimaryKey(
'BookTaskDetail',
element
)
if (btd == null) {
continue
}
// 删除数据
this.bookTaskDetailService.realm.delete(btd)
}
})
} else {
// 新的分镜比旧的多,新增新的分镜
// 先修改分镜数据
this.bookTaskDetailService.transaction(() => {
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = copywritingData[i]
let btd = this.bookTaskDetailService.realm.objectForPrimaryKey(
'BookTaskDetail',
bookTaskDetails[i].id
)
if (btd == null) {
throw new Error('未找到对应的分镜数据,请检查')
}
// 开始修改
btd.startTime = element.startTime
btd.endTime = element.endTime
btd.word = element.word
btd.afterGpt = element.afterGpt
btd.subValue = JSON.stringify(element.subValue)
btd.timeLimit = `${element.startTime} - ${element.endTime}`
}
})
// 添加新的分镜数据
for (let i = bookTaskDetails.length; i < copywritingData.length; i++) {
const element = copywritingData[i]
// 开始新增
this.bookTaskDetailService.AddBookTaskDetail({
bookTaskId: bookTaskId,
bookId: bookTask.bookId,
startTime: element.startTime,
endTime: element.endTime,
status: BookTaskStatus.WAIT,
word: element.word,
afterGpt: element.afterGpt,
subValue: JSON.stringify(element.subValue),
timeLimit: `${element.startTime} - ${element.endTime}`,
// 新增修脸跟随
adetailer: false
})
}
}
// 将新的数据返回
let newData = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTaskId
})
if (newData.length == 0) {
throw new Error('没有找到对应的分镜数据,请检查')
}
return successMessage(
newData,
'保存小说批次数据分镜信息成功!',
'BookTaskDetailServiceHandle_SaveCopywritingInfo'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`保存小说批次数据分镜信息失败,失败原因如下:${error.message}`,
'BookTaskDetailServiceHandle_SaveCopywritingInfo'
)
}
}
}
@@ -0,0 +1,820 @@
import { AddBookTaskCopyData, BookTaskStatus, BookType } from '@/define/enum/bookEnum'
import { Book } from '@/define/model/book/book'
import { BookTask } from '@/define/model/book/bookTask'
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
import {
CheckFileOrDirExist,
CheckFolderExistsOrCreate,
DeleteFolderAllFile
} from '@/define/Tools/file'
import { errorMessage, successMessage } from '@/public/generalTools'
import { cloneDeep, isEmpty } from 'lodash'
import { BookBasicHandle } from './bookBasicHandle'
import path from 'path'
import { getProjectPath } from '../../option/optionCommonService'
import { OptionKeyName } from '@/define/enum/option'
import { optionSerialization } from '../../option/optionSerialization'
import { SettingModal } from '@/define/model/setting'
import { ImageCategory } from '@/define/data/imageData'
import fs from 'fs'
import { ValidateJson } from '@/define/Tools/validate'
import { TimeStringToMilliseconds } from '@/define/Tools/time'
import { SrtHandle } from '../../common/srtHandle'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
export class BookTaskServiceHandle extends BookBasicHandle {
constructor() {
super()
}
/**
*
*
* BookTaskService GetBookTaskDataByCondition
*
* @param {Book.QueryBookTaskCondition} bookTaskCondition -
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async GetBookTaskDataByCondition(
bookTaskCondition: Book.QueryBookTaskCondition
): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let res = await this.bookTaskService.GetBookTaskDataByCondition(bookTaskCondition)
return successMessage(
res,
'获取小说子任务数据成功!',
'BookTaskServiceHandle_GetBookTaskData'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务数据失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_GetBookTaskData'
)
}
}
/**
* ID查询
*
* BookTaskService GetBookTaskDataById ID
*
* @param {string} id - ID
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async GetBookTaskDataById(id: string): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let res = await this.bookTaskService.GetBookTaskDataById(id)
return successMessage(
res,
'获取小说子任务数据成功!',
'BookTaskServiceHandle_GetBookTaskDataById'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务数据失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_GetBookTaskDataById'
)
}
}
/**
*
*
* BookTaskService UpdateBookTaskStatus ID和状态
*
* @param {string} bookTaskId - ID
* @param {BookTaskStatus} status -
* @param {string?} errorMsg -
* @return {Promise<void>} -
* @async
*/
async ModifyBookTaskStatus(
bookTaskId: string,
status: BookTaskStatus,
errorMsg?: string
): Promise<void> {
try {
await this.InitBookBasicHandle()
this.bookTaskService.ModifyBookTaskStatus(bookTaskId, status, errorMsg)
} catch (error) {
throw error
}
}
/**
* ID修改
*
* BookTaskService ModifyBookTaskDataById ID和数据
*
* @param {string} bookTaskId - ID
* @param {Book.SelectBookTask} data -
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async ModifyBookTaskDataById(
bookTaskId: string,
data: Book.SelectBookTask
): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let res = await this.bookTaskService.ModifyBookTaskDataById(bookTaskId, data)
return successMessage(
res,
'修改小说子任务数据成功!',
'BookTaskServiceHandle_ModifyBookTaskData'
)
} catch (error: any) {
return errorMessage(
`修改小说子任务数据失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_ModifyBookTaskData'
)
}
}
/**
*
*
* ID数组删除多个小说子任务
*
*
* @param {string[]} ids - ID数组
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async DeleteBookTaskByIds(ids: string[]): Promise<SuccessItem | ErrorItem> {
try {
let bookTaskIds = ids.map((id) => id.trim())
let bookTasks: Book.SelectBookTask[] = []
await this.InitBookBasicHandle()
for (let i = 0; i < bookTaskIds.length; i++) {
const element = bookTaskIds[i]
let tempBookTask = await this.bookTaskService.GetBookTaskDataById(element)
bookTasks.push(tempBookTask)
}
if (bookTasks.length === 0) {
return errorMessage(
'没有找到要删除的小说子任务数据',
'BookTaskServiceHandle_DeleteBookTask'
)
}
// 开始执行删除
for (let i = 0; i < bookTasks.length; i++) {
const element = bookTasks[i]
let imageFolder = element.imageFolder
// 先删除每个批次对应的数据,然后删除批次
this.bookTaskService.DeleteBookTaskDataById(element.id as string)
// 删除成功,直接把对应的出图文件夹删掉
if (await CheckFileOrDirExist(imageFolder)) {
await DeleteFolderAllFile(imageFolder as string, true)
}
}
// 删除完成 然后将当前小说得所有得子任务返回
let res = await this.bookTaskService.GetBookTaskDataByCondition({
bookId: bookTasks[0].bookId
})
let newBookTasks = res.bookTasks
return successMessage(
{
bookId: bookTasks[0].bookId,
bookTasks: newBookTasks
},
'删除小说子任务数据成功!',
'BookTaskServiceHandle_DeleteBookTask'
)
} catch (error: any) {
return errorMessage(
`删除小说子任务数据失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_DeleteBookTask'
)
}
}
/**
*
* @param bookTask
* @param addNewBookTask
* @param no
* @returns
*/
async CopyBookTaskBaseData(
bookTask: Book.SelectBookTask,
addNewBookTask: Book.AddBookTask,
no: number,
projectPath: string
): Promise<Book.SelectBookTask> {
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
throw new Error('没有找到小说,请检查')
}
let name = book.name + '_' + no.toString().padStart(5, '0')
let imageFolder = path.join(projectPath, `${bookTask.bookId}/tmp/${name}`)
let generalSettingOptions = this.optionRealmService.GetOptionByKey(
OptionKeyName.Software.GeneralSetting
)
let generalSetting = optionSerialization<SettingModal.GeneralSettings>(generalSettingOptions)
let imageCategory = generalSetting.defaultImgGenMethod ?? ImageCategory.Midjourney
if (!isEmpty(bookTask.imageCategory)) {
imageCategory = bookTask.imageCategory
} else {
if (book.type == BookType.MJ_REVERSE) {
imageCategory = ImageCategory.Midjourney
} else if (book.type == BookType.SD_REVERSE) {
imageCategory = ImageCategory.Stable_Diffusion
} else if (book.type == BookType.ORIGINAL) {
imageCategory = generalSetting.defaultImgGenMethod ?? ImageCategory.Midjourney
}
}
let newBookTask = {
id: crypto.randomUUID(),
bookId: bookTask.bookId,
no: no,
name: name,
generateVideoPath: undefined,
srtPath: bookTask.srtPath,
audioPath: bookTask.audioPath,
imageFolder: imageFolder ? path.relative(projectPath, imageFolder) : undefined,
status: BookTaskStatus.WAIT,
errorMsg: undefined,
updateTime: new Date(),
createTime: new Date(),
isAuto: false,
autoAnalyzeCharacter: undefined,
imageStyle: [],
customizeImageStyle: [],
videoConfig: (bookTask.videoConfig ??= undefined),
prefixPrompt: (addNewBookTask.prefixPrompt ??= undefined),
suffixPrompt: addNewBookTask.suffixPrompt ?? undefined,
imageCategory: imageCategory,
subImageFolder: [],
draftSrtStyle: undefined,
backgroundMusic: (bookTask.backgroundMusic ??= undefined),
friendlyReminder: (bookTask.friendlyReminder ??= undefined)
} as Book.SelectBookTask
if (
addNewBookTask.selectTaskDataCategory &&
addNewBookTask.selectTaskDataCategory.includes(AddBookTaskCopyData.IMAGE_STYLE)
) {
newBookTask.imageStyle = bookTask.imageStyle ??= []
newBookTask.customizeImageStyle = bookTask.customizeImageStyle ??= []
}
return newBookTask
}
/**
*
* @param oldBookTaskDetail
*/
async CopyBookTaskDetailBaseData(
bookTask: Book.SelectBookTask,
newBookTask: Book.SelectBookTask,
oldBookTaskDetail: Book.SelectBookTaskDetail[],
addNewBookTask: Book.AddBookTask,
projectPath: string
): Promise<Book.SelectBookTaskDetail[]> {
let bookTaskDetail = [] as Book.SelectBookTaskDetail[]
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
throw new Error('没有找到小说,请检查')
}
let originalTimePath = path.join(book.bookFolderPath as string, `data/${book.id}.mp4.json`)
let originalTime = [] as any[]
if (await CheckFileOrDirExist(originalTimePath)) {
let originalTimeString = await fs.promises.readFile(originalTimePath, 'utf-8')
if (ValidateJson(originalTimeString)) {
originalTime = JSON.parse(originalTimeString)
}
}
// 判断分镜数据和批次数据是不是相同的
if (originalTime.length != oldBookTaskDetail.length) {
originalTime = []
}
for (let i = 0; i < oldBookTaskDetail.length; i++) {
const element = oldBookTaskDetail[i]
let addOneBookTaskDetail = {
id: crypto.randomUUID(),
no: element.no,
name: element.name,
bookId: element.bookId,
bookTaskId: newBookTask.id,
videoPath: element.videoPath ? path.relative(projectPath, element.videoPath) : undefined,
oldImage: element.oldImage ? path.relative(projectPath, element.oldImage) : undefined,
adetailer: element.adetailer == undefined || element.adetailer == null ? false : true,
sdConifg: element.sdConifg,
createTime: new Date(),
updateTime: new Date(),
audioPath: element.audioPath,
subtitlePosition: element.subtitlePosition,
status: BookTaskStatus.WAIT,
imageLock: false
} as Book.SelectBookTaskDetail
// 开始添加数据
if (
bookTask &&
addNewBookTask.selectTaskDataCategory &&
addNewBookTask.selectTaskDataCategory.length > 0
) {
// 有选择的数据,这边要调用各种方法
if (addNewBookTask.selectTaskDataCategory.includes(AddBookTaskCopyData.AFTER_GPT)) {
// 是不是要复制文案
addOneBookTaskDetail.word = element.word
addOneBookTaskDetail.afterGpt = element.afterGpt
addOneBookTaskDetail.subValue = element.subValue
? JSON.stringify(element.subValue)
: undefined
addOneBookTaskDetail.oldImage = element.oldImage
addOneBookTaskDetail.startTime = element.startTime
addOneBookTaskDetail.endTime = element.endTime
addOneBookTaskDetail.timeLimit = element.timeLimit
} else {
// 初始化时间信息
if (originalTime[i]) {
addOneBookTaskDetail.startTime = TimeStringToMilliseconds(originalTime[i][0])
addOneBookTaskDetail.endTime = TimeStringToMilliseconds(originalTime[i][1])
addOneBookTaskDetail.timeLimit = undefined
}
}
if (addNewBookTask.selectTaskDataCategory.includes(AddBookTaskCopyData.GPT_PROMPT)) {
// 是不是要复制GPT提示词
addOneBookTaskDetail.gptPrompt = element.gptPrompt
// 复制反推提示词
// 处理反推数据
let reverseMessage = [] as Book.ReversePrompt[]
if (element.reversePrompt && element.reversePrompt.length > 0) {
reverseMessage = cloneDeep(element.reversePrompt)
for (let k = 0; k < reverseMessage.length; k++) {
reverseMessage[k].id = crypto.randomUUID()
reverseMessage[k].bookTaskDetailId = element.id
}
}
addOneBookTaskDetail.reversePrompt = reverseMessage
}
if (addNewBookTask.selectTaskDataCategory.includes(AddBookTaskCopyData.CHARACTER)) {
// 是不是要复制角色
addOneBookTaskDetail.characterTags = element.characterTags
}
if (addNewBookTask.selectTaskDataCategory.includes(AddBookTaskCopyData.IMAGE_STYLE)) {
// 是不是要复制生图风格
addOneBookTaskDetail.styleTags = element.styleTags
}
if (addNewBookTask.selectTaskDataCategory.includes(AddBookTaskCopyData.PROMPT)) {
// 是不是要复制提示
addOneBookTaskDetail.prompt = element.prompt
}
}
bookTaskDetail.push(addOneBookTaskDetail)
}
return bookTaskDetail
}
/**
*
* @param bookTask
* @param bookTaskDetails
* @param addNewBookTask
* @returns
*/
async AddOneBookTask(
bookTask: Book.SelectBookTask,
bookTaskDetails: Book.SelectBookTaskDetail[],
addNewBookTask: Book.AddBookTask,
no: number,
projectPath: string
): Promise<{
newBookTask: Book.SelectBookTask
newBookTaskDetails: Book.SelectBookTaskDetail[]
}> {
let newBookTask = await this.CopyBookTaskBaseData(bookTask, addNewBookTask, no, projectPath)
let newBookTaskDetails = await this.CopyBookTaskDetailBaseData(
bookTask,
newBookTask,
bookTaskDetails,
addNewBookTask,
projectPath
)
return {
newBookTask: newBookTask,
newBookTaskDetails: newBookTaskDetails
}
}
/**
*
* @param addData
* @returns
*/
async AddNewBookTask(addData: BookTask.AddBookTaskParam): Promise<SuccessItem | ErrorItem> {
try {
if (!global.am.isPro && addData.count > 1) {
return errorMessage(
'批量添加小说子任务数据失败,免费版只能添加一条数据',
'BookTaskServiceHandle_AddNewBookTask'
)
}
if (isEmpty(addData.selectBookId)) {
return errorMessage(
'批量添加小说子任务数据失败,小说ID不能为空',
'BookTaskServiceHandle_AddNewBookTask'
)
}
let bookTasks: Book.SelectBookTask[] = []
let bookTaskDetail: Book.SelectBookTaskDetail[] = []
let projectPath = await getProjectPath()
let maxNo = await this.bookTaskService.GetMaxBookTaskNo(addData.selectBookId)
for (let i = 0; i < addData.count; i++) {
if (addData.copyBookTask && !isEmpty(addData.selectBookTask)) {
let bookTask = await this.bookTaskService.GetBookTaskDataById(
addData.selectBookTask as string
)
let oldBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition(
{
bookTaskId: addData.selectBookTask
}
)
let { newBookTask, newBookTaskDetails } = await this.AddOneBookTask(
bookTask,
oldBookTaskDetail,
addData,
maxNo + i,
projectPath
)
bookTasks.push(newBookTask)
bookTaskDetail.push(...newBookTaskDetails)
} else {
let newBookTask = await this.CopyBookTaskBaseData(
{ bookId: addData.selectBookId },
addData,
maxNo + i,
projectPath
)
bookTasks.push(newBookTask)
}
}
// 先要创建文件夹
for (let i = 0; i < bookTasks.length; i++) {
let imageFolder = path.join(projectPath, bookTasks[i].imageFolder as string)
await CheckFolderExistsOrCreate(imageFolder)
}
// 这边开始添加数据
await this.transaction((realm) => {
try {
for (let i = 0; i < bookTasks.length; i++) {
const element = bookTasks[i]
element.openVideoGenerate = false
realm.create('BookTask', element)
}
for (let i = 0; i < bookTaskDetail.length; i++) {
const element = bookTaskDetail[i]
element.adetailer = false
realm.create('BookTaskDetail', element)
}
} catch (error) {
throw error
}
})
return successMessage(
{
bookId: addData.selectBookId,
bookTasks: bookTasks
},
'添加小说子任务数据成功!',
'BookTaskServiceHandle_AddNewBookTask'
)
} catch (error: any) {
return errorMessage(
`添加小说子任务数据失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_AddNewBookTask'
)
}
}
/**
*
*
* ID获取该小说下所有批次任务的图片和视频生成进度信息
*
*
* @param {string} bookId - ID
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async GetBookTaskImageGenerateProgress(bookId: string): Promise<SuccessItem | ErrorItem> {
try {
let book = await this.bookService.GetBookDataById(bookId)
if (book == null) {
return errorMessage(
'获取小说任务生成进度失败,小说不存在',
'BookTaskServiceHandle_GetBookTaskImageGenerateProgress'
)
}
// 获取小说批次数据
let bookTasks = await this.bookTaskService.GetBookTaskDataByCondition({
bookId: bookId
})
if (bookTasks.bookTasks.length === 0) {
return successMessage(
{},
'获取小说任务生成进度成功!',
'BookTaskServiceHandle_GetBookTaskImageGenerateProgress'
)
}
// 这边开始处理数据
let resData: BookTask.BookTaskProgressRecord = {}
// 获取对应的批次信息 然后去判断图片和总的进度
for (let i = 0; i < bookTasks.bookTasks.length; i++) {
const element = bookTasks.bookTasks[i]
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: element.id,
bookId: bookId
})
if (bookTaskDetails.length <= 0) {
// 没有小说批次的详细任务
resData[element.id as string] = {
imageProgress: 0,
videoProgress: 0,
totalCount: 0,
imageRate: 0,
videoRate: 0
}
continue
}
// 遍历 bookTaskDetails 获取每个小说批次任务的进度数据
let imageProgress = 0
let videoProgress = 0
for (let j = 0; j < bookTaskDetails.length; j++) {
const bookTaskDetail = bookTaskDetails[j] as Book.SelectBookTaskDetail
// 检查图片信息
if (
!isEmpty(bookTaskDetail.outImagePath) &&
(await CheckFileOrDirExist(bookTaskDetail.outImagePath))
) {
imageProgress += 1
}
// 检查视频信息
if (
!isEmpty(bookTaskDetail.generateVideoPath) &&
(await CheckFileOrDirExist(bookTaskDetail.generateVideoPath))
) {
videoProgress += 1
}
}
// 开始添加数据
resData[element.id as string] = {
imageProgress: imageProgress,
videoProgress: videoProgress,
totalCount: bookTaskDetails.length,
imageRate:
bookTaskDetails.length > 0 ? (imageProgress / bookTaskDetails.length) * 100 : 0,
videoRate: bookTaskDetails.length > 0 ? (videoProgress / bookTaskDetails.length) * 100 : 0
}
}
return successMessage(
resData,
'获取小说任务生成进度成功!',
'BookTaskServiceHandle_GetBookTaskImageGenerateProgress'
)
} catch (error: any) {
return errorMessage(
`获取小说任务生成进度失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_GetBookTaskImageGenerateProgress'
)
}
}
/**
*
*
*
*
*
* @param {string} bookId - ID
* @returns {Promise<SuccessItem | ErrorItem>} ID和对应第一张图片路径的映射对象
* - data Record<string, string | undefined> ID
* -
* @example
* // 返回数据格式示例
* {
* "batch-id-1": "/path/to/first/image1.jpg",
* "batch-id-2": "/path/to/first/image2.png",
* "batch-id-3": undefined // 该批次暂无有效图片
* }
* @async
*/
async GetBookTaskFirstImagePath(bookId: string): Promise<SuccessItem | ErrorItem> {
try {
let bookTasks = await this.bookTaskService.GetBookTaskDataByCondition({
bookId: bookId
})
if (bookTasks.bookTasks.length === 0) {
return errorMessage(
'获取小说批次任务的第一张图片路径失败,小说批次任务不存在',
'BookTaskServiceHandle_GetBookTaskFirstImagePath'
)
}
let resData: Record<string, string | undefined> = {}
// 开始处理所有的批次
for (let i = 0; i < bookTasks.bookTasks.length; i++) {
const element = bookTasks.bookTasks[i]
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: element.id,
bookId: bookId
})
if (bookTaskDetails.length <= 0) {
resData[element.id as string] = undefined
continue
}
for (let j = 0; j < bookTaskDetails.length; j++) {
const bookTaskDetail = bookTaskDetails[j]
if (isEmpty(bookTaskDetail.outImagePath)) {
resData[element.id as string] = undefined
continue
}
if (await CheckFileOrDirExist(bookTaskDetail.outImagePath)) {
resData[element.id as string] = bookTaskDetail.outImagePath as string
break
}
}
}
return successMessage(
resData,
'获取小说批次任务的第一张图片路径成功!',
'BookTaskServiceHandle_GetBookTaskFirstImagePath'
)
} catch (error: any) {
return errorMessage(
`获取小说批次任务的第一张图片路径失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_GetBookTaskFirstImagePath'
)
}
}
/**
*
*
* BookTaskService AddBookTask
*
* @param {Book.SelectBookTask} bookTask -
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async AddBookTask(bookTask: Book.SelectBookTask): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let res = await this.bookTaskService.AddBookTask(bookTask)
return successMessage(res, '添加小说子任务数据成功!', 'BookTaskServiceHandle_AddBookTask')
} catch (error: any) {
return errorMessage(
`添加小说子任务数据失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_AddBookTask'
)
}
}
/**
*
*
* BookTaskService ResetBookTaskDataById ID
*
* @param {string} bookTaskId - ID
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async ResetBookTaskDataById(bookTaskId: string): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let res = await this.bookTaskService.ResetBookTaskDataById(bookTaskId)
return successMessage(
res,
'重置小说子任务数据成功!',
'BookTaskServiceHandle_ResetBookTaskData'
)
} catch (error: any) {
return errorMessage(
`重置小说子任务数据失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_ResetBookTaskData'
)
}
}
async ResetAndInitializeBookTask(bookTaskId: string) {
try {
// 先调用重置
await this.InitBookBasicHandle()
let newBookTask = await this.bookTaskService.ResetBookTaskDataById(bookTaskId, false)
// 加载 SRT 数据
const srtHandle = new SrtHandle()
let srtData = await srtHandle.GetSrtDataByPath(newBookTask.srtPath as string)
// 循环添加小说详细信息
for (let i = 0; i < srtData.length; i++) {
const element = srtData[i]
this.bookTaskDetailService.AddBookTaskDetail({
bookTaskId: newBookTask.id,
bookId: newBookTask.bookId,
startTime: element.start,
endTime: element.end,
status: BookTaskStatus.WAIT,
word: element.text,
afterGpt: element.text,
subValue: JSON.stringify([
{
id: crypto.randomUUID(),
end_time: element.end,
start_time: element.start,
srt_value: element.text
}
] as BookTaskDetail.CopywritingSubValue[]),
timeLimit: `${element.start} -- ${element.end}`,
// 新增修脸跟随
adetailer: false // 默认false,实际更具SD设置中为主
})
}
// 添加成功 重新获取最新数据
let newBookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookId: newBookTask.bookId,
bookTaskId: newBookTask.id
})
if (newBookTaskDetails == null || newBookTaskDetails.length <= 0) {
return errorMessage(
'重置小说分镜数据(不包含bootask本身数据),并且初始化全新的分镜信息失败,小说分镜数据不存在',
'BookTaskServiceHandle_ResetAndInitializeBookTask'
)
}
return successMessage(
newBookTaskDetails,
'重置小说分镜数据成功!',
'BookTaskServiceHandle_ResetAndInitializeBookTask'
)
} catch (error: any) {
return errorMessage(
`重置小说分镜数据(不包含bootask本身数据),并且初始化全新的分镜信息失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_ResetAndInitializeBookTask'
)
}
}
/**
*
*
* BookTaskService DeleteBookTaskDataById ID
*
* @param {string} bookTaskId - ID
* @return {Promise<SuccessItem | ErrorItem>} -
* @async
*/
async DeleteBookTaskDataById(bookTaskId: string): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookBasicHandle()
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
if (bookTask == null) {
return errorMessage(
'重置小说子任务数据失败,小说子任务不存在',
'BookTaskServiceHandle_ResetBookTaskDataById'
)
}
let imageFolder = bookTask.imageFolder
let res = this.bookTaskService.DeleteBookTaskDataById(bookTaskId)
// 删除成功 将对应的生图文件夹删除
if (!isEmpty(imageFolder) && (await CheckFileOrDirExist(imageFolder))) {
await DeleteFolderAllFile(imageFolder as string, true)
}
return successMessage(res, '删除小说子任务数据成功!', 'BookTaskServiceHandle_DeleteBookTask')
} catch (error: any) {
return errorMessage(
`删除小说子任务数据失败,失败原因如下:${error.message}`,
'BookTaskServiceHandle_DeleteBookTask'
)
}
}
}
@@ -0,0 +1,350 @@
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
import { BookBasicHandle } from './bookBasicHandle'
import { Book } from '@/define/model/book/book'
import path from 'path'
import compressing from 'compressing'
import fs from 'fs'
import {
CheckFileOrDirExist,
CheckFolderExistsOrCreate,
CopyFileOrFolder,
GetFilesWithExtensions
} from '@/define/Tools/file'
import { OptionKeyName } from '@/define/enum/option'
import { optionSerialization } from '../../option/optionSerialization'
import { SettingModal } from '@/define/model/setting'
import { errorMessage, successMessage } from '@/public/generalTools'
import { isEmpty } from 'lodash'
import { define } from '@/define/define'
import util from 'util'
import { exec } from 'child_process'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import { ValidateJson } from '@/define/Tools/validate'
const execAsync = util.promisify(exec)
import JianyingService from '../../jianying/jianyingService'
export class BookVideoHandle extends BookBasicHandle {
jianyingService: JianyingService
constructor() {
super()
this.jianyingService = new JianyingService()
}
/**
* 稿
* @description
* @param book
* @param bookTask
* @returns Promise<{draftName: string, configJsonPath: string}> 稿
* @throws {Error}
* @example
* ```typescript
* const result = await this.GenerateConfigFile(bookInfo, taskInfo);
* console.log('草稿名称:', result.draftName);
* console.log('配置文件路径:', result.configJsonPath);
* ```
*/
private async GenerateConfigFile(
book: Book.SelectBook,
bookTask: Book.SelectBookTask
): Promise<{
draftName: string
configJsonPath: string
}> {
try {
// 构建配置文件保存路径,格式: 小说文件夹/scripts/任务名_config.json
let configPath = path.join(
book.bookFolderPath as string,
`scripts/${bookTask.name}_config.json`
)
// 确保目录存在,不存在则创建
await CheckFolderExistsOrCreate(path.dirname(configPath))
// 获取任务详细信息(包含所有帧的字幕、图片、时间信息等)
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTask.id
})
// ========== 处理背景音乐配置 ==========
let musicPath: string | undefined = undefined
if (!isEmpty(bookTask.backgroundMusic) && bookTask.backgroundMusic != null) {
// 检查背景音乐文件或文件夹是否存在
if (!CheckFileOrDirExist(bookTask.backgroundMusic)) {
throw new Error('背景音乐文件夹或文件不存在,请检查')
}
// 判断背景音乐是文件还是文件夹
let isFolder = await fs.promises
.stat(bookTask.backgroundMusic as string)
.then((stat) => stat.isDirectory())
.catch(() => false)
if (!isFolder) {
// 如果是文件,直接使用该文件路径
musicPath = bookTask.backgroundMusic
} else {
// 如果是文件夹,从中随机选择一个音频文件
let files = await GetFilesWithExtensions(bookTask.backgroundMusic, ['.mp3', '.wav'])
if (files.length <= 0) {
throw new Error('背景音乐文件夹下面未存在有效的音频文件')
} else {
const randomIndex = Math.floor(Math.random() * files.length)
musicPath = files[randomIndex]
}
}
}
// ========== 获取用户配置设置 ==========
// 获取剪映关键帧动画设置(上下移动、左右移动、缩放等动画参数)
let keyFrameSettingOption = this.optionRealmService.GetOptionByKey(
OptionKeyName.Software.JianyingKeyFrameSetting
)
let keyFrameSetting =
optionSerialization<SettingModal.JianyingKeyFrameSettings>(keyFrameSettingOption)
// 获取通用设置(包含剪映草稿文件夹路径等)
let generalSettingOption = this.optionRealmService.GetOptionByKey(
OptionKeyName.Software.GeneralSetting
)
let generalSetting = optionSerialization<SettingModal.GeneralSettings>(generalSettingOption)
// ========== 准备剪映草稿文件 ==========
let draft_name = bookTask.name as string
let draft_path = path.join(generalSetting.draftPath, draft_name)
// 清理已存在的草稿文件夹
await fs.promises.rm(draft_path, { recursive: true, force: true })
// 从模板压缩包中解压出草稿文件夹
await compressing.zip.uncompress(define.draft_temp_path, draft_path)
let draftPath = path.join(draft_path, 'draft_content.json')
// ========== 构建配置数据对象 ==========
let configData = {
srt_time_information: [] as any[], // 存储所有帧的字幕时间信息
video_config: {
srt_path: bookTask.srtPath, // 字幕文件路径
audio_path: bookTask.audioPath, // 音频文件路径
draft_srt_style: bookTask.draftSrtStyle ? bookTask.draftSrtStyle : '0', // 字幕样式
background_music: musicPath, // 背景音乐路径
friendly_reminder: bookTask.friendlyReminder ? bookTask.friendlyReminder : '0', // 友好提醒设置
draft_content_json_path: draftPath, // 剪映草稿内容文件路径
key_frame_info: {
key_frame: keyFrameSetting.keyFrame, // 关键帧类型
isFixedSpeed: keyFrameSetting.isFixedSpeed, // 是否固定速度
key_frame_time: keyFrameSetting.keyFrameTime, // 关键帧时间
// 上下移动动画配置
up_down_key_frame: {
default_scale: keyFrameSetting.upDownKeyFrame.defaultScale,
start_position: keyFrameSetting.upDownKeyFrame.startPosition,
end_position: keyFrameSetting.upDownKeyFrame.endPosition
},
// 左右移动动画配置
left_right_key_frame: {
default_scale: keyFrameSetting.leftRightKeyFrame.defaultScale,
start_position: keyFrameSetting.leftRightKeyFrame.startPosition,
end_position: keyFrameSetting.leftRightKeyFrame.endPosition
},
// 缩放动画配置
scale_key_frame: {
default_scale: keyFrameSetting.scaleKeyFrame.defaultScale,
start_position: keyFrameSetting.scaleKeyFrame.startPosition,
end_position: keyFrameSetting.scaleKeyFrame.endPosition
},
is_fixed_speed: keyFrameSetting.isFixedSpeed ? keyFrameSetting.isFixedSpeed : false
}
}
}
// 判断是否开启视频生成功能
let openVideo = bookTask.openVideoGenerate ?? false
// ========== 处理每一帧的详细信息 ==========
for (let i = 0; i < bookTaskDetail.length; i++) {
const element = bookTaskDetail[i]
// 验证时间信息完整性
if (element.startTime == null || element.endTime == null) {
throw new Error('字幕时间信息不完整')
}
// 验证字幕内容完整性
if (element.subValue == null) {
throw new Error('字幕内容信息不完整')
}
// 处理字幕内容:如果是字符串格式的JSON,则解析为对象
if (typeof element.subValue === 'string') {
if (ValidateJson(element.subValue)) {
element.subValue = JSON.parse(element.subValue)
} else {
throw new Error('字幕内容信息不完整')
}
}
// 时间单位转换:从毫秒转换为秒
element.startTime = (element.startTime as number) / 1000
element.endTime = (element.endTime as number) / 1000
// 处理子字幕数组,同样进行时间单位转换
element.subValue = Array.isArray(element.subValue)
? (element.subValue as BookTaskDetail.CopywritingSubValue[]).map((sub) => {
sub.start_time = sub.start_time / 1000
sub.end_time = sub.end_time / 1000
return sub
})
: []
// 构建单帧数据对象
let frameData = {
no: element.no, // 帧序号
id: element.id, // 帧ID
lastId: i == 0 ? '' : bookTaskDetail[i - 1].id, // 上一帧ID
word: element.word, // 文字内容
old_image: element.oldImage, // 原始图片
after_gpt: element.afterGpt, // GPT处理后的内容
start_time: element.startTime, // 开始时间(秒)
end_time: element.endTime, // 结束时间(秒)
timeLimit: `${element.startTime} -- ${element.endTime}`, // 时间范围字符串
subValue: element.subValue, // 子字幕数组
character_tags: [], // 角色标签(预留)
gpt_prompt: element.gptPrompt, // GPT提示词
mjMessage: element.mjMessage, // MJ消息
prompt_json: '', // 提示词JSON(预留)
name: element.name + '.png', // 图片文件名
outImagePath: element.outImagePath, // 输出图片路径
generateVideoPath: openVideo ? element.generateVideoPath : null, // 生成视频路径(可选)
subImagePath: element.subImagePath, // 子图片路径
scene_tags: [], // 场景标签(预留)
imageLock: element.imageLock, // 图片锁定状态
prompt: element.prompt // 提示词
}
configData.srt_time_information.push(frameData as any)
}
// ========== 保存配置文件 ==========
// 将配置数据写入到指定路径的JSON文件中
await fs.promises.writeFile(configPath, JSON.stringify(configData), 'utf-8')
// 同时复制一份到通用的config.json文件中,供其他程序使用
let configJsonPath = path.join(book.bookFolderPath as string, 'scripts/config.json')
await CopyFileOrFolder(configPath, configJsonPath)
return {
draftName: draft_name,
configJsonPath: configJsonPath
}
} catch (error) {
throw error
}
}
/**
* 稿
* @param id
* @param operateBookType
* @returns
*/
async AddJianyingDraft(bookTaskId: string): Promise<ErrorItem | SuccessItem> {
try {
await this.InitBookBasicHandle()
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
if (bookTask == null) {
return errorMessage('没有找到小说批次任务,请检查', 'BookVideoHandle_AddJianyingDraft')
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
return errorMessage('没有找到小说,请检查', 'BookVideoHandle_AddJianyingDraft')
}
if (!isEmpty(bookTask.draftDepend) && bookTask.draftDepend != null) {
if (bookTask.name == bookTask.draftDepend) {
throw new Error('草稿名称不能和任务名称相同,请修改任务名称')
}
await this.jianyingService.GenerateDraftFromDepend(
bookTask.draftDepend as string,
bookTask.imageFolder as string,
bookTask.name as string
)
return successMessage(bookTask.name, '添加剪映草稿成功!', 'BookVideoHandle_AddJianyingDraft')
}
let { draftName, configJsonPath } = await this.GenerateConfigFile(book, bookTask)
// 开始调用 exe 执行 草稿的导出
let jianyingExePath = path.join(define.scripts_path, 'xiangbei_jianying_main.exe')
if (!CheckFileOrDirExist(jianyingExePath)) {
throw new Error('没有找到导出剪映的执行文件,请检查')
}
let result: string = ''
try {
const env = {
...process.env,
PYTHONIOENCODING: 'utf-8',
PYTHONLEGACYWINDOWSSTDIO: 'utf-8',
LANG: 'zh_CN.UTF-8',
PYTHONUTF8: '1'
}
// 尝试最简单的执行方式,不使用chcp
const simpleCommand = `"${jianyingExePath}" "${configJsonPath}"`
const output = await execAsync(simpleCommand, {
maxBuffer: 1024 * 1024 * 10,
encoding: 'utf-8',
env: env,
cwd: path.dirname(jianyingExePath),
timeout: 300000
})
if (
output.stderr &&
(output.stderr.includes('Error') ||
output.stderr.includes('failed') ||
output.stderr.includes('UnicodeEncodeError'))
) {
throw new Error(output.stderr)
}
// 导出成功
let stdout = output.stdout
// 将导出的日志写道文件里面
let exportLogPath = path.join(
book.bookFolderPath as string,
`scripts/JianYingExportLog/${draftName}_export_log_${new Date().getTime()}.txt`
)
await CheckFolderExistsOrCreate(path.dirname(exportLogPath))
await fs.promises.writeFile(exportLogPath, stdout, 'utf-8')
// 导出成功 将草稿名字返回
result = draftName
} catch (fallbackError: any) {
// 记录详细的错误信息到文件
const errorLogPath = path.join(
book.bookFolderPath as string,
`scripts/JianYingExportLog/error_${draftName}_${new Date().getTime()}.txt`
)
await CheckFolderExistsOrCreate(path.dirname(errorLogPath))
const errorInfo = {
execAsyncError: fallbackError.message,
scriptPath: jianyingExePath,
configPath: configJsonPath,
timestamp: new Date().toISOString()
}
await fs.promises.writeFile(errorLogPath, JSON.stringify(errorInfo, null, 2), 'utf-8')
throw new Error(`详细错误已记录到: ${errorLogPath}`)
}
// 所有的草稿都添加完毕之后开始返回
return successMessage(result, `添加剪映草稿成功!`, 'BookVideoHandle_AddJianyingDraft')
} catch (error: any) {
return errorMessage(
'添加剪映草稿失败,错误信息如下:' + error.toString(),
'BookVideoHandle_AddJianyingDraft'
)
}
}
}

Some files were not shown because too many files have changed in this diff Show More