v 3.4.5 迁移图转视频 视频多语系 英文和中文

This commit is contained in:
lq1405 2025-09-12 14:52:28 +08:00
parent 2182c1a36e
commit 8bc60256ba
221 changed files with 17967 additions and 3862 deletions

2
.gitignore vendored
View File

@ -1,5 +1,6 @@
node_modules
dist
.idea
out
.DS_Store
.eslintcache
@ -8,3 +9,4 @@ resources/logger
resources/project
Database
build
src/renderer/src/components/Original/MainHome/OriginalTaskCard.vue

View File

@ -1,9 +1,9 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"

View File

@ -1,8 +1,8 @@
{
"name": "laitool-pro",
"productName": "来推 Pro",
"version": "v3.4.3",
"description": "A desktop application for AI image generation and processing, built with Electron and Vue 3.",
"version": "v3.4.5",
"description": "来推 Pro - 一款集音频处理、文案生成、图片生成、视频生成等功能于一体的多合一AI工具软件。",
"main": "./out/main/index.js",
"author": "xiangbei",
"homepage": "https://electron-vite.org",

View File

@ -1,3 +1,4 @@
import { t } from '@/i18n'
import { escapeRegExp, isEmpty } from 'lodash'
//#region 检查字符串中是不是包含中文或者标点符号
/**
@ -35,15 +36,21 @@ export async function RetryWithBackoff<T>(
// 这边记下日志吧
global.logger.error(
fn.name + '_RetryWithBackoff',
`${attempts} 请求失败,开始下一次重试,失败信息如下:` + error.toString()
t("第 {attempts} 请求失败,开始下一次重试,失败信息如下,{error}", {
attempts,
error: error.message
})
)
if (attempts >= retries) {
throw new Error(`失败次数超过 ${retries} 错误信息如下: ${error.message}`)
throw new Error(t("失败次数超过 {retries} 错误信息如下,{error}", {
retries,
error: error.message
}))
}
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
throw new Error('所有重试失败') // 理论上不会到达这里
throw new Error(t('所有重试失败')) // 理论上不会到达这里
}
//#endregion
@ -122,11 +129,13 @@ export function ReplaceSubstrings(
*/
export function GetBaseUrl(url: string): string {
if (isEmpty(url)) {
throw new Error('url不能为空')
throw new Error(t("{data} 不能为空", {
data: 'url'
}))
}
// 判断是不是一个合法的url
if (!url.startsWith('http')) {
throw new Error('一个合法的url请求地址')
throw new Error(t('不是一个合法的url地址'))
}
const parsedUrl = new URL(url)
return `${parsedUrl.protocol}//${parsedUrl.host}`
@ -147,7 +156,7 @@ export async function DownloadFile(url: string, localPath?: string): Promise<voi
const response = await fetch(url)
if (!response.body) {
throw new Error('浏览器不支持流式下载')
throw new Error(t("浏览器不支持流式下载"))
}
const reader = response.body.getReader()
@ -192,7 +201,9 @@ export async function DownloadFile(url: string, localPath?: string): Promise<voi
if (res.statusCode === 200) {
resolve(res)
} else {
reject(new Error(`请求失败,状态码:${res.statusCode}`))
reject(new Error(t("请求失败,状态码:{statusCode}", {
statusCode: res.statusCode
})))
}
})
.on('error', reject)

View File

@ -3,6 +3,7 @@ import { isEmpty } from 'lodash'
import path from 'path'
import util from 'util'
import { exec } from 'child_process'
import { t } from '@/i18n'
const execAsync = util.promisify(exec)
const fspromises = fs.promises
@ -59,7 +60,9 @@ export async function DeleteFolderAllFile(
try {
let folderIsExist = await CheckFileOrDirExist(folderPath)
if (!folderIsExist) {
throw new Error('目的文件夹不存在,' + folderPath)
throw new Error(t("目的文件/文件夹不存在,{data}", {
data: folderPath
}))
}
// 开始删除
let files = await fspromises.readdir(folderPath)
@ -94,14 +97,18 @@ export async function CopyFileOrFolder(source, target, checkParent = false) {
try {
// 判断源文件或文件夹是不是存在
if (!(await CheckFileOrDirExist(source))) {
throw new Error(`源文件或文件夹不存在: ${source}`)
throw new Error(t("源文件或文件夹不存在,{data}", {
data: source
}))
}
// 判断父文件夹是否存在,不存在创建
const parent_path = path.dirname(target)
let parentIsExist = await CheckFileOrDirExist(parent_path)
if (!parentIsExist) {
if (checkParent) {
throw new Error(`目的文件或文件夹的父文件夹不存在: ${parent_path}`)
throw new Error(t("目的文件或文件夹的父文件夹不存在,{data}", {
data: parent_path
}))
} else {
await fspromises.mkdir(parent_path, { recursive: true })
}
@ -147,7 +154,7 @@ export async function IsDirectory(path) {
const stat = await fspromises.stat(path)
return stat.isDirectory()
} catch (error) {
throw new Error(`获取文件夹信息失败: ${path}`)
throw error
}
}
/**
@ -191,7 +198,7 @@ export async function GetFilesWithExtensions(
try {
// 判断当前是不是文件夹
if (!(await IsDirectory(folderPath))) {
throw new Error('输入的不是有效的文件夹地址')
throw new Error(t('输入的不是有效的文件夹地址'))
}
let entries = await fspromises.readdir(folderPath, { withFileTypes: true })
@ -238,7 +245,9 @@ export async function GetFilesWithExtensions(
export async function GetFileSize(filePath: string): Promise<number> {
try {
if (!(await CheckFileOrDirExist(filePath))) {
throw new Error('获取文件大小,指定的文件不存在')
throw new Error(t("目的文件/文件夹不存在,{data}", {
data: filePath
}))
}
const stats = await fspromises.stat(filePath)
return stats.size / 1024
@ -364,7 +373,7 @@ export async function DownloadImageFromUrl(
// 验证下载的数据是否有效
if (buffer.length === 0) {
throw new Error('下载的文件为空')
throw new Error(t('下载的文件为空'))
}
// 将图片数据写入本地文件
@ -373,7 +382,7 @@ export async function DownloadImageFromUrl(
console.log(`图片下载成功: ${localPath} (大小: ${(buffer.length / 1024).toFixed(2)} KB)`)
return localPath
} catch (error) {
lastError = error instanceof Error ? error : new Error('未知错误')
lastError = error instanceof Error ? error : new Error(t('未知错误'))
console.error(`${attempt}次下载失败:`, lastError.message)
@ -389,15 +398,28 @@ export async function DownloadImageFromUrl(
}
// 所有重试都失败了,抛出最后一个错误
const errorMessage = lastError?.message || '未知错误'
const errorMessage = lastError?.message || t('未知错误')
if (errorMessage.includes('timeout') || errorMessage.includes('Timeout')) {
throw new Error(`下载图片超时 (${timeout / 1000}秒),已重试${maxRetries}次: ${errorMessage}`)
throw new Error(t("下载图片超时 ({timeout}秒),已重试{maxRetries}次,失败信息:{errorMessage}", {
timeout: timeout / 1000,
maxRetries,
errorMessage
}))
} else if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('ECONNREFUSED')) {
throw new Error(`网络连接失败,无法访问图片地址,已重试${maxRetries}次: ${errorMessage}`)
throw new Error(t("网络连接失败,无法访问图片地址,已重试 {maxRetries}次,失败信息:{errorMessage}", {
maxRetries,
errorMessage
}))
} else if (errorMessage.includes('Connect Timeout Error')) {
throw new Error(`连接超时,服务器响应缓慢,已重试${maxRetries}次: ${errorMessage}`)
throw new Error(t("连接超时,服务器响应缓慢,已重试{maxRetries}次,失败信息:{errorMessage}", {
maxRetries,
errorMessage
}))
} else {
throw new Error(`下载图片失败,已重试${maxRetries}次: ${errorMessage}`)
throw new Error(t("下载图片失败,已重试{maxRetries}次,失败信息: ${errorMessage}", {
maxRetries,
errorMessage
}))
}
}

View File

@ -3,6 +3,7 @@ import sharp from 'sharp'
import { CheckFileOrDirExist, CheckFolderExistsOrCreate } from './file'
import fs from 'fs'
import https from 'https'
import { t } from '@/i18n'
/**
* (base64或buffer)
@ -21,17 +22,17 @@ export async function ResizeImage(
try {
// 检查 type 参数
if (type !== 'base64' && type !== 'buffer') {
throw new Error('type 参数必须是 "base64" 或 "buffer"')
throw new Error(t('未知类型'))
}
// 判断是不是图片文件
if (!image_path.match(/\.(jpg|jpeg|png)$/)) {
throw new Error('输入的文件地址不是图片文件地址')
throw new Error(t("输入的文件地址不是图片文件地址支持jpg、jpeg、png"))
}
// 判断文件是否存在
if (!(await CheckFileOrDirExist(image_path))) {
throw new Error('文件不存在')
throw new Error(t('文件不存在'))
}
// 修改图片尺寸
@ -58,12 +59,12 @@ export async function GetImageSize(image_path: string) {
try {
// 判断文件是否存在
if (!(await CheckFileOrDirExist(image_path))) {
throw new Error('文件不存在')
throw new Error(t('文件不存在'))
}
// 判断是不是图片文件
if (!image_path.match(/\.(jpg|jpeg|png)$/)) {
throw new Error('输入的文件地址不是图片文件地址')
throw new Error(t("输入的文件地址不是图片文件地址支持jpg、jpeg、png"))
}
// 获取图片的宽高
@ -146,7 +147,7 @@ export function GetImageTypeFromBase64(base64String: string): string {
}
}
} catch (error) {
console.error('解析base64图片类型时出错:', error)
throw error
}
return extension
@ -159,7 +160,9 @@ export function GetImageTypeFromBase64(base64String: string): string {
*/
export function GetImageBase64(url: string): Promise<string> {
if (!url) {
return Promise.reject('URL不能为空')
return Promise.reject(t("{data} 不能为空", {
data: 'URL'
}))
}
if (url.startsWith('http://') || url.startsWith('https://')) {
return new Promise((resolve, reject) => {
@ -242,7 +245,7 @@ export async function ProcessImage(
// 获取图片的元数据
const { width, height } = await image.metadata()
if (!width || !height) {
throw new Error('获取图片的宽高失败')
throw new Error(t('获取图片的宽高失败'))
}
const whiteBackground = await sharp()
@ -316,7 +319,9 @@ export async function Base64ToFile(base64: string, outFilePath: string): Promise
await fs.promises.writeFile(outFilePath, dataBuffer)
// await this.tools.writeArrayToFile(dataBuffer, out_file);
} catch (error: any) {
throw new Error('将base64转换为文件失败失败信息如下' + error.toString())
throw new Error(t("将base64转换为文件失败{error}", {
error: (error as Error).toString()
}))
}
}
@ -340,7 +345,7 @@ export async function ImageSplit(
// 获取图片元数据
const metadata = await sharp(inputPath).metadata()
if (!metadata.height || !metadata.width) {
throw new Error('获取图片的宽高失败')
throw new Error(t('获取图片的宽高失败'))
}
// 计算每个分块的宽高使用Math.floor确保不超出边界

View File

@ -29,7 +29,8 @@ export class Logger {
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '10m',
maxFiles: '14d'
maxFiles: '14d',
options: { flags: 'a', encoding: 'utf8' }
})
]
})

View File

@ -1,3 +1,5 @@
import { t } from "@/i18n"
/**
* JSON解析
* @param str
@ -21,12 +23,12 @@ export function ValidateJson(str: string): boolean {
export function ValidateJsonAndParse<T>(str: string): T {
try {
if (str == null) {
throw new Error('数据不能为空')
throw new Error(t("数据不能为空"))
}
let res = JSON.parse(str) as T
return res
} catch (e) {
throw new Error('数据解析失败,请检查数据格式')
throw new Error(t('数据解析失败,请检查数据格式'))
}
}
@ -48,9 +50,11 @@ interface ValidationErrors {
export function ValidateErrorString(errors: ValidationErrors): string {
const errorMessages = Object.values(errors)
.map((err) => {
return err[0]?.message || '验证错误'
return err[0]?.message || t('验证错误')
})
.join(' ')
let res = '请修正以下错误: ' + (errorMessages || errors.message)
let res = t("请修正以下错误,{error}", {
error: errorMessages || errors.message
})
return res
}

View File

@ -1,3 +1,5 @@
import { t } from "@/i18n"
/**
* word数组进行重新分组
*
@ -135,6 +137,8 @@ export function splitTextByCustomDelimiters(oldText: string, formatSpecialChars:
return lines.join('\n')
} catch (error: any) {
throw new Error('格式化文本失败,失败信息如下:' + error.message)
throw new Error(t("格式化文本失败,{error}", {
error: (error as Error).message
}))
}
}

View File

@ -1,3 +1,4 @@
import { t } from '@/i18n'
import { AIStoryboardMasterAIEnhance } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterAIEnhance'
import { AIStoryboardMasterGeneral } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterGeneral'
import { AIStoryboardMasterMJAncientStyle } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterMJAncientStyle'
@ -24,7 +25,7 @@ export type AiInferenceModelModel = {
export const aiOptionsData: AiInferenceModelModel[] = [
{
value: 'AIStoryboardMasterScenePrompt',
label: '【LaiTool】场景提示大师上下文-提示词不包含人物)',
label: t('【LaiTool】场景提示大师上下文-提示词不包含人物)'),
hasExample: false,
mustCharacter: false,
requestBody: AIStoryboardMasterScenePrompt,
@ -32,7 +33,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [
},
{
value: 'AIStoryboardMasterSpecialEffects',
label: '【LaiTool】分镜大师-特效增强版(上下文-人物场景固定)',
label: t('【LaiTool】分镜大师-特效增强版(上下文-人物场景固定)'),
hasExample: false,
mustCharacter: true,
requestBody: AIStoryboardMasterSpecialEffects,
@ -40,7 +41,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [
},
{
value: 'AIStoryboardMasterGeneral',
label: '【LaiTool】分镜大师-通用版(上下文-人物场景固定-类型推理)',
label: t('【LaiTool】分镜大师-通用版(上下文-人物场景固定-类型推理)'),
hasExample: false,
mustCharacter: true,
requestBody: AIStoryboardMasterGeneral,
@ -48,7 +49,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [
},
{
value: 'AIStoryboardMasterAIEnhance',
label: '【LaiTool】分镜大师-全面版-AI增强上下文-人物场景固定-单帧)',
label: t('【LaiTool】分镜大师-全面版-AI增强上下文-人物场景固定-单帧)'),
hasExample: false,
mustCharacter: true,
requestBody: AIStoryboardMasterAIEnhance,
@ -56,7 +57,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [
},
{
value: 'AIStoryboardMasterOptimize',
label: '【LaiTool】分镜大师-全能优化版(上下文-人物固定)',
label: t('【LaiTool】分镜大师-全能优化版(上下文-人物固定)'),
hasExample: false,
mustCharacter: true,
requestBody: AIStoryboardMasterOptimize,
@ -64,7 +65,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [
},
{
value: 'AIStoryboardMasterMJAncientStyle',
label: '【LaiTool】分镜大师-MJ古风版上下文-人物场景固定-MJ古风提示词',
label: t('【LaiTool】分镜大师-MJ古风版上下文-人物场景固定-MJ古风提示词'),
hasExample: false,
mustCharacter: true,
requestBody: AIStoryboardMasterMJAncientStyle,
@ -72,7 +73,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [
},
{
value: 'AIStoryboardMasterSDEnglish',
label: '【LaiTool】分镜大师-SD英文版上下文-人物场景固定-SD-英文提示词)',
label: t('【LaiTool】分镜大师-SD英文版上下文-人物场景固定-SD-英文提示词)'),
hasExample: false,
mustCharacter: true,
requestBody: AIStoryboardMasterSDEnglish,
@ -80,7 +81,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [
},
{
value: 'AIStoryboardMasterSingleFrame',
label: '【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物自动推理)',
label: t('【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物自动推理)'),
hasExample: false,
mustCharacter: false,
requestBody: AIStoryboardMasterSingleFrame,
@ -88,7 +89,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [
},
{
value: 'AIStoryboardMasterSingleFrameWithCharacter',
label: '【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物场景固定)',
label: t('【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物场景固定)'),
hasExample: false,
mustCharacter: true,
requestBody: AIStoryboardMasterSingleFrameWithCharacter,
@ -106,7 +107,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [
export function GetAIPromptOptionByValue(value: string) {
let aiOptionIndex = aiOptionsData.findIndex((item) => item.value == value)
if (aiOptionIndex == -1) {
throw new Error('没有找到对应的AI选项请先检查配置')
throw new Error(t('没有找到对应的AI选项请先检查配置'))
}
return aiOptionsData[aiOptionIndex]
}

View File

@ -1,12 +1,15 @@
import { t } from "@/i18n"
export const apiDefineData = [
{
label: 'LAI API - 香港',
label: t('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',
video: 'https://api.laitool.cc/mj/submit/video',
update_file: 'https://api.laitool.cc/mj/submit/upload-discord-images',
once_get_task: 'https://api.laitool.cc/mj/task/${id}/fetch'
},
@ -16,13 +19,14 @@ export const apiDefineData = [
buy_url: 'https://api.laitool.cc/register?aff=RCSW'
},
{
label: 'LAI API - 美国',
label: t('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',
video: 'https://laitool.net/mj/submit/video',
update_file: 'https://laitool.net/mj/submit/upload-discord-images',
once_get_task: 'https://laitool.net/mj/task/${id}/fetch'
},
@ -32,7 +36,7 @@ export const apiDefineData = [
buy_url: 'https://laitool.net/register?aff=RCSW'
},
{
label: 'LaiTool生图包',
label: t('LaiTool生图包'),
value: '9c9023bd-871d-4b63-8004-facb3b66c5b3',
isPackage: true,
mj_url: {
@ -55,7 +59,7 @@ export const apiDefineData = [
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的配置请先检查配置')
throw new Error(t('没有找到对应的API的配置请先检查配置'))
}
return apiDefineData[mj_api_url_index]
}

View File

@ -98,30 +98,3 @@ export function getImageCategoryLabel(value: string): TaskModal.TaskStatus {
//#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

View File

@ -1,5 +1,7 @@
//#region MJ 出图方式
import { t } from "@/i18n";
/**
* MJ API
*/
@ -40,10 +42,10 @@ export enum ImageGenerateMode {
*/
export function getImageGenerateModeOptions(): Array<{ label: string; value: string }> {
return [
{ label: 'API模式', value: ImageGenerateMode.MJ_API },
{ label: 'LaiTool生图包', value: ImageGenerateMode.MJ_PACKAGE },
{ label: '代理模式', value: ImageGenerateMode.REMOTE_MJ },
{ label: '本地代理模式(自有账号推荐)', value: ImageGenerateMode.LOCAL_MJ }
{ label: t('API模式'), value: ImageGenerateMode.MJ_API },
{ label: t('LaiTool生图包'), value: ImageGenerateMode.MJ_PACKAGE },
{ label: t('代理模式'), value: ImageGenerateMode.REMOTE_MJ },
{ label: t('本地代理模式(自有账号推荐)'), value: ImageGenerateMode.LOCAL_MJ }
]
}
@ -208,11 +210,11 @@ export enum MJSpeed {
export function getMJSpeedOptions() {
return [
{
label: '快速',
label: t('快速'),
value: MJSpeed.FAST
},
{
label: '慢速',
label: t('慢速'),
value: MJSpeed.RELAX
}
]

View File

@ -1,3 +1,5 @@
import { t } from "@/i18n";
/**
*
*/
@ -18,9 +20,9 @@ export enum PresetCategory {
*/
export function getPresetCategoryOptions(): Array<{ label: string; value: string }> {
return [
{ label: '风格预设', value: PresetCategory.Style },
{ label: '人物预设', value: PresetCategory.Character },
{ label: '场景预设', value: PresetCategory.Scene }
{ label: t('按钮,风格预设'), value: PresetCategory.Style },
{ label: t('按钮,角色预设'), value: PresetCategory.Character },
{ label: t('按钮,场景预设'), value: PresetCategory.Scene }
]
}
@ -31,7 +33,7 @@ export function getPresetCategoryOptions(): Array<{ label: string; value: string
*/
export function getPresetCategoryLabel(value: string): string {
const option = getPresetCategoryOptions().find((item) => item.value === value)
return option ? option.label : '未知类型'
return option ? option.label : t('未知类型')
}
/**
@ -45,9 +47,9 @@ export function getPromptSortLabel(value: string): string {
return option.label
} else {
if (value == 'prompt') {
return '提示词'
return t('提示词')
} else {
return '未知类型'
return t('未知类型')
}
}
}

View File

@ -3,12 +3,8 @@ interface ISoftwareData {
version: string
/** 发布日期 */
date: string
/** 更新说明列表 */
notes: string[]
/** 系统信息 */
systemInfo: {
/** 快速开始 */
quickStart: string
/** 使用文档 */
documentationUrl: string
/** 更新文档 */
@ -34,28 +30,7 @@ interface ISoftwareData {
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',

View File

@ -63,6 +63,7 @@ export class VideoMessage extends Realm.Object<VideoMessage> {
runwayOptions!: string | null // 生成视频的一些设置
lumaOptions!: string | null // 生成视频的一些设置
klingOptions!: string | null // 生成视频的一些设置
mjVideoOptions!: string | null // MJ生成视频的一些设置
messageData!: string | null
static schema: ObjectSchema = {
name: 'VideoMessage',
@ -81,6 +82,7 @@ export class VideoMessage extends Realm.Object<VideoMessage> {
runwayOptions: 'string?',
lumaOptions: 'string?',
klingOptions: 'string?',
mjVideoOptions: 'string?',
messageData: 'string?'
},
primaryKey: 'id'
@ -166,6 +168,7 @@ export class BookTaskDetailModel extends Realm.Object<BookTaskDetailModel> {
bookTaskId!: string
videoPath!: string | null // 视频地址
generateVideoPath!: string | null // 生成视频地址
subVideoPath!: string[] | null // 生成的批次视频的地址
audioPath!: string | null // 音频地址
word!: string | null // 文案
oldImage!: string | null // 旧图片用于SD的图生图
@ -203,6 +206,7 @@ export class BookTaskDetailModel extends Realm.Object<BookTaskDetailModel> {
bookTaskId: { type: 'string', indexed: true },
videoPath: 'string?',
generateVideoPath: 'string?', // 生成视频地址
subVideoPath: 'string[]', // 生成的批次视频的地址
audioPath: 'string?',
word: 'string?',
oldImage: 'string?',

View File

@ -16,6 +16,8 @@ export class TaskListModel extends Realm.Object<TaskListModel> {
startTime!: number
endTime!: number
messageName?: string
taskId?: string // 任务ID可能是视频生成任务的ID
taskMessage?: string // 任务消息,可能是视频生成任务的消息
static schema: ObjectSchema = {
name: 'TaskList',
@ -33,7 +35,9 @@ export class TaskListModel extends Realm.Object<TaskListModel> {
updateTime: 'date',
startTime: 'int',
endTime: 'int',
messageName: 'string?'
messageName: 'string?',
taskId: 'string?', // 任务ID可能是视频生成任务的ID
taskMessage: 'string?' // 任务消息,可能是视频生成任务的消息
},
primaryKey: 'id'
}

View File

@ -13,15 +13,15 @@ import {
} 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'
const { app } = require('electron')
// 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
: path.resolve(app.getPath('userData'), '../laitool-pro/Database/option.realm') // Production path
// 版本迁移
const migration = (_oldRealm: Realm, _newRealm: Realm) => {}
@ -68,7 +68,7 @@ export class RealmBaseService extends BaseService {
PresetModel
],
path: this.dbpath,
schemaVersion: 19, // 数据库版本号,修改时需要增加
schemaVersion: 21, // 数据库版本号,修改时需要增加
migration: migration
}
this.realm = await Realm.open(config)

View File

@ -11,6 +11,7 @@ import { getGeneralSetting, getProjectPath } from '@/main/service/option/optionC
import { SrtHandle } from '@/main/service/common/srtHandle'
import { BookTaskDetailService } from './bookTaskDetailService'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import { t } from '@/i18n'
export class BookService extends RealmBaseService {
static instance: BookService | null = null
@ -37,15 +38,25 @@ export class BookService extends RealmBaseService {
* null
* @param bookId
*/
async GetBookDataById(bookId: string): Promise<Book.SelectBook | null> {
async GetBookDataById(bookId: string, notEmpty: true): Promise<Book.SelectBook>
async GetBookDataById(bookId: string, notEmpty?: false): Promise<Book.SelectBook | null>
async GetBookDataById(
bookId: string,
notEmpty: boolean = false
): Promise<Book.SelectBook | null> {
try {
if (isEmpty(bookId)) {
throw new Error('获取小说信息失败缺少小说ID')
throw new Error(t("{data} 不能为空", {
data: "ID"
}))
}
let projectPath: string = await getProjectPath()
let books = this.realm.objects<BookModel>('Book').filtered('id = $0', bookId)
if (books.length <= 0) {
if (notEmpty) {
throw new Error(t("未找到指定ID的小说数据"))
}
return null
} else {
// 对返回的数据进行处理
@ -148,14 +159,14 @@ export class BookService extends RealmBaseService {
async AddOrModifyBook(book: Book.SelectBook): Promise<Book.SelectBook> {
try {
if (book == null) {
throw new Error('小说数据为空,无法修改')
throw new Error(t('小说数据为空,无法修改'))
}
// 当小说的类型是反推的时候,必须传入视频
if (book.type == BookType.MJ_REVERSE || book.type == BookType.SD_REVERSE) {
// 判断视频是否存在
if (book.oldVideoPath == null || book.oldVideoPath == '') {
throw new Error('反推必须传入视频')
throw new Error(t('反推必须传入视频'))
}
}
@ -166,9 +177,11 @@ export class BookService extends RealmBaseService {
// 判断指定的名字在数据库中是否存在
let books = this.realm.objects('Book').filtered('name = $0', book.name)
if (books.length > 0) {
throw new Error(`小说名字 ${book.name} 已经存在,请更换小说名字`)
throw new Error(t("小说名字 {bookName} 已经存在,请更换小说名字", {
bookName: book.name
}))
}
console.log(this)
// 新增数据
book.id = crypto.randomUUID()
book.createTime = new Date()
@ -209,7 +222,7 @@ export class BookService extends RealmBaseService {
let generalSetting = await getGeneralSetting()
imageCategory = generalSetting.defaultImgGenMethod ?? ImageCategory.Midjourney
} else {
throw new Error('未知的小说类型')
throw new Error(t('未知的小说类型'))
}
const srtHandle = new SrtHandle()
let srtData = await srtHandle.GetSrtDataByPath(book.srtPath as string)
@ -286,7 +299,9 @@ export class BookService extends RealmBaseService {
.objects('Book')
.filtered('name = $0 AND id != $1', book.name, book.id)
if (books.length > 0) {
throw new Error(`小说名字 ${book.name} 已经存在,请更换小说名字`)
throw new Error(t("小说名字 {bookName} 已经存在,请更换小说名字", {
bookName: book.name
}))
}
// 两个文件夹地址不能改,删除两个属性
delete book.bookFolderPath
@ -313,16 +328,16 @@ export class BookService extends RealmBaseService {
async ModifyBookDataById(bookId: string, bookData: Book.SelectBook): Promise<Book.SelectBook> {
try {
if (bookId == null) {
throw new Error('修改小说数据失败缺少小说ID')
throw new Error(t("修改小说数据失败缺少小说ID"))
}
if (bookData == null) {
throw new Error('修改小说数据失败,缺少小说数据')
throw new Error(t('修改小说数据失败,缺少小说数据'))
}
// 检查小说ID对应的数据是不是存在
let bookRes = await this.GetBookDataById(bookId)
if (bookRes == null) {
throw new Error('修改小说数据失败小说ID对应的数据不存在')
throw new Error(t("修改小说数据失败小说ID对应的数据不存在"))
}
if (bookData && bookData.id) {
@ -336,7 +351,7 @@ export class BookService extends RealmBaseService {
bookRes = await this.GetBookDataById(bookId)
if (bookRes == null) {
throw new Error('获取修改后的小说数据失败小说ID对应的数据不存在')
throw new Error(t("获取修改后的小说数据失败小说ID对应的数据不存在"))
}
return bookRes
} catch (error) {
@ -353,7 +368,7 @@ export class BookService extends RealmBaseService {
this.transaction(() => {
let book = this.realm.objectForPrimaryKey('Book', bookId)
if (book == null) {
throw new Error('未找到对应的小说')
throw new Error(t("未找到指定ID的小说数据"))
}
// 删除对应的小说
this.realm.delete(book)

View File

@ -7,6 +7,8 @@ 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'
import { t } from '@/i18n'
import { ValidateJson } from '@/define/Tools/validate'
export class BookTaskDetailService extends RealmBaseService {
static instance: BookTaskDetailService | null = null
@ -38,7 +40,7 @@ export class BookTaskDetailService extends RealmBaseService {
): Promise<Book.SelectBookTaskDetail[]> {
try {
if (condition == null) {
throw new Error('查询小说分镜信息,查询条件不能为空')
throw new Error(t("查询小说分镜信息,查询条件不能为空!"))
}
let tasksToDelete = this.realm.objects<BookTaskDetailModel>('BookTaskDetail')
if (condition.id) {
@ -67,6 +69,23 @@ export class BookTaskDetailService extends RealmBaseService {
subImagePath: (item.subImagePath as string[])?.map((subImage) => {
return JoinPath(projectPath, subImage)
}),
subVideoPath: (item.subVideoPath as string[]).map((subVideo) => subVideo.toString()),
subVideoPathObject: (item.subVideoPath as string[])?.map((subVideo) => {
if (isEmpty(subVideo)) {
return {}
} else {
if (!ValidateJson(subVideo)) {
return {}
} else {
let obj = JSON.parse(subVideo)
if (!isEmpty(obj.localPath)) {
obj.localPath =
JoinPath(projectPath, obj.localPath) + '?t=' + new Date().getTime()
}
return obj
}
}
}),
characterTags: item.characterTags ? item.characterTags.map((tag) => tag) : [],
sceneTags: item.sceneTags ? item.sceneTags.map((tag) => tag) : [],
styleTags: item.styleTags ? item.styleTags.map((tag) => tag) : [],
@ -100,6 +119,42 @@ export class BookTaskDetailService extends RealmBaseService {
}
}
/**
* ID获取指定的小说任务分镜详细数据
* @param bookTaskDetailId
*/
public async GetBookTaskDetailDataById(
bookTaskDetailId: string,
notEmpty: true
): Promise<Book.SelectBookTaskDetail>
public async GetBookTaskDetailDataById(
bookTaskDetailId: string,
notEmpty?: false
): Promise<Book.SelectBookTaskDetail | null>
public async GetBookTaskDetailDataById(
bookTaskDetailId: string,
notEmpty: boolean = false
): Promise<Book.SelectBookTaskDetail | null> {
try {
if (bookTaskDetailId == null) {
throw new Error(t("{data} 不能为空", {
data: "ID"
}))
}
let bookTaskDetails = await this.GetBookTaskDetailDataByCondition({ id: bookTaskDetailId })
if (bookTaskDetails.length <= 0) {
if (notEmpty) {
throw new Error(t('未找到小说分镜数据,请检查!'))
}
return null
} else {
return bookTaskDetails[0] as Book.SelectBookTaskDetail
}
} catch (error) {
throw error
}
}
/**
*
* @param bookTaskDetailId ID
@ -109,34 +164,16 @@ export class BookTaskDetailService extends RealmBaseService {
async GetBookTaskDetailProperty(bookTaskDetailId: string, property: string) {
let bookTaskDetail = await this.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('未找到对应的小说任务详细信息 ' + bookTaskDetailId)
throw new Error(t("未找到指定ID的小说分镜信息ID: {id}", {
id: 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
throw new Error(t("未找到对应的属性,属性: {property}", {
property: property
}))
}
}
@ -149,7 +186,7 @@ export class BookTaskDetailService extends RealmBaseService {
// 判断是不是又小说ID
if (isEmpty(bookTaskDetail.bookId) || isEmpty(bookTaskDetail.bookTaskId)) {
throw new Error(
'新增小说任务详细信息到数据库失败数据不完整缺少小说ID或者小说批次任务ID'
t("新增小说分镜到数据库失败数据不完整缺少小说ID或者小说批次任务ID")
)
}
@ -196,10 +233,21 @@ export class BookTaskDetailService extends RealmBaseService {
updateData: Book.SelectBookTaskDetail
): Promise<Book.SelectBookTaskDetail | null> {
try {
if (
updateData.hasOwnProperty('generateVideoPath') &&
!isEmpty(updateData.generateVideoPath)
) {
let projectPath = await getProjectPath()
updateData.generateVideoPath = path.relative(
projectPath,
updateData.generateVideoPath as string
)
}
this.transaction(() => {
let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('未找到对应的小说任务详细信息')
throw new Error(t("没有找到要更新的小说分镜信息"))
}
// 开始修改
for (let key in updateData) {
@ -225,7 +273,7 @@ export class BookTaskDetailService extends RealmBaseService {
let mjMessageRes = this.realm.objectForPrimaryKey('MJMessage', bookTaskDetailId)
let bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到要更新的小说分镜信息')
throw new Error(t("没有找到要更新的小说分镜信息"))
}
if (bookTaskDetail.mjMessage == null) {
// 新增
@ -233,7 +281,7 @@ export class BookTaskDetailService extends RealmBaseService {
bookTaskDetail.mjMessage = mjMessage
} else {
if (mjMessageRes == null) {
throw new Error('没有找到要更新的出图信息')
throw new Error(t("没有找到要更新的出图信息"))
}
for (const key in mjMessage) {
if (key != 'id') mjMessageRes[key] = mjMessage[key]
@ -252,37 +300,41 @@ export class BookTaskDetailService extends RealmBaseService {
*/
async UpdateBookTaskDetailVideoMessage(
bookTaskDetailId: string,
videoMessage: BookTaskDetail.VideoMessage
videoMessage: Partial<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('没有找到要更新的小说分镜信息')
throw new Error(t('没有找到要更新的小说分镜信息'))
}
if (videoMessageRes == null) {
throw new Error('没有找到要更新的视频信息')
// 处理图片路径 - 转换为相对路径
if (videoMessage.imageUrl && !videoMessage.imageUrl.startsWith('http')) {
videoMessage.imageUrl = path.relative(projectPath, videoMessage.imageUrl)
}
if (bookTaskDetail.videoMessage == null) {
// 新增
// 新增视频消息
videoMessage.id = bookTaskDetailId
bookTaskDetail.videoMessage = videoMessage
} else {
// 更新现有视频消息
let videoMessageRes = this.realm.objectForPrimaryKey('VideoMessage', bookTaskDetailId)
if (videoMessageRes == 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])
}
if (key !== 'id') {
videoMessageRes[key] = videoMessage[key]
}
}
}
}
})
}
@ -307,7 +359,7 @@ export class BookTaskDetailService extends RealmBaseService {
)
if (bookTaskDetails.length <= 0) {
throw new Error('未找到执行的翻译数据,无法写回')
throw new Error(t("未找到执行的翻译的小说分镜数据,无法写回"))
}
let bookTaskDetail = bookTaskDetails[0]
// 直接写入
@ -328,7 +380,7 @@ export class BookTaskDetailService extends RealmBaseService {
DeleteBookTaskDetail(condition: Book.DeleteBookTaskDetailCondition) {
try {
if (isEmpty(condition.id) && isEmpty(condition.bookTaskId) && isEmpty(condition.bookId)) {
throw new Error('删除小说分镜信息失败,没有必要参数')
throw new Error(t('缺少必要的条件必须传入idbookId或者bookTaskId其中一个'))
}
let tasksToDelete = this.realm.objects<BookTaskDetailModel>('BookTaskDetail')
if (condition.id) {
@ -363,7 +415,7 @@ export class BookTaskDetailService extends RealmBaseService {
.objects<BookTaskDetailModel>('BookTaskDetail')
.filtered('id = $0', bookTaskDetailId)
if (bookTaskDetails.length <= 0) {
throw new Error('删除小说任务详细信息的反推提示词失败,未找到对应的分镜信息')
throw new Error(t("未找到对应的小说分镜信息"))
}
let bookTaskDetail = bookTaskDetails[0]
@ -388,7 +440,7 @@ export class BookTaskDetailService extends RealmBaseService {
bookTaskDetailId
)
if (bookTaskDetail == null) {
throw new Error('没有找到要删除的分镜信息')
throw new Error(t("未找到对应的小说分镜信息"))
}
if (bookTaskDetail.mjMessage) {
this.realm.delete(bookTaskDetail.mjMessage)

View File

@ -9,6 +9,7 @@ import { BookTaskStatus, CopyImageType } from '@/define/enum/bookEnum'
import path from 'path'
import { ImageToVideoModels } from '@/define/enum/video'
import { cloneDeep, isEmpty } from 'lodash'
import { t } from '@/i18n'
export class BookTaskService extends RealmBaseService {
static instance: BookTaskService | null = null
@ -104,15 +105,30 @@ export class BookTaskService extends RealmBaseService {
/**
* ID获取小说批次任务的数据
* @param bookTaskId
* @param notEmpty true时保证返回非空数据false时可能返回null
*/
async GetBookTaskDataById(bookTaskId: string): Promise<Book.SelectBookTask> {
async GetBookTaskDataById(bookTaskId: string, notEmpty: true): Promise<Book.SelectBookTask>
async GetBookTaskDataById(
bookTaskId: string,
notEmpty?: false
): Promise<Book.SelectBookTask | null>
async GetBookTaskDataById(
bookTaskId: string,
notEmpty: boolean = false
): Promise<Book.SelectBookTask | null> {
try {
if (bookTaskId == null) {
throw new Error('小说任务ID不能为空')
throw new Error(t("{data} 不能为空", {
data: "ID"
}))
}
let bookTasks = await this.GetBookTaskDataByCondition({ id: bookTaskId })
if (bookTasks.bookTasks.length <= 0) {
throw new Error('未找到对应的小说任务')
if (notEmpty) {
throw new Error(t("未找到对应的小说批次任务信息,请检查!"))
}
return null
} else {
return bookTasks.bookTasks[0]
}
@ -132,7 +148,7 @@ export class BookTaskService extends RealmBaseService {
// 修改对应小说批次任务的状态
let bookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (bookTask == null) {
throw new Error('未找到对应的小说任务')
throw new Error(t('未找到对应的小说批次任务'))
}
bookTask.status = status
bookTask.updateTime = new Date()
@ -158,14 +174,14 @@ export class BookTaskService extends RealmBaseService {
this.transaction(() => {
let updateData = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (updateData == null) {
throw new Error('未找到对应的小说任务详细信息')
throw new Error(t('未找到对应的小说批次任务'))
}
// 开始修改
for (let key in data) {
updateData[key] = data[key]
}
})
let res = await this.GetBookTaskDataById(bookTaskId)
let res = await this.GetBookTaskDataById(bookTaskId, true)
return res
} catch (error) {
throw error
@ -177,7 +193,9 @@ export class BookTaskService extends RealmBaseService {
try {
// 新增
if (bookTask.bookId == '' || bookTask.bookId == null) {
throw new Error('小说ID不能为空')
throw new Error(t("{data} 不能为空", {
data: t('小说ID')
}))
}
bookTask.id = crypto.randomUUID()
@ -195,7 +213,7 @@ export class BookTaskService extends RealmBaseService {
this.realm.create('BookTask', bookTask)
})
// 处理完毕,返回结果
let res = await this.GetBookTaskDataById(bookTask.id)
let res = await this.GetBookTaskDataById(bookTask.id, true)
return res
} catch (error) {
throw error
@ -213,7 +231,7 @@ export class BookTaskService extends RealmBaseService {
let addBookTaskDetail = [] as Book.SelectBookTaskDetail[]
let book = this.realm.objectForPrimaryKey('Book', sourceBookTask.bookId as string)
if (book == null) {
throw new Error('未找到对应的小说')
throw new Error(t('未找到指定ID的小说数据'))
}
let projectPath = await getProjectPath()
@ -263,7 +281,7 @@ export class BookTaskService extends RealmBaseService {
let subImagePath: string[] | undefined
if (element.outImagePath == null || isEmpty(element.outImagePath)) {
throw new Error('部分分镜的输出图片路径为空')
throw new Error(t('部分分镜的输出图片路径为空'))
}
if (copyImageType == CopyImageType.ALL) {
@ -272,7 +290,7 @@ export class BookTaskService extends RealmBaseService {
subImagePath = element.subImagePath
} else if (copyImageType == CopyImageType.ONE) {
if (!element.subImagePath || element.subImagePath.length <= 1) {
throw new Error('部分分镜的子图片路径数量不足或为空')
throw new Error(t('部分分镜的子图片路径数量不足或为空'))
}
// 只复制对应的
@ -285,7 +303,7 @@ export class BookTaskService extends RealmBaseService {
outImagePath = undefined
subImagePath = []
} else {
throw new Error('无效的图片复制类型')
throw new Error(t('未知类型'))
}
if (outImagePath) {
// 单独处理一下显示的图片
@ -390,7 +408,7 @@ export class BookTaskService extends RealmBaseService {
this.transaction(() => {
this.ResetBookTaskDataInfo(bookTaskId, resetBase)
})
let res = await this.GetBookTaskDataById(bookTaskId)
let res = await this.GetBookTaskDataById(bookTaskId, true)
return res
} catch (error) {
throw error
@ -409,7 +427,7 @@ export class BookTaskService extends RealmBaseService {
// 删除批次数据
let bookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (bookTask == null) {
throw new Error('未找到对应的小说任务,无法执行删除操作')
throw new Error(t('未找到对应的小说批次任务'))
}
this.realm.delete(bookTask)
})
@ -423,12 +441,12 @@ export class BookTaskService extends RealmBaseService {
try {
let modifyBookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (modifyBookTask == null) {
throw new Error('未找到对应的小说批次任务,无法执行重置操作')
throw new Error(t('未找到对应的小说批次任务'))
}
let book = this.realm.objectForPrimaryKey('Book', modifyBookTask.bookId)
if (book == null) {
throw new Error('未找到对应的小说,无法执行重置操作')
throw new Error(t('未找到指定ID的小说数据'))
}
if (resetBase) {

View File

@ -5,6 +5,7 @@ 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'
import { t } from '@/i18n'
export class TaskListService extends RealmBaseService {
static instance: TaskListService | null = null
@ -224,13 +225,13 @@ export class TaskListService extends RealmBaseService {
// 通过bookid获取book信息
let book = this.realm.objectForPrimaryKey('Book', bookId)
if (book == null) {
throw new Error('新增后台队列任务到数据库失败,没有找到对应的小说')
throw new Error(t('未找到指定ID的小说数据'))
}
let bookTask
if (bookTaskId) {
bookTask = this.realm.objectForPrimaryKey('BookTask', bookTaskId)
if (bookTask == null) {
throw new Error('新增后台队列任务到数据库失败,没有找到对应的小说批次任务')
throw new Error(t('未找到对应的小说批次任务'))
}
}
@ -239,14 +240,15 @@ export class TaskListService extends RealmBaseService {
bookTaskDetail = this.realm.objectForPrimaryKey('BookTaskDetail', bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error(
'新增后台队列任务到数据库失败,没有找到对应的小说批次任务详情(分镜数据)'
t("未找到指定ID的小说分镜信息ID: {id}", {
id: bookTaskDetailId
})
)
}
}
// 开始往数据库中写数据
let name = `${book.name}-${bookTask ? bookTask.name : 'default'}-${
bookTaskDetail ? bookTaskDetail.name : 'default'
let name = `${book.name}-${bookTask ? bookTask.name : 'default'}-${bookTaskDetail ? bookTaskDetail.name : 'default'
}-${taskType}`
let bookBackTask = {
@ -283,7 +285,7 @@ export class TaskListService extends RealmBaseService {
try {
// 判断数据是不是存在
if (isEmpty(bookBackTask.id) || isEmpty(bookBackTask.status)) {
throw new Error('修改后台队列任务失败,数据不完整,缺少必要字段')
throw new Error(t('修改后台队列任务失败,数据不完整,缺少必要字段'))
}
// 开始修改
this.transaction(() => {
@ -294,7 +296,7 @@ export class TaskListService extends RealmBaseService {
) as TaskModal.Task
// 判断数据是不是存在
if (_bookBackTask == null) {
throw new Error('修改后台队列任务失败,数据不存在')
throw new Error(t('未找到指定的后台任务数据'))
}
// 修改数据
_bookBackTask.status = bookBackTask.status
@ -321,6 +323,33 @@ export class TaskListService extends RealmBaseService {
}
}
/**
*
* @param taskId ID
* @param backTaskParam
*/
UpdateBackTaskData(taskId: string, backTaskParam: Partial<TaskModal.Task>): void {
this.transaction(() => {
// 根据ID获取后台任务
let backTask = this.realm.objectForPrimaryKey('TaskList', taskId)
// 检查任务是否存在
if (backTask == null) {
throw new Error(
t('未找到对应ID的任务任务ID{taskId}', { taskId })
)
}
// 遍历需要更新的字段
for (const key in backTaskParam) {
// 跳过ID字段防止主键被修改
if (key == 'id') {
continue
}
// 更新对应字段的值
backTask[key] = backTaskParam[key]
}
})
}
/**
* idbookIdbookTaskId
*
@ -333,7 +362,7 @@ export class TaskListService extends RealmBaseService {
!bookBackTask.hasOwnProperty('bookId') &&
!bookBackTask.hasOwnProperty('bookTaskId')
) {
throw new Error('删除后台队列任务失败,缺少必要的删除条件')
throw new Error(t("缺少必要的删除条件至少需要id、bookId、bookTaskId其中一个"))
}
this.transaction(() => {
@ -372,7 +401,7 @@ export class TaskListService extends RealmBaseService {
for (let i = 0; i < ids.length; i++) {
let task = this.realm.objectForPrimaryKey('TaskList', ids[i])
if (task == null) {
throw new Error('没有找到对应的任务')
throw new Error(t('未找到指定的后台任务数据'))
}
task.status = BookBackTaskStatus.FAIL
task.errorMessage = errorMessage
@ -414,10 +443,10 @@ export class TaskListService extends RealmBaseService {
for (let i = 0; i < ids.length; i++) {
const element = this.realm.objectForPrimaryKey('TaskList', ids[i])
if (element == null) {
throw new Error('没有找到对应的任务')
throw new Error(t('未找到指定的后台任务数据'))
}
element.status = BookBackTaskStatus.FAIL
element.errorMessage = '任务被丢弃'
element.errorMessage = t('任务被丢弃')
}
})
}

View File

@ -2,6 +2,8 @@ import Realm from 'realm'
import { isEmpty, cloneDeep } from 'lodash'
import { RealmBaseService } from './base/realmBase'
import { OptionType } from '@/define/enum/option'
import { optionSerialization } from '@/main/service/option/optionSerialization'
import { t } from '@/i18n'
export class OptionRealmService extends RealmBaseService {
static instance: OptionRealmService | null = null
@ -23,6 +25,7 @@ export class OptionRealmService extends RealmBaseService {
return OptionRealmService.instance
}
//#region GetOptionByKey
/**
* Optionkeynull
* @param key
@ -32,7 +35,7 @@ export class OptionRealmService extends RealmBaseService {
if (isEmpty(key)) {
return null
}
let res = this.realm.objects('Option').filtered(`key = "${key}"`);
let res = this.realm.objects('Option').filtered(`key = "${key}"`)
if (res.length > 0) {
let resData = Array.from(res).map((item) => {
@ -43,10 +46,13 @@ export class OptionRealmService extends RealmBaseService {
})
return resData[0] as OptionModel.OptionModel
} else {
return null;
return null
}
}
//#endregion
//#region ModifyOptionByKey
/**
* Optionkey
* @param key
@ -56,11 +62,11 @@ export class OptionRealmService extends RealmBaseService {
if (isEmpty(key)) {
return false
}
let option = this.realm.objectForPrimaryKey('Option', key);
let option = this.realm.objectForPrimaryKey('Option', key)
if (option) {
this.realm.write(() => {
option.value = value;
option.type = type;
option.value = value
option.type = type
option.updateTime = new Date()
})
} else {
@ -76,6 +82,38 @@ export class OptionRealmService extends RealmBaseService {
}
return true
}
//#endregion
//#region GetOptionData
/**
*
* @param optionKey
* @param description
* @param defaultValue
* @returns
* @throws
*/
public GetOptionDataByKey<T>(optionKey: string, description: string, defaultValue?: T): T {
try {
// 获取设置数据
let res = this.GetOptionByKey(optionKey)
// 开始解析
let optionData = optionSerialization<T>(res, description, defaultValue)
return optionData
} catch (error: any) {
// 记录错误日志
let errorMessage = t("获取 {description} 失败,{error}", {
description,
error: error.message || t('未知错误')
})
console.error(errorMessage)
// 重新抛出带有更详细信息的错误
throw new Error(errorMessage)
}
}
}

View File

@ -4,6 +4,7 @@ import { cloneDeep, isEmpty } from 'lodash'
import { PresetModel } from '../model/preset'
import path from 'path'
import { PresetModel as DefinePresetModel } from '@/define/model/preset'
import { t } from '@/i18n'
export class PresetRealmService extends RealmBaseService {
static instance: PresetRealmService | null = null
@ -160,13 +161,13 @@ export class PresetRealmService extends RealmBaseService {
}
if (isEmpty(newPreset.label)) {
throw new Error('预设名称不能为空!')
throw new Error(t('预设名称不能为空!'))
}
if (isEmpty(newPreset.type)) {
throw new Error('预设不能为空!')
throw new Error(t('预设类不能为空!'))
}
if (isEmpty(newPreset.id)) {
throw new Error('预设ID不能为空')
throw new Error(t("预设ID不能为空"))
}
// 判断对应的名字和类型是不是存在
@ -174,7 +175,7 @@ export class PresetRealmService extends RealmBaseService {
.objects('Preset')
.filtered('label = $0 && type = $1', newPreset.label, newPreset.type)
if (existingPreset.length > 0) {
throw new Error('再相同类型下已存在当前的预设名字,请修改后再试!')
throw new Error(t('在相同类型下已存在当前的预设名字,请修改后再试!'))
}
// 开始写入数据
@ -196,7 +197,7 @@ export class PresetRealmService extends RealmBaseService {
* */
ModifyPreset(id: string, preset: Partial<DefinePresetModel.Preset>): DefinePresetModel.Preset {
if (isEmpty(id)) {
throw new Error('要修改的预设ID不能为空')
throw new Error(t("预设ID不能为空"))
}
delete preset.id // 删除ID属性避免修改ID
delete preset.createTime // 删除创建时间属性,避免修改创建时间
@ -204,7 +205,7 @@ export class PresetRealmService extends RealmBaseService {
this.transaction(() => {
let existingPreset = this.realm.objectForPrimaryKey<PresetModel>('Preset', id)
if (existingPreset == null) {
throw new Error('要修改的预设不存在!')
throw new Error(t('未找到指定的预设数据'))
}
// 开始修改
for (let key in preset) {
@ -224,12 +225,12 @@ export class PresetRealmService extends RealmBaseService {
* */
DeletePreset(id: string): void {
if (isEmpty(id)) {
throw new Error('要删除的预设ID不能为空')
throw new Error(t("预设ID不能为空"))
}
this.transaction(() => {
let existingPreset = this.realm.objectForPrimaryKey('Preset', id)
if (existingPreset == null) {
throw new Error('要删除的预设不存在!')
throw new Error(t('未找到指定的预设数据'))
}
// 开始删除
this.realm.delete(existingPreset)

View File

@ -1,4 +1,5 @@
import { TaskModal } from '@/define/model/task'
import { t } from '@/i18n'
//#region 小说类型
@ -19,13 +20,13 @@ export enum BookType {
export function GetBookTypeLabel(type: BookType) {
switch (type) {
case BookType.ORIGINAL:
return '原创'
return t('原创')
case BookType.SD_REVERSE:
return 'SD反推'
return t('SD反推')
case BookType.MJ_REVERSE:
return 'MJ反推'
return t('MJ反推')
default:
return '未知类型'
return t('未知类型')
}
}
@ -35,9 +36,9 @@ export function GetBookTypeLabel(type: BookType) {
*/
export function GetBookTypeOptions() {
return [
{ label: '原创', value: BookType.ORIGINAL },
{ label: 'SD反推', value: BookType.SD_REVERSE },
{ label: 'MJ反推', value: BookType.MJ_REVERSE }
{ label: t('原创'), value: BookType.ORIGINAL },
{ label: t('SD反推'), value: BookType.SD_REVERSE },
{ label: t('MJ反推'), value: BookType.MJ_REVERSE }
]
}
@ -325,43 +326,43 @@ export enum BookTagSelectType {
export function GetBookBackTaskTypeLabel(key: string) {
switch (key) {
case BookBackTaskType.STORYBOARD:
return '分镜计算'
return t('分镜计算')
case BookBackTaskType.SPLIT:
return '分割视频'
return t('分割视频')
case BookBackTaskType.AUDIO:
return '提取音频'
return t('提取音频')
case BookBackTaskType.RECOGNIZE:
return '识别字幕'
return t('识别字幕')
case BookBackTaskType.FRAME:
return '抽帧'
return t('抽帧')
case BookBackTaskType.MJ_REVERSE:
return 'MJ反推'
return t('MJ反推')
case BookBackTaskType.SD_REVERSE:
return 'SD反推'
return t('SD反推')
case BookBackTaskType.MJ_IMAGE:
return 'MJ生成图片'
return t('MJ生成图片')
case BookBackTaskType.SD_IMAGE:
return 'SD生成图片'
return t('SD生成图片')
case BookBackTaskType.FLUX_FORGE_IMAGE:
return 'flux forge生成图片'
return t('flux forge生成图片')
case BookBackTaskType.FLUX_API_IMAGE:
return 'flux api生成图片'
return t('flux api生成图片')
case BookBackTaskType.D3_IMAGE:
return 'D3生成图片'
return t('D3生成图片')
case BookBackTaskType.HD:
return '高清'
return t('高清')
case BookBackTaskType.COMPOSING:
return '合成视频'
return t('合成视频')
case BookBackTaskType.INFERENCE:
return '推理'
return t('推理')
case BookBackTaskType.TRANSLATE:
return '翻译'
return t('翻译')
case BookBackTaskType.RUNWAY_VIDEO:
return 'runway生成视频'
return t('runway生成视频')
case BookBackTaskType.LUMA_VIDEO:
return 'luma生成视频'
return t('luma生成视频')
case BookBackTaskType.KLING_VIDEO:
return 'kling生成视频'
return t('kling生成视频')
default:
return key
}
@ -377,199 +378,199 @@ export function GetBookTaskDetailStatusLabel(key: string): TaskModal.TaskStatus
case BookTaskStatus.WAIT:
return {
status: BookTaskStatus.WAIT,
label: '等待',
label: t('等待'),
type: 'warning'
}
case BookTaskStatus.STORYBOARD:
return {
status: BookTaskStatus.STORYBOARD,
label: '分镜计算中',
label: t('分镜计算中'),
type: 'info'
}
case BookTaskStatus.STORYBOARD_FAIL:
return {
status: BookTaskStatus.STORYBOARD_FAIL,
label: '分镜计算失败',
label: t('分镜计算失败'),
type: 'error'
}
case BookTaskStatus.STORYBOARD_DONE:
return {
status: BookTaskStatus.STORYBOARD_DONE,
label: '分镜计算完成',
label: t('分镜计算完成'),
type: 'success'
}
case BookTaskStatus.SPLIT:
return {
status: BookTaskStatus.SPLIT,
label: '分割视频中',
label: t('分割视频中'),
type: 'info'
}
case BookTaskStatus.SPLIT_FAIL:
return {
status: BookTaskStatus.SPLIT_FAIL,
label: '分割视频失败',
label: t('分割视频失败'),
type: 'error'
}
case BookTaskStatus.SPLIT_DONE:
return {
status: BookTaskStatus.SPLIT_DONE,
label: '分割视频完成',
label: t('分割视频完成'),
type: 'success'
}
case BookTaskStatus.AUDIO:
return {
status: BookTaskStatus.AUDIO,
label: '提取音频中',
label: t('提取音频中'),
type: 'info'
}
case BookTaskStatus.AUDIO_FAIL:
return {
status: BookTaskStatus.AUDIO_FAIL,
label: '提取音频失败',
label: t('提取音频失败'),
type: 'error'
}
case BookTaskStatus.AUDIO_DONE:
return {
status: BookTaskStatus.AUDIO_DONE,
label: '提取音频完成',
label: t('提取音频完成'),
type: 'success'
}
case BookTaskStatus.RECOGNIZE:
return {
status: BookTaskStatus.RECOGNIZE,
label: '识别字幕中',
label: t('识别字幕中'),
type: 'info'
}
case BookTaskStatus.RECOGNIZE_FAIL:
return {
status: BookTaskStatus.RECOGNIZE_FAIL,
label: '识别字幕失败',
label: t('识别字幕失败'),
type: 'error'
}
case BookTaskStatus.RECOGNIZE_DONE:
return {
status: BookTaskStatus.RECOGNIZE_DONE,
label: '识别字幕完成',
label: t('识别字幕完成'),
type: 'success'
}
case BookTaskStatus.FRAME:
return {
status: BookTaskStatus.FRAME,
label: '抽帧中',
label: t('抽帧中'),
type: 'info'
}
case BookTaskStatus.FRAME_FAIL:
return {
status: BookTaskStatus.FRAME_FAIL,
label: '抽帧失败',
label: t('抽帧失败'),
type: 'error'
}
case BookTaskStatus.FRAME_DONE:
return {
status: BookTaskStatus.FRAME_DONE,
label: '抽帧完成',
label: t('抽帧完成'),
type: 'success'
}
case BookTaskStatus.REVERSE:
return {
status: BookTaskStatus.REVERSE,
label: '反推中',
label: t('反推中'),
type: 'info'
}
case BookTaskStatus.REVERSE_FAIL:
return {
status: BookTaskStatus.REVERSE_FAIL,
label: '反推失败',
label: t('反推失败'),
type: 'error'
}
case BookTaskStatus.REVERSE_DONE:
return {
status: BookTaskStatus.REVERSE_DONE,
label: '反推完成',
label: t('反推完成'),
type: 'success'
}
case BookTaskStatus.IMAGE:
return {
status: BookTaskStatus.IMAGE,
label: '生成图片中',
label: t('生成图片中'),
type: 'info'
}
case BookTaskStatus.IMAGE_FAIL:
return {
status: BookTaskStatus.IMAGE_FAIL,
label: '生成图片失败',
label: t('生成图片失败'),
type: 'error'
}
case BookTaskStatus.IMAGE_DONE:
return {
status: BookTaskStatus.IMAGE_DONE,
label: '生成图片完成',
label: t('生成图片完成'),
type: 'success'
}
case BookTaskStatus.HD:
return {
status: BookTaskStatus.HD,
label: '高清中',
label: t('高清中'),
type: 'info'
}
case BookTaskStatus.HD_FAIL:
return {
status: BookTaskStatus.HD_FAIL,
label: '高清失败',
label: t('高清失败'),
type: 'error'
}
case BookTaskStatus.HD_DONE:
return {
status: BookTaskStatus.HD_DONE,
label: '高清完成',
label: t('高清完成'),
type: 'success'
}
case BookTaskStatus.COMPOSING:
return {
status: BookTaskStatus.COMPOSING,
label: '合成视频中',
label: t('合成视频中'),
type: 'info'
}
case BookTaskStatus.COMPOSING_FAIL:
return {
status: BookTaskStatus.COMPOSING_FAIL,
label: '合成视频失败',
label: t('合成视频失败'),
type: 'error'
}
case BookTaskStatus.COMPOSING_DONE:
return {
status: BookTaskStatus.COMPOSING_DONE,
label: '合成视频完成',
label: t('合成视频完成'),
type: 'success'
}
case BookTaskStatus.DRAFT_DONE:
return {
status: BookTaskStatus.DRAFT_DONE,
label: '添加草稿完成',
label: t('添加草稿完成'),
type: 'success'
}
case BookTaskStatus.DRAFT_FAIL:
return {
status: BookTaskStatus.DRAFT_FAIL,
label: '添加草稿失败',
label: t('添加草稿失败'),
type: 'error'
}
case BookTaskStatus.IMAGE_TO_VIDEO_ERROR:
return {
status: BookTaskStatus.IMAGE_TO_VIDEO_ERROR,
label: '图转视频失败',
label: t('图转视频失败'),
type: 'error'
}
case BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS:
return {
status: BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS,
label: '图转视频成功',
label: t('图转视频成功'),
type: 'success'
}
default:
return {
status: 'UNKNOWN',
label: '未知',
label: t('未知'),
type: 'warning'
}
}
@ -585,43 +586,43 @@ export function GetBookBackTaskStatusLabel(key: string): TaskModal.TaskStatus {
case BookBackTaskStatus.WAIT:
return {
status: BookBackTaskStatus.WAIT,
label: '等待',
label: t('等待'),
type: 'warning'
}
case BookBackTaskStatus.RUNNING:
return {
status: BookBackTaskStatus.RUNNING,
label: '运行中',
label: t('运行中'),
type: 'info'
}
case BookBackTaskStatus.PAUSE:
return {
status: BookBackTaskStatus.PAUSE,
label: '暂停',
label: t('暂停'),
type: 'warning'
}
case BookBackTaskStatus.DONE:
return {
status: BookBackTaskStatus.DONE,
label: '完成',
label: t('完成'),
type: 'success'
}
case BookBackTaskStatus.FAIL:
return {
status: BookBackTaskStatus.FAIL,
label: '失败',
label: t('失败'),
type: 'error'
}
case BookBackTaskStatus.RECONNECT:
return {
status: BookBackTaskStatus.RECONNECT,
label: '重连',
label: t('重连'),
type: 'warning'
}
default:
return {
status: 'UNKNOWN',
label: '未知',
label: t('未知'),
type: 'warning'
}
}

View File

@ -1,3 +1,5 @@
import { t } from "@/i18n";
/**
*
*
@ -32,19 +34,19 @@ export enum JianyingKeyFrameEnum {
export function getJianyingKeyFrameOptions(): Array<{ label: string; value: JianyingKeyFrameEnum }> {
return [
{
label: '上下关键帧',
label: t('上下关键帧'),
value: JianyingKeyFrameEnum.KFTypePositionY
},
{
label: '左右关键帧',
label: t('左右关键帧'),
value: JianyingKeyFrameEnum.KFTypePositionX
},
{
label: '缩放关键帧',
label: t('缩放关键帧'),
value: JianyingKeyFrameEnum.KFTypeScale
},
{
label: '随机关键帧',
label: t('随机关键帧'),
value: JianyingKeyFrameEnum.KFTypeRandom
}
]

View File

@ -103,5 +103,12 @@ export const OptionKeyName = {
/** Comfy UI 基础设置 */
ComfyUISimpleSetting: 'SD_ComfyUISimpleSetting'
},
Video: {
/** 是否开启右侧控制面板 */
ShowRightPanel: 'Video_ShowRightPanel',
/** 是否开启分页 */
ShowPagination: 'Video_ShowPagination'
}
}

View File

@ -53,12 +53,11 @@ export enum SoftColor {
// 错误红色
ERROR_RED = '#c8161d'
}
export enum ResponseMessageType {
GET_TEXT = 'getText', // 获取文案
REMOVE_WATERMARK = "REMOVE_WATERMARK",// 删除水印
REMOVE_WATERMARK = 'REMOVE_WATERMARK', // 删除水印
MJ_REVERSE = 'MJ_REVERSE', // MJ反推返回反推结果
SD_REVERSE = 'SD_REVERSE', // MJ反推返回反推结果
REVERSE_PROMPT_TRANSLATE = 'REVERSE_PROMPT_TRANSLATE', // 反推提示词翻译
@ -66,17 +65,19 @@ export enum ResponseMessageType {
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" //视频生成成功
RUNWAY_VIDEO = 'RUNWAY_VIDEO', // Runway生成视频
LUMA_VIDEO = 'LUMA_VIDEO', // Luma生成视频
KLING_VIDEO = 'KLING_VIDEO', // Kling生成视频
MJ_VIDEO = 'MJ_VIDEO', // MJ生成视频
MJ_VIDEO_EXTEND = 'MJ_VIDEO_EXTEND', // MJ生成视频拓展
VIDEO_SUCESS = 'VIDEO_SUCESS' //视频生成成功
}
export enum LaiAPIType {
// 主要的
MAIN = "main",
MAIN = 'main',
// 香港代理
HK_PROXY = "hk-proxy",
HK_PROXY = 'hk-proxy',
// 备用站点
BAK_MAIN = 'bak-main'
}
@ -84,6 +85,5 @@ export enum LaiAPIType {
// 同步GPTkey的分类
export enum SyncGptKeyType {
// 字幕设置
SUBTITLE_SETTING = 'subtitle_setting',
SUBTITLE_SETTING = 'subtitle_setting'
}

View File

@ -1,39 +1,38 @@
//#region 图转视频类型
import { BookBackTaskType } from "./bookEnum";
import { t } from '@/i18n'
import { BookBackTaskType } from './bookEnum'
/** 图片转视频的方式 */
export enum ImageToVideoModels {
/** runway 生成视频 */
RUNWAY = "RUNWAY",
RUNWAY = 'RUNWAY',
/** luma 生成视频 */
LUMA = "LUMA",
LUMA = 'LUMA',
/** 可灵生成视频 */
KLING = "KLING",
KLING = 'KLING',
/** Pika 生成视频 */
PIKA = "PIKA",
PIKA = 'PIKA',
/** MJ 图转视频 */
MJ_VIDEO = "MJ_VIDEO",
MJ_VIDEO = 'MJ_VIDEO',
/** MJ 视频拓展 */
MJ_VIDEO_EXTEND = "MJ_VIDEO_EXTEND"
MJ_VIDEO_EXTEND = 'MJ_VIDEO_EXTEND'
}
export const MappingTaskTypeToVideoModel = (type: BookBackTaskType | string) => {
switch (type) {
case BookBackTaskType.LUMA_VIDEO:
return ImageToVideoModels.LUMA;
return ImageToVideoModels.LUMA
case BookBackTaskType.RUNWAY_VIDEO:
return ImageToVideoModels.RUNWAY;
return ImageToVideoModels.RUNWAY
case BookBackTaskType.KLING_VIDEO:
return ImageToVideoModels.KLING;
return ImageToVideoModels.KLING
case BookBackTaskType.MJ_VIDEO:
return ImageToVideoModels.MJ_VIDEO;
return ImageToVideoModels.MJ_VIDEO
case BookBackTaskType.MJ_VIDEO_EXTEND:
return ImageToVideoModels.MJ_VIDEO_EXTEND;
return ImageToVideoModels.MJ_VIDEO_EXTEND
default:
return "UNKNOWN"
return 'UNKNOWN'
}
}
@ -45,17 +44,17 @@ export const MappingTaskTypeToVideoModel = (type: BookBackTaskType | string) =>
export const GetImageToVideoModelsLabel = (model: ImageToVideoModels | string) => {
switch (model) {
case ImageToVideoModels.RUNWAY:
return "Runway";
return 'Runway'
case ImageToVideoModels.LUMA:
return "Luma";
return 'Luma'
case ImageToVideoModels.KLING:
return "可灵";
return t('可灵')
case ImageToVideoModels.PIKA:
return "Pika";
return 'Pika'
case ImageToVideoModels.MJ_VIDEO:
return "MJ视频";
return t('MJ视频')
default:
return "未知";
return '未知'
}
}
@ -70,55 +69,63 @@ export const GetImageToVideoModelsLabel = (model: ImageToVideoModels | string) =
*/
export const GetImageToVideoModelsOptions = () => {
return [
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.MJ_VIDEO), value: ImageToVideoModels.MJ_VIDEO },
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.RUNWAY), value: ImageToVideoModels.RUNWAY },
{
label: GetImageToVideoModelsLabel(ImageToVideoModels.MJ_VIDEO),
value: ImageToVideoModels.MJ_VIDEO
},
{
label: GetImageToVideoModelsLabel(ImageToVideoModels.RUNWAY),
value: ImageToVideoModels.RUNWAY
},
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.LUMA), value: ImageToVideoModels.LUMA },
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.KLING), value: ImageToVideoModels.KLING },
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.PIKA), value: ImageToVideoModels.PIKA },
{
label: GetImageToVideoModelsLabel(ImageToVideoModels.KLING),
value: ImageToVideoModels.KLING
},
{ label: GetImageToVideoModelsLabel(ImageToVideoModels.PIKA), value: ImageToVideoModels.PIKA }
]
}
//#endregion
//#region 通用
/** 生成视频的方式 */
export enum VideoModel {
/** 文生视频 */
TEXT_TO_VIDEO = "textToVideo",
TEXT_TO_VIDEO = 'textToVideo',
/** 图生视频 */
IMAGE_TO_VIDEO = "imageToVideo",
IMAGE_TO_VIDEO = 'imageToVideo'
}
/** 图转视频的状态 */
export enum VideoStatus {
/** 等待 */
WAIT = "wait",
WAIT = 'wait',
/** 处理中 */
PROCESSING = "processing",
PROCESSING = 'processing',
/** 完成 */
SUCCESS = "success",
SUCCESS = 'success',
/** 失败 */
FAIL = "fail",
FAIL = 'fail'
}
export const GetVideoStatus = (status: VideoStatus | string) => {
switch (status) {
case VideoStatus.WAIT:
case "0":
return "等待";
case '0':
return t('等待')
case VideoStatus.PROCESSING:
case "1":
return "处理中";
case '1':
return t('处理中')
case VideoStatus.SUCCESS:
case "3":
return "完成";
case '3':
return t('完成')
case VideoStatus.FAIL:
case '2':
return "失败";
return t('失败')
default:
return "未知";
return t('未知')
}
}
@ -128,14 +135,14 @@ export const GetVideoStatus = (status: VideoStatus | string) => {
/** runway 生成视频的模型 */
export enum RunawayModel {
GNE2 = "gen2",
GNE3 = "gen3",
GNE2 = 'gen2',
GNE3 = 'gen3'
}
/** runway 合成视频的时长 */
export enum RunwaySeconds {
FIVE = 5,
TEN = 10,
TEN = 10
}
//#endregion
@ -143,9 +150,9 @@ export enum RunwaySeconds {
export enum KlingMode {
/** 高性能 */
STD = "std",
STD = 'std',
/** 高表现 */
PRO = "pro"
PRO = 'pro'
}
//#endregion
@ -156,24 +163,25 @@ export enum KlingMode {
* indextaskId必填
*/
export enum MJVideoAction {
Extend = "extend",
Extend = 'extend'
}
/**
*
*/
export enum MJVideoImageType {
Base64 = "base64",
Url = "url",
Base64 = 'base64',
Url = 'url'
}
/**
* MJ Video的动作幅度
*/
export enum MJVideoMotion {
High = "high",
Low = "low",
High = 'high',
Low = 'low'
}
/**
* MJ视频动作幅度的标签
*
@ -183,11 +191,11 @@ export enum MJVideoMotion {
export function GetMJVideoMotionLabel(model: MJVideoMotion | string) {
switch (model) {
case MJVideoMotion.High:
return "高 (High)";
return t('高 (High)')
case MJVideoMotion.Low:
return "低 (Low)";
return t('低 (Low)')
default:
return "无效"
return t('未知')
}
}
@ -199,9 +207,107 @@ export function GetMJVideoMotionLabel(model: MJVideoMotion | string) {
export function GetMJVideoMotionOptions() {
return [
{
label: GetMJVideoMotionLabel(MJVideoMotion.Low), value: MJVideoMotion.Low
}, {
label: GetMJVideoMotionLabel(MJVideoMotion.High), value: MJVideoMotion.High
label: GetMJVideoMotionLabel(MJVideoMotion.Low),
value: MJVideoMotion.Low
},
{
label: GetMJVideoMotionLabel(MJVideoMotion.High),
value: MJVideoMotion.High
}
]
}
/**
* MJ Video
*/
export enum MJVideoType {
/** 标清 480p */
SD = 'vid_1.1_i2v_480',
/** 高清 720p */
HD = 'vid_1.1_i2v_720'
}
/**
* MJ视频质量类型的标签
*
* @param type MJ视频质量类型枚举值或字符串
* @returns
*/
export function GetMJVideoTypeLabel(type: MJVideoType | string) {
switch (type) {
case MJVideoType.SD:
return t('标清 (SD 480p)')
case MJVideoType.HD:
return t('高清 (HD 720p)')
default:
return t('未知')
}
}
/**
* MJ视频质量类型的选项列表
*
* @returns UI组件
*/
export function GetMJVideoTypeOptions() {
return [
{
label: GetMJVideoTypeLabel(MJVideoType.SD),
value: MJVideoType.SD
},
{ label: GetMJVideoTypeLabel(MJVideoType.HD), value: MJVideoType.HD }
]
}
/**
* MJ Video
*/
export enum MJVideoBatchSize {
/** 生成1个视频 */
ONE = 1,
/** 生成2个视频 */
TWO = 2,
/** 生成4个视频 */
FOUR = 4
}
/**
* MJ视频批次大小的标签
*
* @param batchSize MJ视频批次大小枚举值或数字
* @returns
*/
export function GetMJVideoBatchSizeLabel(batchSize: MJVideoBatchSize | number) {
switch (batchSize) {
case MJVideoBatchSize.ONE:
return t('1个视频')
case MJVideoBatchSize.TWO:
return t('2个视频')
case MJVideoBatchSize.FOUR:
return t('4个视频')
default:
return t('未知')
}
}
/**
* MJ视频批次大小的选项列表
*
* @returns UI组件
*/
export function GetMJVideoBatchSizeOptions() {
return [
{
label: GetMJVideoBatchSizeLabel(MJVideoBatchSize.ONE),
value: MJVideoBatchSize.ONE
},
{
label: GetMJVideoBatchSizeLabel(MJVideoBatchSize.TWO),
value: MJVideoBatchSize.TWO
},
{
label: GetMJVideoBatchSizeLabel(MJVideoBatchSize.FOUR),
value: MJVideoBatchSize.FOUR
}
]
}

View File

@ -0,0 +1,27 @@
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { ipcMain } from 'electron'
import { bookHandle } from '@/main/service/book'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
export function bookVideoIpc() {
// 处理获取小说图转视频信息列表的请求
ipcMain.handle(
DEFINE_STRING.BOOK.GET_VIDEO_BOOK_INFO_LIST,
async (_, condition: BookVideo.BookVideoInfoListQuertCondition) =>
await bookHandle.GetVideoBookInfoList(condition)
)
// 处理初始化视频消息数据的请求
ipcMain.handle(
DEFINE_STRING.BOOK.INIT_VIDEO_MESSAGE,
async (_, bookTaskId: string) =>
await bookHandle.InitVideoMessage(bookTaskId)
)
// 处理更新小说分镜视频消息的请求
ipcMain.handle(
DEFINE_STRING.BOOK.UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE,
async (_, bookTaskDetailId: string, videoMessage: Partial<BookTaskDetail.VideoMessage>) =>
await bookHandle.UpdateBookTaskDetailVideoMessage(bookTaskDetailId, videoMessage)
)
}

View File

@ -4,6 +4,7 @@ import { bookTaskDetailIpc } from './bookIPC/bookTaskDetailIpc'
import { bookPromptIpc } from './bookIPC/bookPromptIpc'
import { bookImageIpc } from './bookIPC/bookImageIpc'
import { bookExportIpc } from './bookIPC/bookExportIpc'
import { bookVideoIpc } from './bookIPC/bookVideoIpc'
function BookIpc() {
// 小说数据相关
@ -23,6 +24,9 @@ function BookIpc() {
// 小说导出相关
bookExportIpc()
// 小说视频 相关
bookVideoIpc()
}
export default BookIpc

View File

@ -127,6 +127,12 @@ function SystemIpc() {
async (_, extensions?: string[]) => await electronInterface.SelectFolderOrFile(extensions)
)
/** 读取文本文件内容 */
ipcMain.handle(
DEFINE_STRING.SYSTEM.READ_TEXT_FILE,
async (_, filePath: string) => await electronInterface.ReadTextFile(filePath)
)
//#endregion
}

View File

@ -1,7 +1,7 @@
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { ipcMain } from 'electron'
import { TaskHandle } from '@/main/service/task'
import { BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
import { TaskModal } from '@/define/model/task'
let taskHandle: TaskHandle = new TaskHandle()
@ -15,23 +15,7 @@ function TaskIpc() {
/** 添加单个个任务 */
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
)
async (_, task: TaskModal.Task) => await taskHandle.AddOneTask(task)
)
/** 添加多个任务 */

View File

@ -138,9 +138,26 @@ const BOOK = {
DOWNLOAD_IMAGE_URL_AND_SPLIT: 'DOWNLOAD_IMAGE_URL_AND_SPLIT',
//#endregion
//#region 视频相关
//#region 导出相关
/** 添加剪映草稿 */
ADD_JIANYING_DRAFT: 'ADD_JIANYING_DRAFT'
ADD_JIANYING_DRAFT: 'ADD_JIANYING_DRAFT',
//#endregion
// #region 视频相关
/** 获取指定的条件的图转视频的数据,包含字批次 */
GET_VIDEO_BOOK_INFO_LIST: 'GET_VIDEO_BOOK_INFO_LIST',
/** 初始化视频消息数据 */
INIT_VIDEO_MESSAGE: 'INIT_VIDEO_MESSAGE',
/** 更新小说分镜的视频消息 */
UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE: 'UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE',
/** MJ VIDEO 图转视频返回前端数据任务 */
MJ_VIDEO_TO_VIDEO_RETURN: 'MJ_VIDEO_TO_VIDEO_RETURN'
//#endregion
}

View File

@ -48,6 +48,9 @@ const SYSTEM = {
/** 选择文件夹或指定后缀的文件 */
SELECT_FOLDER_OR_FILE: 'SELECT_FOLDER_OR_FILE',
/** 读取文本文件内容 */
READ_TEXT_FILE: 'READ_TEXT_FILE',
//#endregion
/** 打开指定的url */

View File

@ -24,6 +24,7 @@ declare namespace Book {
no?: number
name?: string
bookFolderPath?: string
children?: SelectBookTask[]
type?: BookType
oldVideoPath?: string
srtPath?: string
@ -141,6 +142,8 @@ declare namespace Book {
bookTaskId?: string
videoPath?: string // 视频地址
generateVideoPath?: string // 生成的视频地址
subVideoPath?: string[] // 生成的批次视频的地址
subVideoPathObject?: subVideoPathModel[] //生成视频的完成结构显示
audioPath?: string // 音频地址
draftDepend?: string // 草稿依赖
word?: string // 文案

View File

@ -1,6 +1,8 @@
import {
ImageToVideoModels,
KlingMode,
MJVideoBatchSize,
MJVideoType,
RunawayModel,
RunwaySeconds,
VideoModel,
@ -26,6 +28,9 @@ declare namespace BookTaskDetail {
runwayOptions?: string
lumaOptions?: string
klingOptions?: string
mjVideoOptions?: string
messageData?: string
videoUrls?: string[] // 视频地址数组
messageData?: string
}
@ -78,6 +83,66 @@ declare namespace BookTaskDetail {
callback_url?: string // 回调地址,可选,生成视频完成后,会向该地址发送通知
}
interface MjVideoOptions {
/**
* indextaskId必填
*/
action?: MJVideoAction
/**
*
*/
image: string
/**
*
*/
index?: number
/**
*
*/
motion: MJVideoMotion
/**
* True时
*/
noStorage?: boolean
/**
*
*/
notifyHook?: string
/** 提示词 */
prompt?: null | string
state?: string
/**
* ID
*/
taskId?: string
/**
*
*/
raw?: boolean
/**
* SD标清/HD高清
*/
videoType?: MJVideoType
/**
*
*/
batchSize?: MJVideoBatchSize
/**
*
*/
endImageUrl?: string
/**
*
*/
loop?: boolean
}
//#endregion
//#region 小说文案相关

13
src/define/model/book/bookVideo.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
declare namespace BookVideo {
/**
*
*
* @interface BookVideoInfoListQuertCondition
* @property {string?} [bookId] - ID
* @property {string?} [bookTaskId] - ID
*/
interface BookVideoInfoListQuertCondition {
bookId?: string // 小说ID
bookTaskId?: string // 小说任务ID
}
}

View File

@ -1,5 +1,7 @@
import { ImageToVideoCategory } from '@/define/data/imageData'
import { ImageGenerateMode, MJRobotType, MJSpeed } from '../data/mjData'
import { JianyingKeyFrameEnum } from '../enum/jianyingEnum'
import { ImageToVideoModels } from '@/define/enum/video'
declare namespace SettingModal {
//#region 基础设置
@ -24,7 +26,7 @@ declare namespace SettingModal {
hdScale: number
/** 默认图转视频方式 */
defaultImg2Video: ImageToVideoCategory
defaultImg2Video: ImageToVideoModels
/** 系统语言 */
language: string

View File

@ -21,6 +21,8 @@ declare namespace TaskModal {
startTime?: number
endTime?: number
messageName?: string
taskId?: string // 任务ID可能是第三方服务的任务ID
taskMessage?: string // 任务消息,可能是第三方服务的任务消息
}
interface TaskCondition {

View File

@ -1,3 +1,4 @@
import { t } from '@/i18n'
import { DEFINE_STRING } from './ipcDefineString'
import { TimeDelay } from './Tools/time'
@ -101,7 +102,7 @@ export class AsyncQueue {
if (this.showEndTime) {
this.global.wins[0].webContents.send(DEFINE_STRING.SYSTEM.SHOW_MESSAGE_DIALOG, {
code: 0,
message: '试用时间已到,请联系客服'
message: t('试用时间已到,请联系客服')
})
}
this.showEndTime = false
@ -134,7 +135,7 @@ export class AsyncQueue {
await TimeDelay(2000)
this.global.wins[0].webContents.send(DEFINE_STRING.SYSTEM.SHOW_MAIN_NOTIFICATION, {
code: 0,
message: `生图失败共5次尝试目前第${retryCount}`
message: t("生图失败共5次尝试目前第 {retryCount} 次", { retryCount })
})
task()
.then(() => {
@ -155,7 +156,11 @@ export class AsyncQueue {
if (errorFunc) {
errorFunc(
batchId,
`任务 ${taskId} 执行失败,错误信息:${error.toString()}开始清理整个批次批次ID${batchId}`
t("任务 {taskId} 执行失败,错误信息:{error}开始清理整个批次批次ID{batchId}", {
taskId,
error: error.message,
batchId
})
)
}
if (!this.manualMode) {
@ -175,7 +180,11 @@ export class AsyncQueue {
if (errorFunc) {
errorFunc(
batchId,
`任务 ${taskId} 执行失败,错误信息:${error.toString()}开始清理整个批次批次ID${batchId}`
t("任务 {taskId} 执行失败,错误信息:{error}开始清理整个批次批次ID{batchId}", {
taskId,
error: error.message,
batchId
})
)
}
if (!this.manualMode) {

716
src/i18n/README.md Normal file
View File

@ -0,0 +1,716 @@
# 多语言国际化系统 (i18n)
这是一个为 LaiTool Pro 设计的完整多语言国际化解决方案,支持对象键和字符串键两种使用方式,提供完整的代码提示和类型安全。
## ✨ 特性
- 🌍 **5种语言支持**:简体中文、繁体中文、英文、日文、韩文
- 🎯 **智能代码提示**:使用对象键获得完整的 IntelliSense 支持
- 🛡️ **类型安全**:完整的 TypeScript 支持,编译时检查
- 💰 **多货币格式化**:支持 CNY、USD、EUR、JPY、KRW 等
- 📅 **日期时间本地化**:根据语言自动格式化日期时间
- 🔄 **动态语言切换**:运行时无缝切换语言
- 💾 **自动持久化**:语言设置自动保存到本地存储
- 🔧 **向下兼容**:同时支持新旧两种 API 使用方式
- ⚡ **Electron 优化**:解决了 Electron 环境下的动态导入问题
## 🔧 最新修复 (2025年9月5日)
- ✅ **修复了 Electron 环境下的语言文件加载问题**
- ✅ **使用静态导入替代动态导入,避免路径解析错误**
- ✅ **优化了语言切换性能,现在是同步操作**
- ✅ **增强了错误处理和日志记录**
## 🏗️ 架构结构
```
src/i18n/
├── index.ts # 核心 I18nManager 类
├── main.ts # 统一导出入口
├── keys.ts # 翻译键对象定义(提供代码提示)
├── types.ts # TypeScript 类型定义
├── constants.ts # 常量和配置
├── locales/ # 语言包目录
│ ├── zh-cn.ts # 简体中文
│ ├── zh-tw.ts # 繁体中文
│ ├── en.ts # 英文
│ ├── ja.ts # 日文
│ └── ko.ts # 韩文
└── utils/
└── helpers.ts # 便捷工具函数
```
## 🚀 快速开始
### 基础用法
```typescript
import { t, menu, common, video, formatPrice } from '@/i18n/main'
// 🎯 推荐方式:使用对象键(有完整代码提示)
const title = t(menu.home) // ✅ 输入 "menu." 显示所有选项
const save = t(common.save) // ✅ 类型安全,编译时检查
const generate = t(video.generate) // ✅ 自动补全,不会拼写错误
// 传统方式:使用字符串键(仍然支持)
const titleStr = t('menu.home') // ⚠️ 没有代码提示
// 价格格式化
const price = formatPrice(99.99, 'CNY') // "¥99.99"
```
### 在 Vue 组件中使用
```vue
<template>
<div>
<h1>{{ t(menu.home) }}</h1>
<button @click="save">{{ t(common.save) }}</button>
<button @click="cancel">{{ t(common.cancel) }}</button>
<p>{{ formatPrice(99.99, 'CNY') }}</p>
</div>
</template>
<script setup>
import { t, formatPrice, menu, common } from '@/i18n/main'
function save() {
console.log(t(common.loading))
}
function cancel() {
console.log(t(common.cancelled))
}
</script>
```
## 📚 可用的键对象
导入键对象后,您可以享受完整的代码提示:
```typescript
import {
menu, // 菜单相关
common, // 通用操作
video, // 视频功能
settings, // 设置选项
price, // 价格相关
task, // 任务管理
file, // 文件操作
project, // 项目管理
error, // 错误信息
validation // 表单验证
} from '@/i18n/main'
// 使用示例
const homeTitle = t(menu.home) // "首页"
const saveButton = t(common.save) // "保存"
const videoGenerate = t(video.generate) // "生成视频"
const generalSettings = t(settings.general) // "通用设置"
```
## 🛠️ API 参考
### 核心翻译函数
#### `t(key, params?)`
支持对象键和字符串键的翻译函数
```typescript
// 对象键方式(推荐)
t(menu.home) // "首页"
t(common.welcome, { name: '张三' }) // "欢迎 张三"
// 字符串键方式
t('menu.home') // "首页"
t('common.welcome', { name: '张三' }) // "欢迎 张三"
```
### 格式化函数
#### `formatPrice(amount, currency?)`
价格格式化
```typescript
formatPrice(99.99, 'CNY') // "¥99.99"
formatPrice(99.99, 'USD') // "$99.99"
formatPrice(99.99, 'EUR') // "€99.99"
formatPrice(99.99, 'JPY') // "¥100"
formatPrice(99.99, 'KRW') // "₩100"
```
#### `formatNumber(number, type?, locale?)`
数字格式化
```typescript
formatNumber(1234.56, 'decimal') // "1,234.56"
formatNumber(0.856, 'percent') // "85.6%"
formatNumber(1234, 'integer') // "1,234"
formatNumber(99.99, 'currency') // "¥99.99"
```
#### `formatDate(date, type?, locale?)`
日期格式化
```typescript
formatDate(new Date(), 'short') // "2025/9/5"
formatDate(new Date(), 'medium') // "2025年9月5日"
formatDate(new Date(), 'long') // "2025年9月5日 星期四"
formatDate(new Date(), 'time') // "14:30:45"
formatDate(new Date(), 'dateTime') // "2025年9月5日 14:30"
```
### 语言管理
#### `switchLocale(locale)`
切换语言
```typescript
await switchLocale('en') // 切换到英文
await switchLocale('zh-tw') // 切换到繁体中文
await switchLocale('ja') // 切换到日文
```
#### `getCurrentLocale()`
获取当前语言
```typescript
const currentLang = getCurrentLocale() // "zh-cn"
```
#### `getSupportedLocales()`
获取支持的语言列表
```typescript
const languages = getSupportedLocales()
// ['zh-cn', 'zh-tw', 'en', 'ja', 'ko']
```
#### `detectAndSetLanguage()`
自动检测并设置语言
```typescript
const detectedLang = await detectAndSetLanguage()
```
## 🌍 支持的语言
| 语言代码 | 语言名称 | 默认货币 | 示例 |
|---------|---------|---------|------|
| zh-cn | 简体中文 | CNY (¥) | 首页 |
| zh-tw | 繁体中文 | TWD (NT$) | 首頁 |
| en | English | USD ($) | Home |
| ja | 日本語 | JPY (¥) | ホーム |
| ko | 한국어 | KRW (₩) | 홈 |
## 💰 货币格式化
| 货币代码 | 符号 | 示例输出 |
|---------|-----|----------|
| CNY | ¥ | ¥99.99 |
| USD | $ | $99.99 |
| EUR | € | €99.99 |
| JPY | ¥ | ¥100 |
| KRW | ₩ | ₩100 |
| TWD | NT$ | NT$100 |
## 🎯 最佳实践
### ✅ 推荐做法
1. **使用对象键获得代码提示**
```typescript
import { t, menu, common } from '@/i18n/main'
const title = t(menu.home) // ✅ 有完整提示
const save = t(common.save) // ✅ 类型安全
```
2. **在组件中使用 computed**
```typescript
const pageTitle = computed(() => t(menu.home))
const buttonText = computed(() => t(common.save))
```
3. **创建翻译 Hook**
```typescript
function usePageTranslations() {
return {
title: computed(() => t(menu.home)),
buttons: {
save: computed(() => t(common.save)),
cancel: computed(() => t(common.cancel))
}
}
}
```
### ❌ 避免的做法
1. **不要使用魔法字符串**
```typescript
const title = t('menu.home') // ❌ 容易出错,没有提示
```
2. **不要在模板中直接使用字符串键**
```vue
<!-- ❌ 不推荐 -->
<h1>{{ t('menu.home') }}</h1>
<!-- ✅ 推荐 -->
<h1>{{ t(menu.home) }}</h1>
```
## 🔧 开发指南
### 添加新的翻译键
1. **在 `keys.ts` 中添加键定义**
```typescript
export const newCategory = {
newKey: 'newCategory.newKey'
} as const
```
2. **在所有语言文件中添加翻译**
```typescript
// zh-cn.ts
newCategory: {
newKey: '新功能'
}
// en.ts
newCategory: {
newKey: 'New Feature'
}
```
3. **在 `main.ts` 中导出**
```typescript
export { newCategory } from './keys'
```
### 测试多语言功能
```typescript
import { switchLocale, t, menu } from '@/i18n/main'
// 测试所有语言
const languages = ['zh-cn', 'zh-tw', 'en', 'ja', 'ko']
for (const lang of languages) {
await switchLocale(lang)
console.log(`${lang}: ${t(menu.home)}`)
}
```
## 🐛 故障排除
### 常见问题
**Q: 代码提示不显示?**
A: 确保正确导入了键对象:`import { menu, common } from '@/i18n/main'`
**Q: 翻译显示为键名?**
A: 检查语言文件中是否包含对应的键,确保所有语言文件结构一致。
**Q: 语言切换后页面没更新?**
A: 在 Vue 组件中使用 `computed(() => t(key))` 确保响应式更新。
**Q: TypeScript 报错键不存在?**
A: 检查是否在 `keys.ts` 中定义了对应的键,并在 `main.ts` 中正确导出。
## 📖 使用示例
### 完整的多语言页面
```vue
<template>
<div class="page">
<header>
<h1>{{ t(menu.home) }}</h1>
<select @change="changeLanguage" v-model="currentLang">
<option value="zh-cn">简体中文</option>
<option value="zh-tw">繁体中文</option>
<option value="en">English</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
</select>
</header>
<main>
<div class="actions">
<button @click="save">{{ t(common.save) }}</button>
<button @click="cancel">{{ t(common.cancel) }}</button>
<button @click="generateVideo">{{ t(video.generate) }}</button>
</div>
<div class="price">
<p>{{ t(price.currentPrice) }}: {{ formatPrice(99.99, 'CNY') }}</p>
</div>
<div class="status">
<p v-if="loading">{{ t(common.loading) }}</p>
<p v-if="success">{{ t(common.success) }}</p>
<p v-if="error">{{ t(common.error) }}</p>
</div>
</main>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import {
t,
formatPrice,
switchLocale,
getCurrentLocale,
menu,
common,
video,
price
} from '@/i18n/main'
const currentLang = ref(getCurrentLocale())
const loading = ref(false)
const success = ref(false)
const error = ref(false)
async function changeLanguage(event) {
const newLang = event.target.value
await switchLocale(newLang)
currentLang.value = newLang
}
function save() {
loading.value = true
setTimeout(() => {
loading.value = false
success.value = true
}, 1000)
}
function cancel() {
console.log(t(common.cancel))
}
function generateVideo() {
console.log(t(video.generate))
}
</script>
```
这就是您的多语言系统的完整使用指南!🎉
## 🔀 翻译文本合并功能
新增了翻译文本合并功能,可以根据不同语言使用相应的分隔符来合并多个翻译文本。
### 主要功能
#### 1. `mergeTranslations` - 合并两个翻译
合并两个翻译键对应的文本,支持自定义分隔符和结尾符号。
```typescript
import { mergeTranslations } from '@/i18n'
// 基本使用
const result = mergeTranslations('common.save', 'common.cancel')
// 中文: "保存,取消"
// 英文: "Save, Cancel"
// 带选项使用
const result = mergeTranslations(
'video.generate',
'video.processing',
{
separator: 'comma', // 分隔符类型
suffix: 'period', // 结尾符号
locale: 'zh-cn' // 指定语言
}
)
// 结果: "生成视频,处理中。"
```
#### 2. `mergeMultipleTranslations` - 合并多个翻译
合并多个翻译键对应的文本。
```typescript
import { mergeMultipleTranslations } from '@/i18n'
const result = mergeMultipleTranslations(
['common.save', 'common.edit', 'common.delete'],
{
separator: 'comma',
suffix: 'period'
}
)
// 中文: "保存,编辑,删除。"
// 英文: "Save, Edit, Delete."
```
#### 3. `getLocaleSeparators` - 获取语言分隔符
获取指定语言的分隔符配置。
```typescript
import { getLocaleSeparators } from '@/i18n'
const separators = getLocaleSeparators('zh-cn')
// 返回: {
// comma: '', semicolon: '', period: '。', colon: '', space: ' ',
// exclamation: '', question: '', ellipsis: '……'
// }
```
### 配置选项
#### MergeTranslationOptions
```typescript
interface MergeTranslationOptions {
separator?: SeparatorType | string // 分隔符
suffix?: SeparatorType | string | null // 结尾符号
locale?: string // 指定语言
}
```
#### 支持的分隔符类型
- `comma` - 逗号分隔
- `semicolon` - 分号分隔
- `period` - 句号分隔
- `colon` - 冒号分隔
- `space` - 空格分隔
- `exclamation` - 叹号分隔 🆕
- `question` - 问号分隔 🆕
- `ellipsis` - 省略号分隔 🆕
- 或者直接传入自定义字符串
#### 🎯 推荐使用 Separators 常量(智能提示)🆕
为了获得更好的开发体验,我们提供了 `Separators` 常量对象:
```typescript
import { mergeTranslations, Separators } from '@/i18n'
// ✅ 推荐方式:使用 Separators 常量(有完整智能提示)
const result = mergeTranslations('common.save', 'common.cancel', {
separator: Separators.comma, // 输入 Separators. 显示所有选项
suffix: Separators.period // 每个选项都有详细注释说明
})
// ❌ 传统方式:使用字符串(容易拼错,没有提示)
const oldWay = mergeTranslations('common.save', 'common.cancel', {
separator: 'comma', // 没有智能提示
suffix: 'peroid' // 可能拼写错误
})
```
**Separators 常量的优势:**
- ✅ 完整的智能提示和自动补全
- ✅ 类型安全,编译时检查错误
- ✅ 每个选项都有详细注释说明效果
- ✅ 重构友好,修改时自动更新引用
- ✅ 更好的代码可读性和维护性
### 语言分隔符支持
目前支持以下语言的专用分隔符:
| 语言 | 逗号 | 分号 | 句号 | 冒号 | 空格 | 叹号 | 问号 | 省略号 |
|------|------|------|------|------|------|------|------|--------|
| zh-cn | | | 。 | | (空格) | | | …… |
| zh-tw | | | 。 | | (空格) | | | …… |
| en | , (带空格) | ; (带空格) | . (带空格) | : (带空格) | (空格) | ! (带空格) | ? (带空格) | ... |
| ja | 、 | | 。 | | (空格) | | | …… |
| ko | , (带空格) | ; (带空格) | . (带空格) | : (带空格) | (空格) | ! (带空格) | ? (带空格) | ... |
### 合并功能使用场景
#### 🎯 推荐使用方式(使用 Separators 常量)
```typescript
import { mergeTranslations, mergeMultipleTranslations, Separators } from '@/i18n'
// ✅ 组合操作按钮(智能提示)
const actionButtons = mergeMultipleTranslations(
['common.save', 'common.edit', 'common.delete'],
{ separator: Separators.space } // 输入 Separators. 显示所有可用选项
)
// ✅ 成功提示(叹号)
const successMessage = mergeTranslations(
'common.success',
'file.upload',
{
separator: Separators.exclamation,
suffix: Separators.exclamation
}
)
// ✅ 确认对话框(问号)
const confirmDialog = mergeTranslations(
'common.confirm',
'common.delete',
{
separator: Separators.space,
suffix: Separators.question
}
)
// ✅ 加载状态(省略号)
const loadingStatus = mergeTranslations(
'common.loading',
'video.processing',
{ separator: Separators.ellipsis }
)
```
#### 传统使用方式(仍然支持)
#### 1. 组合操作按钮
```typescript
const actionButtons = mergeMultipleTranslations(
['common.save', 'common.edit', 'common.delete'],
{ separator: 'space' }
)
```
#### 2. 文件信息显示
```typescript
const fileInfo = mergeTranslations(
'file.size',
'file.type',
{ separator: 'comma' }
)
```
#### 3. 任务状态
```typescript
const taskStatus = mergeTranslations(
'task.status',
'task.progress',
{ separator: 'colon' }
)
```
#### 4. 错误信息
```typescript
const errorMessage = mergeTranslations(
'error.uploadFailed',
'common.error_information',
{ separator: 'period', suffix: 'period' }
)
```
#### 5. 带参数的翻译合并
```typescript
const validation = mergeTranslations(
'validation.minLength',
'validation.maxLength',
{ separator: 'semicolon' },
{ min: 3 }, // 第一个翻译的参数
{ max: 20 } // 第二个翻译的参数
)
```
#### 6. 新增标点符号使用场景 🆕
```typescript
// 成功提示(叹号)
const successMessage = mergeTranslations(
'common.success',
'file.upload',
{ separator: 'exclamation', suffix: 'exclamation' }
)
// 中文: "成功!上传文件!" 英文: "Success! Upload File!"
// 确认对话框(问号)
const confirmDialog = mergeTranslations(
'common.confirm',
'common.delete',
{ separator: 'space', suffix: 'question' }
)
// 中文: "确认 删除?" 英文: "Confirm Delete?"
// 加载状态(省略号)
const loadingStatus = mergeTranslations(
'common.loading',
'video.processing',
{ separator: 'ellipsis', suffix: null }
)
// 中文: "加载中……处理中" 英文: "Loading...Processing"
// 警告信息(组合使用)
const warningMessage = mergeMultipleTranslations(
['common.warning', 'error.network', 'common.retry'],
{ separator: 'exclamation', suffix: 'question' }
)
// 中文: "警告!网络错误!重试?" 英文: "Warning! Network Error! Retry?"
```
### 合并功能注意事项
1. 如果指定的语言不存在分隔符配置,会回退到英文配置
2. `suffix` 设置为 `null` 时不添加结尾符号
3. 自定义分隔符会直接使用传入的字符串,不会根据语言调整
4. 函数会自动获取当前语言设置,也可以通过 `locale` 参数强制指定
### 完整的合并功能示例
```vue
<template>
<div class="merge-demo">
<h2>{{ t(menu.home) }}</h2>
<!-- 基本合并 -->
<p>{{ mergeTranslations('common.save', 'common.cancel') }}</p>
<!-- 多个合并 -->
<p>{{ fileOperations }}</p>
<!-- 带结尾符号 -->
<p>{{ taskInfo }}</p>
<!-- 自定义分隔符 -->
<p>{{ customSeparator }}</p>
</div>
</template>
<script setup>
import { computed } from 'vue'
import {
t,
mergeTranslations,
mergeMultipleTranslations,
menu
} from '@/i18n/main'
// 文件操作组合
const fileOperations = computed(() =>
mergeMultipleTranslations(
['file.upload', 'file.download', 'file.delete'],
{ separator: 'comma', suffix: 'period' }
)
)
// 任务信息
const taskInfo = computed(() =>
mergeTranslations(
'task.status',
'task.running',
{ separator: 'colon' }
)
)
// 自定义分隔符
const customSeparator = computed(() =>
mergeTranslations(
'book.title',
'book.author',
{ separator: ' | ', suffix: null }
)
)
</script>
```
这个完整的多语言系统现在包含了基础翻译功能和高级的文本合并功能!🚀

211
src/i18n/constants.ts Normal file
View File

@ -0,0 +1,211 @@
import type { LocaleConfig, SupportedLocale, DateTimeFormats, NumberFormats, PriceConfig } from './types'
/**
*
*/
export const SUPPORTED_LOCALES: LocaleConfig[] = [
{
code: 'zh-cn',
name: '简体中文',
flag: '🇨🇳',
currency: '¥',
currencyCode: 'CNY'
},
{
code: 'zh-tw',
name: '繁體中文',
flag: '🇹🇼',
currency: 'NT$',
currencyCode: 'TWD'
},
{
code: 'en',
name: 'English',
flag: '🇺🇸',
currency: '$',
currencyCode: 'USD'
},
{
code: 'ja',
name: '日本語',
flag: '🇯🇵',
currency: '¥',
currencyCode: 'JPY'
},
{
code: 'ko',
name: '한국어',
flag: '🇰🇷',
currency: '₩',
currencyCode: 'KRW'
}
]
/**
*
*/
export const DEFAULT_LOCALE: SupportedLocale = 'zh-cn'
/**
*
*/
export const FALLBACK_LOCALE: SupportedLocale = 'en'
/**
*
*/
export const PRICE_CONFIGS: Record<SupportedLocale, PriceConfig> = {
'zh-cn': {
symbol: '¥',
code: 'CNY',
decimal: 2,
thousands: ',',
decimalSeparator: '.',
symbolPosition: 'before'
},
'zh-tw': {
symbol: 'NT$',
code: 'TWD',
decimal: 0,
thousands: ',',
decimalSeparator: '.',
symbolPosition: 'before'
},
'en': {
symbol: '$',
code: 'USD',
decimal: 2,
thousands: ',',
decimalSeparator: '.',
symbolPosition: 'before'
},
'ja': {
symbol: '¥',
code: 'JPY',
decimal: 0,
thousands: ',',
decimalSeparator: '.',
symbolPosition: 'before'
},
'ko': {
symbol: '₩',
code: 'KRW',
decimal: 0,
thousands: ',',
decimalSeparator: '.',
symbolPosition: 'before'
}
}
/**
*
*/
export const DATETIME_FORMATS: DateTimeFormats = {
'zh-cn': {
short: { year: 'numeric', month: '2-digit', day: '2-digit' },
medium: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
time: { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false },
dateTime: {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}
},
'zh-tw': {
short: { year: 'numeric', month: '2-digit', day: '2-digit' },
medium: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
time: { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false },
dateTime: {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}
},
'en': {
short: { year: 'numeric', month: '2-digit', day: '2-digit' },
medium: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
time: { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true },
dateTime: {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: true
}
},
'ja': {
short: { year: 'numeric', month: '2-digit', day: '2-digit' },
medium: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
time: { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false },
dateTime: {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}
},
'ko': {
short: { year: 'numeric', month: '2-digit', day: '2-digit' },
medium: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' },
time: { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false },
dateTime: {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}
}
}
/**
*
*/
export const NUMBER_FORMATS: NumberFormats = {
'zh-cn': {
currency: { style: 'currency', currency: 'CNY', currencyDisplay: 'symbol' },
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1 },
integer: { minimumFractionDigits: 0, maximumFractionDigits: 0 }
},
'zh-tw': {
currency: { style: 'currency', currency: 'TWD', currencyDisplay: 'symbol' },
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1 },
integer: { minimumFractionDigits: 0, maximumFractionDigits: 0 }
},
'en': {
currency: { style: 'currency', currency: 'USD', currencyDisplay: 'symbol' },
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1 },
integer: { minimumFractionDigits: 0, maximumFractionDigits: 0 }
},
'ja': {
currency: { style: 'currency', currency: 'JPY', currencyDisplay: 'symbol' },
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1 },
integer: { minimumFractionDigits: 0, maximumFractionDigits: 0 }
},
'ko': {
currency: { style: 'currency', currency: 'KRW', currencyDisplay: 'symbol' },
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1 },
integer: { minimumFractionDigits: 0, maximumFractionDigits: 0 }
}
}

267
src/i18n/index.ts Normal file
View File

@ -0,0 +1,267 @@
import type { SupportedLocale, I18nMessage, PriceConfig } from './types'
import {
DEFAULT_LOCALE,
FALLBACK_LOCALE,
SUPPORTED_LOCALES,
PRICE_CONFIGS,
DATETIME_FORMATS,
NUMBER_FORMATS
} from './constants'
// 静态导入所有语言文件
import zhCnMessages from './locales/zh-cn'
import zhTwMessages from './locales/zh-tw'
import enMessages from './locales/en'
import jaMessages from './locales/ja'
import koMessages from './locales/ko'
// 语言文件映射
const LOCALE_MESSAGES: Record<SupportedLocale, I18nMessage> = {
'zh-cn': zhCnMessages,
'zh-tw': zhTwMessages,
en: enMessages,
ja: jaMessages,
ko: koMessages
}
/**
*
*/
export class I18nManager {
private static instance: I18nManager | null = null
private currentLocale: SupportedLocale = DEFAULT_LOCALE
private messages: Record<SupportedLocale, I18nMessage> = {} as any
// private fallbackMessages: I18nMessage = {}
private constructor() {
this.init()
}
/**
*
*/
public static getInstance(): I18nManager {
if (!I18nManager.instance) {
I18nManager.instance = new I18nManager()
}
return I18nManager.instance
}
/**
*
*/
private async init() {
// 加载默认语言包
this.loadLocale(DEFAULT_LOCALE)
this.loadLocale(FALLBACK_LOCALE)
// this.fallbackMessages = this.messages[FALLBACK_LOCALE] || {}
}
/**
*
*/
public loadLocale(locale: SupportedLocale) {
try {
const messages = LOCALE_MESSAGES[locale]
if (messages) {
this.messages[locale] = messages
} else {
console.warn(`Unsupported locale: ${locale}`)
}
} catch (error) {
console.warn(`Failed to load locale: ${locale}`, error)
}
}
/**
*
*/
public setLocale(locale: SupportedLocale) {
if (!this.messages[locale]) {
this.loadLocale(locale)
}
this.currentLocale = locale
// 保存到本地存储
if (typeof localStorage !== 'undefined') {
localStorage.setItem('app-locale', locale)
}
}
/**
*
*/
public getLocale(): SupportedLocale {
return this.currentLocale
}
/**
*
*
* t('menu.home') t(menu.home)
*/
public t(key: string, params?: Record<string, any>): string {
const keyString = typeof key === 'string' ? key : String(key)
const message =
this.getMessage(keyString, this.currentLocale) ||
this.getMessage(keyString, FALLBACK_LOCALE) ||
keyString
return params ? this.interpolate(message, params) : message
}
/**
*
*
*/
public tl(
key: string,
locale: SupportedLocale,
params?: Record<string, any>
): string {
const keyString = typeof key === 'string' ? key : String(key)
const message =
this.getMessage(keyString, locale) || this.getMessage(keyString, FALLBACK_LOCALE) || keyString
return params ? this.interpolate(message, params) : message
}
/**
*
*/
private getMessage(key: string, locale: SupportedLocale): string {
const messages = this.messages[locale]
if (!messages) return ''
let result: any = messages
if (result && typeof result === 'object' && key in result) {
result = result[key]
} else {
return key
}
return typeof result === 'string' ? result : ''
}
/**
*
*/
private interpolate(message: string, params: Record<string, any>): string {
return message.replace(/\{(\w+)\}/g, (match, key) => {
return params[key] !== undefined ? String(params[key]) : match
})
}
/**
*
*/
public formatPrice(amount: number, locale?: SupportedLocale): string {
const targetLocale = locale || this.currentLocale
const config = PRICE_CONFIGS[targetLocale]
if (!config) return String(amount)
const formatter = new Intl.NumberFormat(targetLocale, {
style: 'currency',
currency: config.code,
minimumFractionDigits: config.decimal,
maximumFractionDigits: config.decimal
})
return formatter.format(amount)
}
/**
*
*/
public formatNumber(
number: number,
type: 'currency' | 'decimal' | 'percent' | 'integer' = 'decimal',
locale?: SupportedLocale
): string {
const targetLocale = locale || this.currentLocale
const formats = NUMBER_FORMATS[targetLocale]
if (!formats || !formats[type]) return String(number)
const formatter = new Intl.NumberFormat(targetLocale, formats[type])
return formatter.format(number)
}
/**
*
*/
public formatDate(
date: Date,
type: 'short' | 'medium' | 'long' | 'time' | 'dateTime' = 'medium',
locale?: SupportedLocale
): string {
const targetLocale = locale || this.currentLocale
const formats = DATETIME_FORMATS[targetLocale]
if (!formats || !formats[type]) return date.toLocaleDateString()
const formatter = new Intl.DateTimeFormat(targetLocale, formats[type])
return formatter.format(date)
}
/**
*
*/
public getSupportedLocales() {
return SUPPORTED_LOCALES
}
/**
*
*/
public getPriceConfig(locale?: SupportedLocale): PriceConfig {
return PRICE_CONFIGS[locale || this.currentLocale]
}
/**
*
*/
public detectBrowserLanguage(): SupportedLocale {
if (typeof navigator === 'undefined') return DEFAULT_LOCALE
const browserLang = navigator.language.toLowerCase()
// 精确匹配
for (const locale of SUPPORTED_LOCALES) {
if (browserLang === locale.code) {
return locale.code
}
}
// 语言代码匹配(如 zh-cn 匹配 zh
const langCode = browserLang.split('-')[0]
for (const locale of SUPPORTED_LOCALES) {
if (locale.code.startsWith(langCode)) {
return locale.code
}
}
return DEFAULT_LOCALE
}
/**
*
*/
public loadFromStorage(): SupportedLocale {
if (typeof localStorage === 'undefined') return DEFAULT_LOCALE
const saved = localStorage.getItem('app-locale') as SupportedLocale
if (saved && SUPPORTED_LOCALES.some((l) => l.code === saved)) {
return saved
}
return this.detectBrowserLanguage()
}
}
// 导出工具函数
export * from './utils/helpers'
export * from './types'
export * from './constants'

1846
src/i18n/locales/en.ts Normal file

File diff suppressed because it is too large Load Diff

1
src/i18n/locales/ja.ts Normal file
View File

@ -0,0 +1 @@
export default {}

1
src/i18n/locales/ko.ts Normal file
View File

@ -0,0 +1 @@
export default {}

1846
src/i18n/locales/zh-cn.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
export default {}

50
src/i18n/main.ts Normal file
View File

@ -0,0 +1,50 @@
// 导出核心类型
export type { SupportedLocale, LocaleConfig, I18nMessage, PriceConfig, I18nConfig } from './types'
// 导出常量
export {
SUPPORTED_LOCALES,
DEFAULT_LOCALE,
FALLBACK_LOCALE,
PRICE_CONFIGS,
DATETIME_FORMATS,
NUMBER_FORMATS,
} from './constants'
// 导出核心管理器
export { I18nManager } from './index'
// 导出工具函数
export {
useI18n,
t,
formatPrice,
formatNumber,
formatDate,
switchLocale,
getCurrentLocale,
getSupportedLocales,
detectAndSetLanguage
} from './utils/helpers'
// 导出语言包
import zhCN from './locales/zh-cn'
// import zhTW from './locales/zh-tw'
import en from './locales/en'
// import ja from './locales/ja'
// import ko from './locales/ko'
export const locales = {
'zh-cn': zhCN,
// 'zh-tw': zhTW,
en: en,
// ja: ja,
// ko: ko
}
// 默认导出 i18n 实例
import { I18nManager } from './index'
export const i18nManager = I18nManager.getInstance()
export default i18nManager

79
src/i18n/types.ts Normal file
View File

@ -0,0 +1,79 @@
/**
*
*/
export type SupportedLocale = 'zh-cn' | 'zh-tw' | 'en' | 'ja' | 'ko'
export interface LocaleConfig {
/** 语言代码 */
code: SupportedLocale
/** 语言名称 */
name: string
/** 语言标识符emoji 或图标) */
flag: string
/** 是否为从右到左的语言 */
rtl?: boolean
/** 货币符号 */
currency: string
/** 货币代码 */
currencyCode: string
}
export interface TranslationKey {
[key: string]: string | TranslationKey
}
export interface I18nMessage {
[key: string]: string | I18nMessage
}
export interface PriceConfig {
/** 货币符号 */
symbol: string
/** 货币代码 */
code: string
/** 小数位数 */
decimal: number
/** 千分位分隔符 */
thousands: string
/** 小数点分隔符 */
decimalSeparator: string
/** 货币符号位置 */
symbolPosition: 'before' | 'after'
}
export interface DateTimeFormats {
[locale: string]: {
short: Intl.DateTimeFormatOptions
medium: Intl.DateTimeFormatOptions
long: Intl.DateTimeFormatOptions
time: Intl.DateTimeFormatOptions
dateTime: Intl.DateTimeFormatOptions
}
}
export interface NumberFormats {
[locale: string]: {
currency: Intl.NumberFormatOptions
decimal: Intl.NumberFormatOptions
percent: Intl.NumberFormatOptions
integer: Intl.NumberFormatOptions
}
}
export interface I18nConfig {
/** 当前语言 */
locale: SupportedLocale
/** 备用语言 */
fallbackLocale: SupportedLocale
/** 可用语言列表 */
availableLocales: LocaleConfig[]
/** 是否检测浏览器语言 */
detectBrowserLanguage: boolean
/** 日期时间格式 */
datetimeFormats: DateTimeFormats
/** 数字格式 */
numberFormats: NumberFormats
/** 价格配置 */
priceConfigs: Record<SupportedLocale, PriceConfig>
}

299
src/i18n/utils/helpers.ts Normal file
View File

@ -0,0 +1,299 @@
import { I18nManager, SupportedLocale } from '../index'
/**
* -
* 使用方式: Separators.comma, Separators.period
*/
export const Separators = {
/** 逗号分隔符:中文() 英文(, ) */
comma: 'comma' as const,
/** 分号分隔符:中文() 英文(; ) */
semicolon: 'semicolon' as const,
/** 句号分隔符:中文(。) 英文(. ) */
period: 'period' as const,
/** 冒号分隔符:中文() 英文(: ) */
colon: 'colon' as const,
/** 空格分隔符:所有语言( ) */
space: 'space' as const,
/** 叹号分隔符:中文() 英文(! ) */
exclamation: 'exclamation' as const,
/** 问号分隔符:中文() 英文(? ) */
question: 'question' as const,
/** 省略号分隔符:中文(……) 英文(...) */
ellipsis: 'ellipsis' as const
} as const
/**
* - Separators
*/
export type SeparatorType = keyof typeof Separators
/**
*
*
*/
export interface LocaleSeparatorConfig {
comma: string // 逗号:用于列举、分隔项目
semicolon: string // 分号:用于分隔相关但独立的语句
period: string // 句号:用于结束句子
colon: string // 冒号:用于引出说明、列表等
space: string // 空格:用于分隔单词或元素
exclamation: string // 叹号:表示强调、惊叹、感叹
question: string // 问号:表示疑问、询问
ellipsis: string // 省略号:表示省略、延续或停顿
}
/**
*
*
*/
export interface MergeTranslationOptions {
/** 分隔符类型:用于连接两个翻译文本的标点符号,默认为 'comma' */
separator?: SeparatorType | string
/** 结尾符号:添加到合并结果末尾的标点符号,设为 null 则不添加结尾符号 */
suffix?: SeparatorType | string
/** 指定语言:强制使用特定语言的分隔符,不指定则使用当前语言设置 */
locale?: SupportedLocale
}
/**
* i18n
*/
export function useI18n() {
return I18nManager.getInstance()
}
/**
*
* @param key 'common.welcome' 'validation.minLength'
* @param params
* : { variableName: value }
* 使 {variableName}
* @returns
*
* @example
* // 基本使用
* t('common.save') // "保存"
*
* // 带参数使用
* t('common.welcome', { name: '张三' }) // "欢迎 张三"
* t('validation.minLength', { min: 3 }) // "至少需要 3 个字符"
* t('file.size', { size: '10MB', type: 'jpg' }) // "文件大小: 10MB, 类型: jpg"
*/
export function t(key: string, params?: Record<string, any>): string {
return useI18n().t(key, params)
}
/**
*
*/
export function formatPrice(amount: number, locale?: string): string {
return useI18n().formatPrice(amount, locale as any)
}
/**
*
*/
export function formatNumber(
number: number,
type: 'currency' | 'decimal' | 'percent' | 'integer' = 'decimal',
locale?: string
): string {
return useI18n().formatNumber(number, type, locale as any)
}
/**
*
*/
export function formatDate(
date: Date,
type: 'short' | 'medium' | 'long' | 'time' | 'dateTime' = 'medium',
locale?: string
): string {
return useI18n().formatDate(date, type, locale as any)
}
/**
*
*/
export async function switchLocale(locale: string) {
const i18n = useI18n()
i18n.setLocale(locale as any)
// 触发全局事件,通知应用语言已更改
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('locale-changed', { detail: locale }))
}
}
/**
*
*/
export function getCurrentLocale(): string {
return useI18n().getLocale()
}
/**
*
*/
export function getSupportedLocales() {
return useI18n().getSupportedLocales()
}
/**
*
*/
export async function detectAndSetLanguage() {
const i18n = useI18n()
const detectedLocale = i18n.loadFromStorage()
i18n.setLocale(detectedLocale)
return detectedLocale
}
/**
*
*/
export function getLocaleSeparators(locale?: string): LocaleSeparatorConfig {
const currentLocale = locale || getCurrentLocale()
// 根据不同语言返回不同的分隔符
const separatorConfig: Record<string, LocaleSeparatorConfig> = {
'zh-cn': {
comma: '',
semicolon: '',
period: '。',
colon: '',
space: ' ',
exclamation: '',
question: '',
ellipsis: '……'
},
'zh-tw': {
comma: '',
semicolon: '',
period: '。',
colon: '',
space: ' ',
exclamation: '',
question: '',
ellipsis: '……'
},
en: {
comma: ', ',
semicolon: '; ',
period: '. ',
colon: ': ',
space: ' ',
exclamation: '! ',
question: '? ',
ellipsis: '...'
},
ja: {
comma: '、',
semicolon: '',
period: '。',
colon: '',
space: ' ',
exclamation: '',
question: '',
ellipsis: '……'
},
ko: {
comma: ', ',
semicolon: '; ',
period: '. ',
colon: ': ',
space: ' ',
exclamation: '! ',
question: '? ',
ellipsis: '...'
}
}
return separatorConfig[currentLocale] || separatorConfig['en']
}
/**
*
* @param key1
* @param key2
* @param options
* @param params1
* @param params2
*/
export function mergeTranslations(
key1: string,
key2: string,
options: MergeTranslationOptions = {},
params1?: Record<string, any>,
params2?: Record<string, any>
): string {
const { separator = 'comma', suffix = null, locale } = options
// 获取翻译文本
const text1 = t(key1, params1)
const text2 = t(key2, params2)
// 获取当前语言的分隔符配置
const separators = getLocaleSeparators(locale)
// 获取分隔符
const getSeparator = (type: string) => {
if (type in separators) {
return separators[type]
}
return type // 如果不是预定义类型,直接使用传入的字符串
}
let result = text1 + getSeparator(separator) + text2
// 添加结尾符号
if (suffix !== null) {
result += getSeparator(suffix)
}
return result
}
/**
*
* @param keys
* @param options
* @param paramsArray keys一一对应
*/
export function mergeMultipleTranslations(
keys: string[],
options: MergeTranslationOptions = {},
paramsArray?: Array<Record<string, any> | undefined>
): string {
const { separator = 'comma', suffix = null, locale } = options
if (keys.length === 0) return ''
// 获取所有翻译文本
const texts = keys.map((key, index) => {
const params = paramsArray?.[index]
return t(key, params)
})
// 获取当前语言的分隔符配置
const separators = getLocaleSeparators(locale)
// 获取分隔符
const getSeparator = (type: string) => {
if (type in separators) {
return separators[type]
}
return type
}
let result = texts.join(getSeparator(separator))
// 添加结尾符号
if (suffix !== null) {
result += getSeparator(suffix)
}
return result
}

View File

@ -6,6 +6,7 @@ import { AiReasonCommon } from '../aiReason/aiReasonCommon'
import { groupWordsByCharCount } from '@/define/Tools/write'
import { cloneDeep, isEmpty } from 'lodash'
import { OptionKeyName } from '@/define/enum/option'
import { t } from '@/i18n'
/**
* AI分镜处理类
@ -33,19 +34,18 @@ export class AIStoryboard extends BookBasicHandle {
await this.InitBookBasicHandle()
// 根据ID获取小说批次任务
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
if (!bookTask) throw new Error('小说批次任务未找到')
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId, true)
// 获取任务详情中的分镜数据
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTask.id
})
if (!bookTaskDetails || bookTaskDetails.length === 0) throw new Error('小说分镜信息未找到')
if (!bookTaskDetails || bookTaskDetails.length === 0) throw new Error(t('未找到对应的小说分镜信息'))
// 提取AI处理后的分镜文本
let word = bookTaskDetails.map((item) => item.afterGpt)
if (word == undefined || word.length === 0) throw new Error('分镜内容为空')
if (word == undefined || word.length === 0) throw new Error(t('所有分镜文案内容为空无法进行AI合并'))
// 将文本按500字符分组避免单次请求过长
let groupWord = groupWordsByCharCount(word as string[], 500)
@ -65,7 +65,7 @@ export class AIStoryboard extends BookBasicHandle {
} else if (type === 'short') {
request = cloneDeep(AIWordMergeShort)
} else {
throw new Error('不支持的分镜合并类型')
throw new Error(t('不支持的分镜合并类型'))
}
// 将当前分组的文本合并为一个字符串
@ -99,7 +99,6 @@ export class AIStoryboard extends BookBasicHandle {
result.push(res)
}
console.log('分镜合并结果:', result)
return result
} catch (error) {
throw error

View File

@ -9,6 +9,7 @@ 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'
import { t } from '@/i18n'
/**
* AI推理通用工具类
@ -55,7 +56,7 @@ export class AiReasonCommon {
let aiReasonSetting = optionSerialization<SettingModal.InferenceAISettings>(
res,
'设置-> 推理设置'
t('设置 -> 推理设置')
)
if (
isEmpty(aiReasonSetting.apiProvider) ||
@ -64,7 +65,9 @@ export class AiReasonCommon {
isEmpty(aiReasonSetting.aiPromptValue)
) {
throw new Error(
'请检查 ‘设置-> 推理设置’ 的API提供商、API令牌、推理模型、推理模式等是不是存在'
t("请检查 {path} 的API提供商、API令牌、推理模型、推理模式等是不是存在", {
path: t('设置 -> 推理设置')
})
)
}
@ -88,7 +91,7 @@ export class AiReasonCommon {
GetInferenceModelMessage(): AiInferenceModelModel {
let selectInferenceModel = GetAIPromptOptionByValue(this.aiReasonSetting.aiPromptValue)
if (isEmpty(selectInferenceModel)) {
throw new Error('请检查推理模型是否存在!')
throw new Error(t('请检查推理模型是否存在!'))
}
return selectInferenceModel
}
@ -270,12 +273,12 @@ export class AiReasonCommon {
)
if (isEmpty(characterString) && selectInferenceModel.mustCharacter) {
throw new Error('当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!')
throw new Error(t('当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!'))
}
let requestBody = selectInferenceModel.requestBody
if (requestBody == null) {
throw new Error('未找到对应的分镜预设的请求数据,请检查')
throw new Error(t('未找到对应的分镜预设的请求数据,请检查'))
}
requestBody.messages = this.replaceMessageObject(requestBody.messages, {

View File

@ -4,19 +4,18 @@
*
*
*/
import { BookVideoHandle } from "../subBookHandle/bookVideoHandle"
import { BookExportHandle } from '../subBookHandle/bookExportHandle'
export class BookExportEntrance {
bookVideoHandle: BookVideoHandle
bookExportHandle: BookExportHandle
constructor() {
this.bookVideoHandle = new BookVideoHandle()
this.bookExportHandle = new BookExportHandle()
}
//#region 视频相关
/** 添加剪映草稿 */
AddJianyingDraft = async (bookTaskId: string) =>
await this.bookVideoHandle.AddJianyingDraft(bookTaskId)
await this.bookExportHandle.AddJianyingDraft(bookTaskId)
//#endregion
}

View File

@ -0,0 +1,38 @@
import { TaskModal } from '@/define/model/task'
import { BookVideoServiceHandle } from '../subBookHandle/bookVideoServiceHandle'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
export class BookVideoEntrance {
bookVideoServiceHandle!: BookVideoServiceHandle
constructor() {
this.bookVideoServiceHandle = new BookVideoServiceHandle()
}
/**
* bookTaskId
*/
async GetVideoBookInfoList(condition: BookVideo.BookVideoInfoListQuertCondition) {
return this.bookVideoServiceHandle.GetVideoBookInfoList(condition)
}
/** 初始化视频消息数据 */
async InitVideoMessage(bookTaskId: string) {
return this.bookVideoServiceHandle.InitVideoMessage(bookTaskId)
}
/** 更新小说分镜的视频消息 */
async UpdateBookTaskDetailVideoMessage(
bookTaskDetailId: string,
videoMessage: Partial<BookTaskDetail.VideoMessage>
) {
return this.bookVideoServiceHandle.UpdateBookTaskDetailVideoMessage(
bookTaskDetailId,
videoMessage
)
}
/** 图文转视频 */
async MediaToVideo(task: TaskModal.Task) {
return this.bookVideoServiceHandle.MediaToVideo(task)
}
}

View File

@ -5,6 +5,7 @@ import { BookPromptEntrance } from './bookIndex/bookPromptEntrance'
import { BookImageEntrance } from './bookIndex/bookImageEntrance'
import { BookExportEntrance } from './bookIndex/bookExportEntrance'
import { mixin } from '@/main/utils/mixin'
import { BookVideoEntrance } from './bookIndex/bookVideoEntrance'
// 使用装饰器实现多重继承
// 类型接口声明让TypeScript知道合并后的类型
@ -14,14 +15,16 @@ interface BookHandle
BookTaskDetailEntrance,
BookPromptEntrance,
BookImageEntrance,
BookExportEntrance {}
BookExportEntrance,
BookVideoEntrance {}
@mixin(
BookTaskEntrance,
BookTaskDetailEntrance,
BookPromptEntrance,
BookImageEntrance,
BookExportEntrance
BookExportEntrance,
BookVideoEntrance
)
class BookHandle extends BookDataEntrance {
constructor() {

View File

@ -2,12 +2,14 @@ import { BookTaskDetailService } from '@/define/db/service/book/bookTaskDetailSe
import { BookTaskService } from '@/define/db/service/book/bookTaskService'
import { OptionRealmService } from '@/define/db/service/optionService'
import { BookService } from '@/define/db/service/book/bookService'
import { TaskListService } from '@/define/db/service/book/taskListService'
export class BookBasicHandle {
bookTaskDetailService!: BookTaskDetailService
bookTaskService!: BookTaskService
optionRealmService!: OptionRealmService
bookService!: BookService
taskListService!: TaskListService
constructor() {
// 初始化
@ -27,6 +29,9 @@ export class BookBasicHandle {
if (!this.bookService) {
this.bookService = await BookService.getInstance()
}
if (!this.taskListService) {
this.taskListService = await TaskListService.getInstance()
}
}
async transaction(callback: (realm: any) => void) {

View File

@ -22,8 +22,9 @@ import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import { ValidateJson } from '@/define/Tools/validate'
const execAsync = util.promisify(exec)
import JianyingService from '../../jianying/jianyingService'
import { t } from '@/i18n'
export class BookVideoHandle extends BookBasicHandle {
export class BookExportHandle extends BookBasicHandle {
jianyingService: JianyingService
constructor() {
super()
@ -70,7 +71,7 @@ export class BookVideoHandle extends BookBasicHandle {
if (!isEmpty(bookTask.backgroundMusic) && bookTask.backgroundMusic != null) {
// 检查背景音乐文件或文件夹是否存在
if (!CheckFileOrDirExist(bookTask.backgroundMusic)) {
throw new Error('背景音乐文件夹或文件不存在,请检查')
throw new Error(t('背景音乐文件夹或文件不存在,请检查'))
}
// 判断背景音乐是文件还是文件夹
@ -86,7 +87,7 @@ export class BookVideoHandle extends BookBasicHandle {
// 如果是文件夹,从中随机选择一个音频文件
let files = await GetFilesWithExtensions(bookTask.backgroundMusic, ['.mp3', '.wav'])
if (files.length <= 0) {
throw new Error('背景音乐文件夹下面未存在有效的音频文件')
throw new Error(t('背景音乐文件夹下面未存在有效的音频文件'))
} else {
const randomIndex = Math.floor(Math.random() * files.length)
musicPath = files[randomIndex]
@ -166,12 +167,12 @@ export class BookVideoHandle extends BookBasicHandle {
// 验证时间信息完整性
if (element.startTime == null || element.endTime == null) {
throw new Error('字幕时间信息不完整')
throw new Error(t('字幕时间信息不完整'))
}
// 验证字幕内容完整性
if (element.subValue == null) {
throw new Error('字幕内容信息不完整')
throw new Error(t('字幕内容信息不完整'))
}
// 处理字幕内容如果是字符串格式的JSON则解析为对象
@ -179,7 +180,7 @@ export class BookVideoHandle extends BookBasicHandle {
if (ValidateJson(element.subValue)) {
element.subValue = JSON.parse(element.subValue)
} else {
throw new Error('字幕内容信息不完整')
throw new Error(t('字幕内容信息不完整'))
}
}
@ -252,23 +253,27 @@ export class BookVideoHandle extends BookBasicHandle {
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
if (bookTask == null) {
return errorMessage('没有找到小说批次任务,请检查', 'BookVideoHandle_AddJianyingDraft')
return errorMessage(t("未找到对应的小说批次任务"), 'BookVideoHandle_AddJianyingDraft')
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
return errorMessage('没有找到小说,请检查', 'BookVideoHandle_AddJianyingDraft')
return errorMessage(t("未找到指定ID的小说数据"), 'BookVideoHandle_AddJianyingDraft')
}
if (!isEmpty(bookTask.draftDepend) && bookTask.draftDepend != null) {
if (bookTask.name == bookTask.draftDepend) {
throw new Error('草稿名称不能和任务名称相同,请修改任务名称')
throw new Error(t('草稿名称不能和任务名称相同,请修改任务名称'))
}
await this.jianyingService.GenerateDraftFromDepend(
bookTask.draftDepend as string,
bookTask.imageFolder as string,
bookTask.name as string
)
return successMessage(bookTask.name, '添加剪映草稿成功!', 'BookVideoHandle_AddJianyingDraft')
return successMessage(
bookTask.name,
t("导出剪映草稿成功!"),
'BookVideoHandle_AddJianyingDraft'
)
}
let { draftName, configJsonPath } = await this.GenerateConfigFile(book, bookTask)
@ -276,7 +281,7 @@ export class BookVideoHandle extends BookBasicHandle {
// 开始调用 exe 执行 草稿的导出
let jianyingExePath = path.join(define.scripts_path, 'xiangbei_jianying_main.exe')
if (!CheckFileOrDirExist(jianyingExePath)) {
throw new Error('没有找到导出剪映的执行文件,请检查')
throw new Error(t('没有找到导出剪映的执行文件,请检查') + ':' + jianyingExePath)
}
let result: string = ''
try {
@ -335,14 +340,16 @@ export class BookVideoHandle extends BookBasicHandle {
}
await fs.promises.writeFile(errorLogPath, JSON.stringify(errorInfo, null, 2), 'utf-8')
throw new Error(`详细错误已记录到: ${errorLogPath}`)
throw new Error(JSON.stringify(errorInfo, null, 2))
}
// 所有的草稿都添加完毕之后开始返回
return successMessage(result, `添加剪映草稿成功!`, 'BookVideoHandle_AddJianyingDraft')
return successMessage(result, t("导出剪映草稿成功!"), 'BookVideoHandle_AddJianyingDraft')
} catch (error: any) {
return errorMessage(
'添加剪映草稿失败,错误信息如下:' + error.toString(),
t('导出剪映草稿失败:{error}', {
error: error.message
}),
'BookVideoHandle_AddJianyingDraft'
)
}

View File

@ -24,6 +24,7 @@ import { SettingModal } from '@/define/model/setting'
const execAsync = util.promisify(exec)
import { MJApiService } from '../../mj/mjApiService'
import { ImageSplit } from '@/define/Tools/image'
import { t } from '@/i18n'
export class BookImageHandle extends BookBasicHandle {
mjApiService: MJApiService
@ -66,16 +67,14 @@ export class BookImageHandle extends BookBasicHandle {
): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> {
try {
await this.InitBookBasicHandle()
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId, true)
let bookTaskDetail =
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到对应的小说子任务数据请检查ID是否正确')
}
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId, true)
// 获取小说项目的地址
let imageFolder = bookTask.imageFolder
if (imageFolder == null) {
throw new Error('没有找到对应的小说项目的图片地址,请检查')
throw new Error(t("没有找到对应的小说批次任务的图片输出地址,请检查"))
}
let currentImagePath = path.join(imageFolder, `${bookTaskDetail.name}.png`)
@ -93,7 +92,7 @@ export class BookImageHandle extends BookBasicHandle {
}
sourceImagePath = path.resolve(sourceImagePath)
if (!(await CheckFileOrDirExist(sourceImagePath))) {
throw new Error(`图片文件 ${sourceImagePath} 不存在,请检查`)
throw new Error(t("图片文件 {sourceImagePath} 不存在,请检查", { sourceImagePath }))
}
// 复制文件,判断父文件夹是不是存在
@ -107,12 +106,14 @@ export class BookImageHandle extends BookBasicHandle {
// 返回数据
return successMessage(
currentImagePath + `?t=${Date.now()}`,
'将指定的图片放到主图中成功',
t('将指定的图片放到主图中成功'),
'BookImage_MoveImageToMainImage'
)
} catch (error: any) {
return errorMessage(
'将指定的图片放到主图中失败,错误信息如下:' + error.message,
t("将指定的图片放到主图中失败,{error}", {
error: error.message
}),
'BookImage_MoveImageToMainImage'
)
}
@ -163,19 +164,17 @@ export class BookImageHandle extends BookBasicHandle {
bookTaskId: id
})
} else if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
let tempBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (tempBookTaskDetail == null) {
throw new Error('没有找到要删除的分镜数据请检查ID是否正确')
}
let tempBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id, true)
bookTaskDetails = [tempBookTaskDetail]
} else {
throw new Error('不支持的操作类型,请检查')
throw new Error(t("未知操作"))
}
//这边过滤被锁定的数据
bookTaskDetails = bookTaskDetails.filter((item) => !item.imageLock)
if (bookTaskDetails.length <= 0) {
throw new Error('没有要删除的分镜数据,请检查')
throw new Error(t("没有要删除的分镜数据,请检查"))
}
// 开始删除数据,要删除图片数据和出图的信息
@ -191,12 +190,14 @@ export class BookImageHandle extends BookBasicHandle {
return successMessage(
bookTaskDetails,
'删除所有的生图数据成功',
t('删除所有的生图数据成功'),
'BookImage_ResetGenerateImage'
)
} catch (error: any) {
return errorMessage(
'删除所有的图片数据失败,失败信息如下:' + error.toString(),
t("删除所有的图片数据失败,{error}", {
error: error.message
}),
'BookImage_ResetGenerateImage'
)
}
@ -233,14 +234,14 @@ export class BookImageHandle extends BookBasicHandle {
): Promise<string> {
console.log('开始上传图片', bookTaskDetailId, imageFile, option)
if (!(await CheckFileOrDirExist(path.resolve(imageFile)))) {
throw new Error(`图片文件 ${imageFile} 不存在,请检查`)
throw new Error(t("图片文件 {sourceImagePath} 不存在,请检查", {
sourceImagePath: imageFile
}))
}
// 修改数据库数据
let bookTaskDetail =
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到对应的小说子任务数据请检查ID是否正确')
}
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId, true)
let projectPath = await getProjectPath()
// 将图片复制到对应的文件夹中
// 生成一个0-10的随机数
@ -254,7 +255,8 @@ export class BookImageHandle extends BookBasicHandle {
if (option == 'outImagePath') {
let outImagePath = bookTaskDetail.outImagePath
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
bookTaskDetail.bookTaskId as string,
true
)
if (isEmpty(bookTaskDetail.outImagePath)) {
outImagePath = path.join(
@ -284,7 +286,7 @@ export class BookImageHandle extends BookBasicHandle {
})
return newImagePath + '?t=' + new Date().getTime()
} else {
throw new Error('无效的操作类型,请检查')
throw new Error(t('未知操作'))
}
}
@ -331,7 +333,7 @@ export class BookImageHandle extends BookBasicHandle {
let res = await this.UploadOne(bookTaskDetailId, imageFile as string, option)
return successMessage(
res,
'上传图片,并修改小说信息成功',
t('上传图片,并修改小说信息成功'),
'BookImage_UpLoadImageToBookAndUpdateMessage'
)
} else if (option == 'subImagePath') {
@ -342,15 +344,17 @@ export class BookImageHandle extends BookBasicHandle {
}
return successMessage(
subImage as string[],
'上传图片,并修改小说信息成功',
t('上传图片,并修改小说信息成功'),
'BookImage_UpLoadImageToBookAndUpdateMessage'
)
} else {
throw new Error('无效的操作类型,请检查')
throw new Error(t('未知操作'))
}
} catch (error: any) {
return errorMessage(
'上传图片到小说中,并修改小说信息失败,错误信息如下:' + error.message,
t("上传图片,并修改小说信息失败,{error}", {
error: error.message
}),
'BookImage_UpLoadImageToBookAndUpdateMessage'
)
}
@ -379,18 +383,17 @@ export class BookImageHandle extends BookBasicHandle {
*/
private async HDOneImagePrivate(bookTaskDetailId: string, scale: number = 2): Promise<string> {
let bookTaskDetail =
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到要高清的分镜数据请检查ID是否正确')
}
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId, true)
let outImagePath = bookTaskDetail.outImagePath as string
if (isEmpty(outImagePath)) {
throw new Error('没有找到要高清的分镜数据请检查ID是否正确')
throw new Error(t("未检测到分镜的输出图片,无法进行高清处理"))
}
let imageExist = await CheckFileOrDirExist(outImagePath as string)
if (!imageExist) {
throw new Error('没有找到要高清的图片,请检查图片是否存在')
throw new Error(t("图片文件 {sourceImagePath} 不存在,请检查", {
sourceImagePath: outImagePath
}))
}
let newImagePath = path.join(
@ -439,11 +442,13 @@ export class BookImageHandle extends BookBasicHandle {
// 高清完成
return successMessage(
outImagePath + '?t=' + new Date().getTime(),
'高清分镜成功',
t('分镜高清图片成功'),
'BookImage_HDOneImage'
)
} catch (error: any) {
return errorMessage('高清分镜失败,失败信息如下:' + error.toString(), 'BookImage_HDOneImage')
return errorMessage(t("分镜高清图片失败,{error}", {
error: error.message
}), 'BookImage_HDOneImage')
}
}
@ -489,20 +494,14 @@ export class BookImageHandle extends BookBasicHandle {
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('获取到的小说为空,无法执行操作')
}
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId, true)
let book = await this.bookService.GetBookDataById(bookTaskDetail.bookId as string, true)
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
bookTaskDetail.bookTaskId as string, true
)
if (bookTask == null) {
throw new Error('获取到的任务为空,无法执行操作')
}
let imagePath = path.join(
book.bookFolderPath as string,
`data\\MJOriginalImage\\${bookTaskDetail.id}_${new Date().getTime()}.png`
@ -530,7 +529,7 @@ export class BookImageHandle extends BookBasicHandle {
path.join(book.bookFolderPath as string, 'data\\MJOriginalImage')
)
if (imageRes && imageRes.length < 4) {
throw new Error('图片裁剪失败')
throw new Error(t('图片裁剪失败'))
}
// 修改数据
// 修改数据库数据,将图片保存到对应的文件夹中
@ -583,14 +582,17 @@ export class BookImageHandle extends BookBasicHandle {
subImagePath: imageRes.map((item) => item + '?time=' + new Date().getTime()),
mjMessage: mjMessage
},
'下载指定的图片地址并且分割成功',
t('下载指定的图片地址并且分割成功'),
'BookImage_DownloadImageUrlAndSplit'
)
} catch (error: any) {
return {
code: 0,
message: '下载指定的图片地址并且分割错误,错误信息如下:' + error.message
}
return errorMessage(
t("下载指定的图片地址并且分割失败,{error}", {
error: error.message
}),
'BookImage_DownloadImageUrlAndSplit'
)
}
}
@ -602,35 +604,25 @@ export class BookImageHandle extends BookBasicHandle {
* 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
* );
* @example // 下载并应用整个书籍任务的所有分镜图片(不覆盖已有图片)
// 下载并应用整个书籍任务的所有分镜图片(不覆盖已有图片)
const result = await bookImageHandle.GetImageUrlAndDownload(
"task-123",
OperateBookType.BOOKTASK,
false
);
// 下载并应用单个分镜图片(覆盖已有图片)
const result = await bookImageHandle.GetImageUrlAndDownload(
"detail-456",
OperateBookType.BOOKTASKDETAIL,
true
);
*/
async GetImageUrlAndDownload(
bookTaskDetailId: string
@ -639,43 +631,32 @@ export class BookImageHandle extends BookBasicHandle {
await this.InitBookBasicHandle()
let bookTaskDetail =
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId)
if (bookTaskDetail == null) {
throw new Error('没有找到要采集的分镜数据请检查ID是否正确')
}
await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId, true)
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
bookTaskDetail.bookTaskId as string, true
)
if (bookTask == null) {
throw new Error('没有找到要采集的小说批次任务数据请检查ID是否正确')
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
throw new Error('没有找到要采集的小说数据请检查ID是否正确')
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string, true)
if (bookTask.imageCategory != ImageCategory.Midjourney) {
throw new Error('只有MJ模式下才能使用这个功能')
throw new Error(t('只有MJ模式下才能使用这个功能'))
}
let mjGeneralSettingOption = this.optionRealmService.GetOptionByKey(
OptionKeyName.Midjourney.GeneralSetting
)
let mjGeneralSetting = optionSerialization<SettingModal.MJGeneralSettings>(
mjGeneralSettingOption,
'‘设置 -> MJ设置'
t("设置 -> MJ设置")
)
if (mjGeneralSetting.outputMode != ImageGenerateMode.MJ_API) {
throw new Error('只有MJ API模式下才能使用这个功能')
throw new Error(t('只有MJ API模式下才能使用这个功能'))
}
if (!bookTaskDetail.mjMessage) {
throw new Error('没有找到对应的分镜数据请检查ID是否正确')
throw new Error(t('分镜中没有MJ的消息数据请检查分镜数据'))
}
if (isEmpty(bookTaskDetail.mjMessage.messageId)) {
throw new Error('没有找到对应分镜的MJ Task ID请检查分镜数据')
throw new Error(t('没有找到对应分镜的MJ Task ID请检查分镜数据'))
}
// 这边开始采集
let task_res = await this.mjApiService.GetMJAPITaskById(
@ -691,7 +672,7 @@ export class BookImageHandle extends BookBasicHandle {
`data\\MJOriginalImage\\${task_res.messageId}.png`
)
if (isEmpty(task_res.imageClick)) {
throw new Error('没有找到对应的分镜的MJ图片链接请检查分镜数据')
throw new Error(t('没有找到对应的分镜的MJ图片链接请检查分镜数据'))
}
await CheckFolderExistsOrCreate(path.dirname(imagePath))
@ -706,7 +687,7 @@ export class BookImageHandle extends BookBasicHandle {
)
)
if (imageArray && imageArray.length < 4) {
throw new Error('图片裁剪失败')
throw new Error(t('图片裁剪失败'))
}
} else {
for (let i = 0; i < task_res.imageUrls.length; i++) {
@ -750,10 +731,12 @@ export class BookImageHandle extends BookBasicHandle {
})
let result = await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId)
return successMessage(result, '获取图片链接并且下载成功', 'BookImage_GetImageUrlAndDownload')
return successMessage(result, t('获取图片链接并且下载成功'), 'BookImage_GetImageUrlAndDownload')
} catch (error: any) {
return errorMessage(
'获取图片链接并且下载失败,错误信息如下:' + error.message,
t("获取图片链接并且下载失败,{error}", {
error: (error as Error).message
}),
'BookImage_GetImageUrlAndDownload'
)
}

View File

@ -19,6 +19,7 @@ import { SDServiceHandle } from '../../sd/sdServiceHandle'
import { aiHandle } from '../../ai'
import { AICharacterAnalyseRequestData } from '@/define/data/aiData/aiPrompt/CharacterAndScene/aiCharacterAnalyseRequestData'
import { AISceneAnalyseRequestData } from '@/define/data/aiData/aiPrompt/CharacterAndScene/aiSceneAnalyseRequestData'
import { t } from '@/i18n'
export class BookPromptHandle extends BookBasicHandle {
aiReasonCommon: AiReasonCommon
@ -71,7 +72,7 @@ export class BookPromptHandle extends BookBasicHandle {
await this.InitBookBasicHandle()
if (operateBookType == OperateBookType.BOOKTASK) {
bookTask = await this.bookTaskService.GetBookTaskDataById(id)
bookTask = await this.bookTaskService.GetBookTaskDataById(id, true)
allBookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: id
})
@ -83,21 +84,19 @@ export class BookPromptHandle extends BookBasicHandle {
bookTaskDetails = allBookTaskDetails
}
} else if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
let singleBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (singleBookTaskDetail == null) {
throw new Error('没有找到要推理的分镜数据请检查ID是否正确')
}
let singleBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id, true)
bookTask = await this.bookTaskService.GetBookTaskDataById(
singleBookTaskDetail.bookTaskId as string
singleBookTaskDetail.bookTaskId as string,
true
)
bookTaskDetails = [singleBookTaskDetail]
} else if (operateBookType == OperateBookType.UNDERBOOKTASK) {
let singleBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (singleBookTaskDetail == null) {
throw new Error('没有找到要推理的分镜数据请检查ID是否正确')
}
let singleBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id, true)
bookTask = await this.bookTaskService.GetBookTaskDataById(
singleBookTaskDetail.bookTaskId as string
singleBookTaskDetail.bookTaskId as string,
true
)
// 获取全部的分镜数据
allBookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
@ -115,7 +114,7 @@ export class BookPromptHandle extends BookBasicHandle {
}
}
} else {
throw new Error('未知操作类型')
throw new Error(t('未知操作'))
}
if (!allBookTaskDetails || allBookTaskDetails.length <= 0) {
allBookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
@ -124,14 +123,14 @@ export class BookPromptHandle extends BookBasicHandle {
}
if (bookTaskDetails.length <= 0) {
throw new Error('没有找到要推理的分镜数据')
throw new Error(t('没有找到要推理的分镜数据'))
}
let generalSettingOption = this.optionRealmService.GetOptionByKey(
OptionKeyName.Software.GeneralSetting
)
let generalSetting = optionSerialization<SettingModal.GeneralSettings>(
generalSettingOption,
'设置 -> 通用设置'
t('设置 -> 通用设置')
)
let tasks = [] as Array<() => Promise<any>>
@ -189,23 +188,41 @@ export class BookPromptHandle extends BookBasicHandle {
// 分批次执行异步任务
await ExecuteConcurrently(tasks, global.am.isPro ? (generalSetting.concurrency ?? 1) : 1)
// 执行完毕
return successMessage(null, '推理所有数据完成', 'BookPrompt_OriginalGetPrompt')
return successMessage(null, t("推理所有数据完成"), 'BookPrompt_OriginalGetPrompt')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务详细数据失败,失败原因如下:${error.message}`,
t("推理所有数据失败,{error}", {
error: error.message
}),
'BookPromptHandle_OriginalGetAiPrompt'
)
}
}
/**
* AI分镜头合并
* @param bookTaskId ID
* @param type
* @returns
* @throws
* @example
* const result = await bookPromptHandle.AIStoryboardMerge("task-123", BookTask.StoryboardMergeType.MJ);
* if (result.code === 1) {
* // 合并成功
* } else {
* // 合并失败
* }
*/
AIStoryboardMerge = async (bookTaskId: string, type: BookTask.StoryboardMergeType) => {
try {
let res = await aiHandle.AIStoryboardMerge(bookTaskId, type)
return successMessage(res, 'AI分镜头合并成功', 'BookPromptHandle_AIStoryboardMerge')
return successMessage(res, t('AI分镜头合并成功'), 'BookPromptHandle_AIStoryboardMerge')
} catch (error: any) {
return errorMessage(
`AI分镜头合并失败失败原因如下${error.message}`,
t('AI分镜头合并成功', {
error: error.message
}),
'BookPromptHandle_AIStoryboardMerge'
)
}
@ -250,11 +267,13 @@ export class BookPromptHandle extends BookBasicHandle {
} else if (type == PromptMergeType.SD_MERGE) {
return await this.sdServiceHandle.MergeSDPrompt(id, operateBookType)
} else {
throw new Error('未知的合并模式,请检查')
throw new Error(t('未知的合并模式,请检查'))
}
} catch (error: any) {
return errorMessage(
'合并提示词失败,错误信息如下:' + error.message,
t("合并提示词失败,{error}", {
error: error.message
}),
'ReverseBook_MergePrompt'
)
}
@ -288,19 +307,19 @@ export class BookPromptHandle extends BookBasicHandle {
* PresetCategory.Scene
* );
*/
AutoAnalyzeCharacterOrScene = async (bookTaskId: string, type: PresetCategory) => {
AutoAnalyzeCharacterOrScene = async (bookTaskId: string, type: PresetCategory): Promise<GeneralResponse.SuccessItem | GeneralResponse.ErrorItem> => {
try {
if (type != PresetCategory.Character && type != PresetCategory.Scene) {
throw new Error('分析的类型只能是角色或场景,请检查')
throw new Error(t('分析的类型只能是角色或场景,请检查'))
}
await this.InitBookBasicHandle()
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId, true)
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTaskId
})
if (bookTaskDetails.length <= 0) {
throw new Error('没有找到要分析的分镜数据请先导入文案或者时srt')
throw new Error(t('没有找到要分析的分镜数据请先导入文案或者时srt'))
}
let words = bookTaskDetails
@ -315,7 +334,7 @@ export class BookPromptHandle extends BookBasicHandle {
} else if (type == PresetCategory.Scene) {
requestData = AISceneAnalyseRequestData
} else {
throw new Error('未知的分析类型,请检查')
throw new Error(t('未知的分析类型,请检查'))
}
requestData.messages = this.aiReasonCommon.replaceMessageObject(requestData.messages, {
@ -359,12 +378,14 @@ export class BookPromptHandle extends BookBasicHandle {
return successMessage(
autoAnalyzeCharacterData,
'自动分析角色或场景成功',
t('自动分析角色或场景成功'),
'ReverseBook_AutoAnalyzeCharacterOrScene'
)
} catch (error: any) {
return errorMessage(
'自动分析角色或场景失败,错误信息如下:' + error.message,
t("自动分析角色或场景失败,{error}", {
error: error.message
}),
'ReverseBook_AutoAnalyzeCharacterOrScene'
)
}

View File

@ -5,6 +5,7 @@ import { BookBasicHandle } from './bookBasicHandle'
import { CheckFileOrDirExist, DeleteFolderAllFile } from '@/define/Tools/file'
import path from 'path'
import { isEmpty } from 'lodash'
import { t } from '@/i18n'
/**
*
@ -34,11 +35,13 @@ export class BookServiceHandle extends BookBasicHandle {
try {
await this.InitBookBasicHandle()
let res = await this.bookService.AddOrModifyBook(book)
return successMessage(res, '添加/修改小说信息成功!', 'BookServiceHandle_AddOrModifyBook')
return successMessage(res, t('添加/修改小说信息成功!'), 'BookServiceHandle_AddOrModifyBook')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`添加/修改小说信息失败,失败原因如下:${error.message}`,
t("添加/修改小说信息失败,{error}", {
error: error.message
}),
'BookServiceHandle_AddOrModifyBook'
)
}
@ -59,11 +62,13 @@ export class BookServiceHandle extends BookBasicHandle {
try {
await this.InitBookBasicHandle()
let res = await this.bookService.GetBookDataCondition(queryCondition)
return successMessage(res, '获取小说数据成功!', 'BookServiceHandle_GetBookDataCondition')
return successMessage(res, t('获取小说数据成功!'), 'BookServiceHandle_GetBookDataCondition')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说数据失败,失败原因如下:${error.message}`,
t("获取小说数据失败,{error}", {
error: error.message
}),
'BookServiceHandle_GetBookDataCondition'
)
}
@ -82,11 +87,13 @@ export class BookServiceHandle extends BookBasicHandle {
try {
await this.InitBookBasicHandle()
let res = await this.bookService.GetBookDataById(id)
return successMessage(res, '获取小说数据成功!', 'BookServiceHandle_GetBookDataById')
return successMessage(res, t('获取小说数据成功!'), 'BookServiceHandle_GetBookDataById')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说数据失败,失败原因如下:${error.message}`,
t("获取小说数据失败,{error}", {
error: error.message
}),
'BookServiceHandle_GetBookDataById'
)
}
@ -109,11 +116,13 @@ export class BookServiceHandle extends BookBasicHandle {
try {
await this.InitBookBasicHandle()
let res = await this.bookService.ModifyBookDataById(id, bookData)
return successMessage(res, '修改小说数据成功!', 'BookServiceHandle_ModifyBookDataById')
return successMessage(res, t('修改小说数据成功!'), 'BookServiceHandle_ModifyBookDataById')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`修改小说数据失败,失败原因如下:${error.message}`,
t("修改小说数据失败,{error}", {
error: error.message
}),
'BookServiceHandle_ModifyBookDataById'
)
}
@ -123,13 +132,7 @@ export class BookServiceHandle extends BookBasicHandle {
async ResetBookDataInfo(bookId: string) {
try {
await this.InitBookBasicHandle()
let book = await this.bookService.GetBookDataById(bookId)
if (book == null) {
return errorMessage(
'未找到小说数据,重置小说数据失败!',
'BookServiceHandle_ResetBookDataInfo'
)
}
let book = await this.bookService.GetBookDataById(bookId, true)
let bookDataPath = path.resolve(book.bookFolderPath as string, 'data')
let bookScriptPath = path.resolve(book.bookFolderPath as string, 'script')
@ -159,10 +162,12 @@ export class BookServiceHandle extends BookBasicHandle {
}
// 删除完毕 返回
return successMessage(null, '重置小说数据成功!', 'BookServiceHandle_ResetBookDataInfo')
return successMessage(null, t('重置小说数据成功!'), 'BookServiceHandle_ResetBookDataInfo')
} catch (error: any) {
return errorMessage(
`重置小说数据失败,失败原因如下:${error.message}`,
t("重置小说数据失败,{error}", {
error: error.message
}),
'BookServiceHandle_ResetBookDataInfo'
)
}
@ -185,19 +190,13 @@ export class BookServiceHandle extends BookBasicHandle {
let resetRes = await this.ResetBookDataInfo(id)
if (resetRes.code != 1) {
return errorMessage(
`删除小说失败,失败原因如下:${resetRes.message}`,
t("删除小说数据失败,{error}", {
error: resetRes.message
}),
'BookServiceHandle_DeleteBookDataInfoById'
)
}
let book = await this.bookService.GetBookDataById(id)
if (book == null) {
return errorMessage(
'未找到小说数据,删除小说失败!',
'BookServiceHandle_DeleteBookDataInfoById'
)
}
let book = await this.bookService.GetBookDataById(id, true)
let projectPath = book.bookFolderPath as string
this.bookService.DeleteBookDataById(id)
@ -206,11 +205,13 @@ export class BookServiceHandle extends BookBasicHandle {
if (!isEmpty(projectPath) && (await CheckFileOrDirExist(projectPath))) {
await DeleteFolderAllFile(projectPath, true)
}
return successMessage(null, '删除小说数据成功!', 'BookServiceHandle_DeleteBookDataById')
return successMessage(null, t('删除小说数据成功!'), 'BookServiceHandle_DeleteBookDataById')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`删除小说数据失败,失败原因如下:${error.message}`,
t("删除小说数据失败,{error}", {
error: error.message
}),
'BookServiceHandle_DeleteBookDataById'
)
}

View File

@ -1,23 +1,19 @@
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'
import { BookBasicHandle } from './bookBasicHandle'
import { t } from '@/i18n'
export class BookTaskDetailServiceHandle {
private bookTaskDetailService!: BookTaskDetailService
export class BookTaskDetailServiceHandle extends BookBasicHandle {
private bookTaskServiceHandle: BookTaskServiceHandle
constructor() {
super()
this.bookTaskServiceHandle = new BookTaskServiceHandle()
}
private async InitBookTaskDetailServiceHandle() {
// 如果 bookTaskDetailService 已经初始化,则直接返回
if (this.bookTaskDetailService) return
this.bookTaskDetailService = await BookTaskDetailService.getInstance()
}
/**
*
@ -32,18 +28,20 @@ export class BookTaskDetailServiceHandle {
bookTaskDetailCondition: Book.QueryBookTaskDetailCondition
): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookTaskDetailServiceHandle()
await this.InitBookBasicHandle()
let res =
await this.bookTaskDetailService.GetBookTaskDetailDataByCondition(bookTaskDetailCondition)
return successMessage(
res,
'获取小说子任务详细数据成功!',
t('获取小说分镜数据成功!'),
'BookTaskDetailServiceHandle_GetBookTaskDetailData'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务详细数据失败,失败原因如下:${error.message}`,
t("获取小说分镜数据失败,{error}", {
error: error.message
}),
'BookTaskDetailServiceHandle_GetBookTaskDetailData'
)
}
@ -60,17 +58,19 @@ export class BookTaskDetailServiceHandle {
*/
async GetBookTaskDetailDataById(id: string): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookTaskDetailServiceHandle()
await this.InitBookBasicHandle()
let res = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
return successMessage(
res,
'获取小说子任务详细数据成功!',
t('获取小说分镜数据成功!'),
'BookTaskDetailServiceHandle_GetBookTaskDetailDataById'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务详细数据失败,失败原因如下:${error.message}`,
t("获取小说分镜数据失败,{error}", {
error: error.message
}),
'BookTaskDetailServiceHandle_GetBookTaskDetailDataById'
)
}
@ -91,20 +91,22 @@ export class BookTaskDetailServiceHandle {
updateData: Book.SelectBookTaskDetail
): Promise<SuccessItem | ErrorItem> {
try {
await this.InitBookTaskDetailServiceHandle()
await this.InitBookBasicHandle()
let res = await this.bookTaskDetailService.ModifyBookTaskDetailById(
bookTaskDetailId,
updateData
)
return successMessage(
res,
'修改小说子任务详细数据成功!',
t('修改小说分镜数据成功!'),
'BookTaskDetailServiceHandle_ModifyBookTaskDetailById'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`修改小说子任务详细数据失败,失败原因如下:${error.message}`,
t("修改小说分镜数据失败,{error}", {
error: error.message
}),
'BookTaskDetailServiceHandle_ModifyBookTaskDetailById'
)
}
@ -128,14 +130,16 @@ export class BookTaskDetailServiceHandle {
): Promise<SuccessItem | ErrorItem> {
try {
if (operateBookType != OperateBookType.BOOKTASK) {
throw new Error('目前只支持对小说任务的文案保存')
throw new Error(t("目前只支持对小说批次任务的文案保存"))
}
await this.bookTaskServiceHandle.InitBookBasicHandle()
let bookTask =
await this.bookTaskServiceHandle.bookTaskService.GetBookTaskDataById(bookTaskId)
let bookTask = await this.bookTaskServiceHandle.bookTaskService.GetBookTaskDataById(
bookTaskId,
true
)
await this.InitBookTaskDetailServiceHandle()
await this.InitBookBasicHandle()
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTaskId
})
@ -153,7 +157,7 @@ export class BookTaskDetailServiceHandle {
bookTaskDetails[i].id
)
if (btd == null) {
throw new Error('未找到对应的分镜数据,请检查')
throw new Error(t('未找到对应的小说分镜信息'))
}
// 开始修改
btd.startTime = element.startTime
@ -195,7 +199,7 @@ export class BookTaskDetailServiceHandle {
bookTaskDetails[i].id
)
if (btd == null) {
throw new Error('未找到对应的分镜数据,请检查')
throw new Error(t('未找到对应的小说分镜信息'))
}
// 开始修改
btd.startTime = element.startTime
@ -230,17 +234,19 @@ export class BookTaskDetailServiceHandle {
bookTaskId: bookTaskId
})
if (newData.length == 0) {
throw new Error('没有找到对应的分镜数据,请检查')
throw new Error(t('未找到对应的小说分镜信息'))
}
return successMessage(
newData,
'保存小说批次数据分镜信息成功!',
t('保存小说分镜数据成功!'),
'BookTaskDetailServiceHandle_SaveCopywritingInfo'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`保存小说批次数据分镜信息失败,失败原因如下:${error.message}`,
t("保存小说分镜数据失败,{error}", {
error: error.message
}),
'BookTaskDetailServiceHandle_SaveCopywritingInfo'
)
}

View File

@ -26,6 +26,7 @@ import { ValidateJson } from '@/define/Tools/validate'
import { TimeStringToMilliseconds } from '@/define/Tools/time'
import { SrtHandle } from '../../common/srtHandle'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import { t } from '@/i18n'
export class BookTaskServiceHandle extends BookBasicHandle {
constructor() {
@ -49,13 +50,15 @@ export class BookTaskServiceHandle extends BookBasicHandle {
let res = await this.bookTaskService.GetBookTaskDataByCondition(bookTaskCondition)
return successMessage(
res,
'获取小说子任务数据成功!',
t("获取小说批次任务数据成功!"),
'BookTaskServiceHandle_GetBookTaskData'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务数据失败,失败原因如下:${error.message}`,
t("获取小说批次任务数据失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_GetBookTaskData'
)
}
@ -76,13 +79,15 @@ export class BookTaskServiceHandle extends BookBasicHandle {
let res = await this.bookTaskService.GetBookTaskDataById(id)
return successMessage(
res,
'获取小说子任务数据成功!',
t("获取小说批次任务数据成功!"),
'BookTaskServiceHandle_GetBookTaskDataById'
)
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取小说子任务数据失败,失败原因如下:${error.message}`,
t("获取小说批次任务数据失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_GetBookTaskDataById'
)
}
@ -131,12 +136,14 @@ export class BookTaskServiceHandle extends BookBasicHandle {
let res = await this.bookTaskService.ModifyBookTaskDataById(bookTaskId, data)
return successMessage(
res,
'修改小说子任务数据成功!',
t("修改小说批次任务数据成功!"),
'BookTaskServiceHandle_ModifyBookTaskData'
)
} catch (error: any) {
return errorMessage(
`修改小说子任务数据失败,失败原因如下:${error.message}`,
t("修改小说批次任务数据失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_ModifyBookTaskData'
)
}
@ -159,12 +166,12 @@ export class BookTaskServiceHandle extends BookBasicHandle {
await this.InitBookBasicHandle()
for (let i = 0; i < bookTaskIds.length; i++) {
const element = bookTaskIds[i]
let tempBookTask = await this.bookTaskService.GetBookTaskDataById(element)
let tempBookTask = await this.bookTaskService.GetBookTaskDataById(element, true)
bookTasks.push(tempBookTask)
}
if (bookTasks.length === 0) {
return errorMessage(
'没有找到要删除的小说子任务数据',
t("没有找到要删除的小说批次任务数据"),
'BookTaskServiceHandle_DeleteBookTask'
)
}
@ -190,12 +197,14 @@ export class BookTaskServiceHandle extends BookBasicHandle {
bookId: bookTasks[0].bookId,
bookTasks: newBookTasks
},
'删除小说子任务数据成功!',
t("删除小说批次任务数据成功!"),
'BookTaskServiceHandle_DeleteBookTask'
)
} catch (error: any) {
return errorMessage(
`删除小说子任务数据失败,失败原因如下:${error.message}`,
t("删除小说批次任务数据失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_DeleteBookTask'
)
}
@ -214,10 +223,8 @@ export class BookTaskServiceHandle extends BookBasicHandle {
no: number,
projectPath: string
): Promise<Book.SelectBookTask> {
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
throw new Error('没有找到小说,请检查')
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string, true)
let name = book.name + '_' + no.toString().padStart(5, '0')
let imageFolder = path.join(projectPath, `${bookTask.bookId}/tmp/${name}`)
@ -288,10 +295,8 @@ export class BookTaskServiceHandle extends BookBasicHandle {
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 book = await this.bookService.GetBookDataById(bookTask.bookId as string, true)
let originalTimePath = path.join(book.bookFolderPath as string, `data/${book.id}.mp4.json`)
let originalTime = [] as any[]
if (await CheckFileOrDirExist(originalTimePath)) {
@ -423,13 +428,13 @@ export class BookTaskServiceHandle extends BookBasicHandle {
try {
if (!global.am.isPro && addData.count > 1) {
return errorMessage(
'批量添加小说子任务数据失败,免费版只能添加一条数据',
t("批量添加小说批次任务数据失败,免费版只能添加一条数据"),
'BookTaskServiceHandle_AddNewBookTask'
)
}
if (isEmpty(addData.selectBookId)) {
return errorMessage(
'批量添加小说子任务数据失败小说ID不能为空',
t('批量添加小说批次任务数据失败小说ID不能为空'),
'BookTaskServiceHandle_AddNewBookTask'
)
}
@ -442,7 +447,7 @@ export class BookTaskServiceHandle extends BookBasicHandle {
for (let i = 0; i < addData.count; i++) {
if (addData.copyBookTask && !isEmpty(addData.selectBookTask)) {
let bookTask = await this.bookTaskService.GetBookTaskDataById(
addData.selectBookTask as string
addData.selectBookTask as string, true
)
let oldBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition(
{
@ -497,12 +502,14 @@ export class BookTaskServiceHandle extends BookBasicHandle {
bookId: addData.selectBookId,
bookTasks: bookTasks
},
'添加小说子任务数据成功!',
t("添加小说批次任务数据成功!"),
'BookTaskServiceHandle_AddNewBookTask'
)
} catch (error: any) {
return errorMessage(
`添加小说子任务数据失败,失败原因如下:${error.message}`,
t("添加小说批次任务数据失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_AddNewBookTask'
)
}
@ -520,13 +527,7 @@ export class BookTaskServiceHandle extends BookBasicHandle {
*/
async GetBookTaskImageGenerateProgress(bookId: string): Promise<SuccessItem | ErrorItem> {
try {
let book = await this.bookService.GetBookDataById(bookId)
if (book == null) {
return errorMessage(
'获取小说任务生成进度失败,小说不存在',
'BookTaskServiceHandle_GetBookTaskImageGenerateProgress'
)
}
await this.InitBookBasicHandle()
// 获取小说批次数据
let bookTasks = await this.bookTaskService.GetBookTaskDataByCondition({
@ -535,7 +536,7 @@ export class BookTaskServiceHandle extends BookBasicHandle {
if (bookTasks.bookTasks.length === 0) {
return successMessage(
{},
'获取小说任务生成进度成功!',
t("获取小说批次任务生成进度成功!"),
'BookTaskServiceHandle_GetBookTaskImageGenerateProgress'
)
}
@ -595,12 +596,14 @@ export class BookTaskServiceHandle extends BookBasicHandle {
}
return successMessage(
resData,
'获取小说任务生成进度成功!',
t("获取小说批次任务生成进度成功!"),
'BookTaskServiceHandle_GetBookTaskImageGenerateProgress'
)
} catch (error: any) {
return errorMessage(
`获取小说任务生成进度失败,失败原因如下:${error.message}`,
t("获取小说批次任务生成进度失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_GetBookTaskImageGenerateProgress'
)
}
@ -632,10 +635,8 @@ export class BookTaskServiceHandle extends BookBasicHandle {
})
if (bookTasks.bookTasks.length === 0) {
return errorMessage(
'获取小说批次任务的第一张图片路径失败,小说批次任务不存在',
'BookTaskServiceHandle_GetBookTaskFirstImagePath'
)
return successMessage({}, t('获取小说批次任务的第一张图片路径成功!'), 'BookTaskServiceHandle_GetBookTaskFirstImagePath')
}
let resData: Record<string, string | undefined> = {}
// 开始处理所有的批次
@ -666,12 +667,14 @@ export class BookTaskServiceHandle extends BookBasicHandle {
}
return successMessage(
resData,
'获取小说批次任务的第一张图片路径成功!',
t('获取小说批次任务的第一张图片路径成功!'),
'BookTaskServiceHandle_GetBookTaskFirstImagePath'
)
} catch (error: any) {
return errorMessage(
`获取小说批次任务的第一张图片路径失败,失败原因如下:${error.message}`,
t("获取小说批次任务的第一张图片路径失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_GetBookTaskFirstImagePath'
)
}
@ -711,10 +714,8 @@ export class BookTaskServiceHandle extends BookBasicHandle {
// 初始化复制数量为100后续会根据实际子图数量调整
let copyCount = 100
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
if (bookTask == null) {
throw new Error('没有找到对应的数小说任务,请检查数据')
}
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId, true)
// 获取该批次下所有的分镜详情,用于检查子图情况
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
@ -722,7 +723,7 @@ export class BookTaskServiceHandle extends BookBasicHandle {
})
if (bookTaskDetails == null || bookTaskDetails.length <= 0) {
throw new Error('没有对应的小说分镜任务,请先添加分镜任务')
throw new Error(t('没有对应的小说分镜任务,请先添加分镜任务'))
}
// 遍历所有分镜,找出子图数量最少的分镜,以此作为复制批次的数量基准
@ -730,10 +731,10 @@ export class BookTaskServiceHandle extends BookBasicHandle {
const element = bookTaskDetails[i]
// 检查分镜是否有子图路径
if (!element.subImagePath) {
throw new Error('检测到图片没有出完,请先检查出图')
throw new Error(t('检测到图片没有出完,请先检查出图'))
}
if (element.subImagePath == null || element.subImagePath.length <= 0) {
throw new Error('检测到图片没有出完,请先检查出图')
throw new Error(t('检测到图片没有出完,请先检查出图'))
}
// 更新最小子图数量(取所有分镜中子图数量的最小值)
if (element.subImagePath.length < copyCount) {
@ -742,7 +743,7 @@ export class BookTaskServiceHandle extends BookBasicHandle {
}
// 检查是否有足够的子图进行一拆四操作至少需要2张子图才能拆分
if (copyCount - 1 <= 0) {
throw new Error('有分镜子图数量不足,无法进行一拆四')
throw new Error(t("有分镜子图数量不足,无法进行一拆四"))
}
// 开始执行复制操作:创建 (copyCount-1) 个新批次
@ -755,11 +756,13 @@ export class BookTaskServiceHandle extends BookBasicHandle {
)
// 返回成功结果
return successMessage(null, '一拆四成功', 'BookTaskServiceHandle_OneToFourBookTask')
return successMessage(null, t("一拆四成功!"), 'BookTaskServiceHandle_OneToFourBookTask')
} catch (error: any) {
// 捕获并返回错误信息
return errorMessage(
`小说批次任务 一拆四 失败,失败原因如下:${error.message}`,
t("一拆四失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_OneToFourBookTask'
)
}
@ -778,10 +781,12 @@ export class BookTaskServiceHandle extends BookBasicHandle {
try {
await this.InitBookBasicHandle()
let res = await this.bookTaskService.AddBookTask(bookTask)
return successMessage(res, '添加小说子任务数据成功!', 'BookTaskServiceHandle_AddBookTask')
return successMessage(res, t("添加小说批次任务数据成功!"), 'BookTaskServiceHandle_AddBookTask')
} catch (error: any) {
return errorMessage(
`添加小说子任务数据失败,失败原因如下:${error.message}`,
t("添加小说批次任务数据失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_AddBookTask'
)
}
@ -803,17 +808,24 @@ export class BookTaskServiceHandle extends BookBasicHandle {
let res = await this.bookTaskService.ResetBookTaskDataById(bookTaskId)
return successMessage(
res,
'重置小说子任务数据成功!',
t("重置小说批次任务数据成功!"),
'BookTaskServiceHandle_ResetBookTaskData'
)
} catch (error: any) {
return errorMessage(
`重置小说子任务数据失败,失败原因如下:${error.message}`,
t("重置小说批次任务数据失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_ResetBookTaskData'
)
}
}
/**
*
* @param bookTaskId
* @returns
*/
async ResetAndInitializeBookTask(bookTaskId: string) {
try {
// 先调用重置
@ -857,18 +869,20 @@ export class BookTaskServiceHandle extends BookBasicHandle {
if (newBookTaskDetails == null || newBookTaskDetails.length <= 0) {
return errorMessage(
'重置小说分镜数据不包含bootask本身数据并且初始化全新的分镜信息失败小说分镜数据不存在',
t("重置小说分镜数据(不包含批次任务本身数据),并且初始化全新的分镜信息失败,小说分镜数据不存在"),
'BookTaskServiceHandle_ResetAndInitializeBookTask'
)
}
return successMessage(
newBookTaskDetails,
'重置小说分镜数据成功!',
t("重置小说分镜数据成功!"),
'BookTaskServiceHandle_ResetAndInitializeBookTask'
)
} catch (error: any) {
return errorMessage(
`重置小说分镜数据不包含bootask本身数据并且初始化全新的分镜信息失败失败原因如下${error.message}`,
t("重置小说分镜数据(不包含批次任务本身数据),并且初始化全新的分镜信息失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_ResetAndInitializeBookTask'
)
}
@ -887,13 +901,8 @@ export class BookTaskServiceHandle extends BookBasicHandle {
try {
await this.InitBookBasicHandle()
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId)
if (bookTask == null) {
return errorMessage(
'重置小说子任务数据失败,小说子任务不存在',
'BookTaskServiceHandle_ResetBookTaskDataById'
)
}
let bookTask = await this.bookTaskService.GetBookTaskDataById(bookTaskId, true)
let imageFolder = bookTask.imageFolder
let res = this.bookTaskService.DeleteBookTaskDataById(bookTaskId)
@ -902,10 +911,12 @@ export class BookTaskServiceHandle extends BookBasicHandle {
if (!isEmpty(imageFolder) && (await CheckFileOrDirExist(imageFolder))) {
await DeleteFolderAllFile(imageFolder as string, true)
}
return successMessage(res, '删除小说子任务数据成功!', 'BookTaskServiceHandle_DeleteBookTask')
return successMessage(res, t("删除小说批次任务数据成功!"), 'BookTaskServiceHandle_DeleteBookTask')
} catch (error: any) {
return errorMessage(
`删除小说子任务数据失败,失败原因如下:${error.message}`,
t("删除小说批次任务数据失败,{error}", {
error: error.message
}),
'BookTaskServiceHandle_DeleteBookTask'
)
}

View File

@ -0,0 +1,356 @@
import { errorMessage, SendReturnMessage, successMessage } from '@/public/generalTools'
import { BookBasicHandle } from './bookBasicHandle'
import { Book } from '@/define/model/book/book'
import {
ImageToVideoModels,
KlingMode,
MJVideoBatchSize,
MJVideoMotion,
MJVideoType,
RunawayModel,
RunwaySeconds,
VideoModel,
VideoStatus
} from '@/define/enum/video'
import { SettingModal } from '@/define/model/setting'
import { OptionKeyName } from '@/define/enum/option'
import { GetApiDefineDataById } from '@/define/data/apiData'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import { GetBaseUrl } from '@/define/Tools/common'
import { isEmpty } from 'lodash'
import { t } from '@/i18n/main'
import { getProjectPath } from '../../option/optionCommonService'
import { TaskModal } from '@/define/model/task'
import { BookBackTaskStatus, BookBackTaskType, BookTaskStatus } from '@/define/enum/bookEnum'
import { VideoHandle } from '@/main/service/video/index'
export class BookVideoServiceHandle extends BookBasicHandle {
constructor() {
super()
}
//#region GetVideoBookInfoList
/**
*
* bookTaskId
*
* @param condition bookTaskId等参数
* @returns
*/
GetVideoBookInfoList = async (condition: BookVideo.BookVideoInfoListQuertCondition) => {
try {
await this.InitBookBasicHandle()
// 获取小说的所有的数据
let bookRes = await this.bookService.GetBookDataCondition({
id: condition.bookId
})
if (bookRes.resultBooks == null || bookRes.resultBooks.length <= 0) {
return errorMessage(
t("获取小说数据错误,未找到指定条件的小说数据"),
'BookVideoServiceHandle_GetVideoBookInfoList'
)
}
let bookList = bookRes.resultBooks ?? []
// 有指定的小说批次任务的ID情况下 只返回当前的小说数据
if (condition.bookTaskId) {
let bookTask = await this.bookTaskService.GetBookTaskDataById(condition.bookTaskId, true)
// 再上面的小说数据里面获取数据 然后直接返回就行
let bookInfo = bookList.find((book) => book.id === bookTask.bookId)
if (!bookInfo) {
return errorMessage(
t("没有找到对应的小说数据,请先添加小说"),
'BookImageTextToVideoInfo_GetVideoBookInfoList'
)
}
bookInfo.children = [bookTask]
// 返回
return successMessage(
bookInfo,
t('获取小说批次任务数据成功'),
'BookImageTextToVideoInfo_GetVideoBookInfoList'
)
}
// 没有那个数据 将所有的数据进行处理
let res: Book.SelectBook[] = []
for (let i = 0; i < bookList.length; i++) {
const element = bookList[i]
// 获取小说批次任务数据
let bookTaskRes = await this.bookTaskService.GetBookTaskDataByCondition({
bookId: element.id
})
if (bookTaskRes.bookTasks == null || bookTaskRes.bookTasks.length <= 0) {
continue
}
// 检查所有的 bookTasks 里面是不是开启了图转视频功能
let videoBookTasks = bookTaskRes.bookTasks.filter((task) => task.openVideoGenerate)
if (videoBookTasks.length <= 0) {
continue
} else {
element.children = videoBookTasks
res.push(element)
}
}
return successMessage(
res,
t('获取小说批次任务数据成功'),
'BookImageTextToVideoInfo_GetVideoBookInfoList'
)
} catch (error: any) {
return errorMessage(
t("获取小说批次任务数据失败,{error}", {
error: error.message
}),
'BookImageTextToVideoInfo_GetVideoBookInfoList'
)
}
}
//#endregion
//#region InitVideoMessage
/**
*
* @param params
* @returns
*/
InitVideoMessage = async (bookTaskId: string) => {
try {
await this.InitBookBasicHandle()
let bookTaskDetails = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: bookTaskId
})
if (bookTaskDetails == null || bookTaskDetails.length <= 0) {
let d = t("初始视频消息失败,未找到指定小说批次任务的分镜数据")
return errorMessage(d, 'BookVideoServiceHandle_InitVideoMessage')
}
let project_path = await getProjectPath()
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i]
if (element.videoMessage) {
continue
}
let { videoMessage } = await this.InitVideoMessageData(element, project_path)
// 修改数据报错
await this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
element.id as string,
videoMessage
)
}
return successMessage(
null,
t("初始视频消息成功!"),
'BookVideoServiceHandle_InitVideoMessage'
)
} catch (error: any) {
return errorMessage(
t("初始化分镜视频消息失败,{error}", {
error: error.message
}),
'BookVideoServiceHandle_InitVideoMessage'
)
}
}
//#endregion
//#region InitVideoMessageData
async InitVideoMessageData(bookTaskDetail: Book.SelectBookTaskDetail, project_path: string) {
try {
console.log(project_path)
let s = t("设置 -> 通用设置")
console.log('翻译输出', s)
// 获取基础要的数据
let generalSettingString =
this.optionRealmService.GetOptionDataByKey<SettingModal.GeneralSettings>(
OptionKeyName.Software.GeneralSetting,
t("设置 -> 通用设置")
)
let defaultVideoMode = generalSettingString.defaultImg2Video ?? ImageToVideoModels.MJ_VIDEO
let inferenceSetting =
this.optionRealmService.GetOptionDataByKey<SettingModal.InferenceAISettings>(
OptionKeyName.InferenceAI.InferenceSetting,
t("设置 -> 推理设置")
)
let gptUrl = GetApiDefineDataById(inferenceSetting.apiProvider)?.gpt_url
if (gptUrl == null || isEmpty(gptUrl)) {
throw new Error()
}
// 开始设置默认设置
let outImage =
bookTaskDetail.outImagePath != null && !isEmpty(bookTaskDetail.outImagePath)
? bookTaskDetail.outImagePath
: ''
// 设置为runway
let optionObject: BookTaskDetail.RunwayOption = {
callback_url: GetBaseUrl(gptUrl),
image: outImage,
model: RunawayModel.GNE3,
prompt: '',
options: {
seconds: RunwaySeconds.FIVE
}
}
let lumaOptions: BookTaskDetail.lumaOptions = {
user_prompt: '',
aspect_ratio: '4:3',
expand_prompt: true,
loop: false,
image_url: outImage
}
let klingOptions: BookTaskDetail.klingOptions = {
model: 'kling-v1',
image: outImage,
image_tail: '',
prompt: '',
negative_prompt: '',
mode: KlingMode.STD,
duration: RunwaySeconds.FIVE
}
let mjVideoOptions: BookTaskDetail.MjVideoOptions = {
action: undefined,
image: outImage, // 或者根据 Image 类型的定义提供默认值
index: undefined,
motion: MJVideoMotion.High, // 根据 Motion 类型的定义提供默认值
noStorage: false,
notifyHook: undefined,
prompt: null,
state: undefined,
taskId: undefined,
raw: false,
batchSize: MJVideoBatchSize.ONE,
videoType: MJVideoType.HD
}
let videoMessage: BookTaskDetail.VideoMessage = {
id: bookTaskDetail.id,
msg: '',
prompt: '',
videoType: defaultVideoMode,
style: '',
imageUrl: outImage,
bookTaskDetailId: bookTaskDetail.id,
runwayOptions: JSON.stringify(optionObject),
lumaOptions: JSON.stringify(lumaOptions),
klingOptions: JSON.stringify(klingOptions),
mjVideoOptions: JSON.stringify(mjVideoOptions),
status: VideoStatus.WAIT,
model: VideoModel.IMAGE_TO_VIDEO
}
return { optionObject, lumaOptions, klingOptions, mjVideoOptions, videoMessage }
} catch (error) {
throw error
}
}
//#endregion
//#region UpdateBookTaskDetailVideoMessage
/**
*
* @param bookTaskDetailId ID
* @param videoMessage
* @returns
*/
async UpdateBookTaskDetailVideoMessage(
bookTaskDetailId: string,
videoMessage: Partial<BookTaskDetail.VideoMessage>
) {
try {
await this.InitBookBasicHandle()
await this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
bookTaskDetailId,
videoMessage
)
return successMessage(
t("修改小说分镜的VideoMessage成功"),
'BookVideoServiceHandle_UpdateBookTaskDetailVideoMessage'
)
} catch (error: any) {
return errorMessage(
t("修改小说分镜的VideoMessage失败{error}", { error: error.message }),
'BookVideoServiceHandle_UpdateBookTaskDetailVideoMessage'
)
}
}
//#endregion
//#region MediaToVideo
async MediaToVideo(task: TaskModal.Task) {
try {
// 更具不同的方式调用不同的处理方法
const videoHandle = new VideoHandle()
switch (task.type) {
case BookBackTaskType.MJ_VIDEO:
return await videoHandle.MJImageToVideo(task)
default:
throw new Error('未知的视频生成方式,请检查')
}
} catch (error) {
// 统一处理 报错信息
let message = t("图转视频失败,失败信息如下:{error}", { error: (error as Error).message })
// 修改批次状态
await this.bookTaskService.ModifyBookTaskDataById(task.bookTaskId as string, {
status: BookTaskStatus.IMAGE_TO_VIDEO_ERROR,
errorMsg: message
})
// 修改任务状态
this.taskListService.UpdateTaskStatus({
id: task.id as string,
status: BookBackTaskStatus.FAIL,
errorMessage: message
})
// 修改对应的小说分镜的 videoMessage 的状态
await this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
{
status: VideoStatus.FAIL,
taskId: '',
msg: message
}
)
// 发送消息
SendReturnMessage(
{
code: 0,
id: task.bookTaskDetailId as string,
data: BookTaskStatus.IMAGE_TO_VIDEO_ERROR,
message: message
},
task.messageName as string
)
return errorMessage(message, 'BookVideoServiceHandle_ImageToVideo')
}
}
//#endregion
}

View File

@ -1,4 +1,5 @@
import { CheckFileOrDirExist } from '@/define/Tools/file'
import { t } from '@/i18n'
import fs from 'fs'
import { isEmpty } from 'lodash'
@ -23,13 +24,15 @@ export class SrtHandle {
async GetSrtDataByPath(srtPath: string): Promise<SrtDataModel[]> {
try {
if (isEmpty(srtPath)) {
throw new Error('srt文件路径不能为空')
throw new Error(t("srt文件路径不能为空"))
}
if (!srtPath.toLowerCase().endsWith('.srt')) {
throw new Error('srt文件后缀不正确请检查')
throw new Error(t("srt文件后缀不正确请检查"))
}
if (!(await CheckFileOrDirExist(srtPath))) {
throw new Error(`srt文件不存在路径为${srtPath}`)
throw new Error(t("目的文件/文件夹不存在,{data}", {
data: srtPath
}))
}
let srt_data = (await fs.promises.readFile(srtPath)).toString('utf-8')
const entries = srt_data.replace(/\r\n/g, '\n').split('\n\n')

View File

@ -13,6 +13,7 @@ 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 { t } from '@/i18n'
/**
*
@ -51,7 +52,7 @@ class JianyingService {
// 反序列化设置数据为具体的设置对象
let generalSetting = optionSerialization<SettingModal.GeneralSettings>(
res,
'设置 -> 通用设置'
t('设置 -> 通用设置')
)
// 从设置中获取剪映草稿文件夹路径
@ -66,13 +67,15 @@ class JianyingService {
// 返回成功结果
return successMessage(
result,
'获取剪映草稿文件列表成功',
t("获取剪映草稿文件列表成功!"),
'JianyingService_GetJianyingDraftFileList'
)
} catch (error: any) {
// 捕获异常并返回错误信息
return errorMessage(
'获取剪映草稿文件列表失败,失败信息:' + error.message,
t("获取剪映草稿文件列表失败,{error}", {
error: error.message
}),
'JianyingService_GetJianyingDraftFileList'
)
}
@ -179,7 +182,7 @@ class JianyingService {
let dependDraftPath = path.resolve(generalSetting.draftPath, dependDraftName)
if (!(await CheckFileOrDirExist(dependDraftPath))) {
throw new Error('依赖的草稿文件不存在,请检查')
throw new Error(t("依赖的草稿文件不存在,请检查"))
}
let draftPath = path.resolve(generalSetting.draftPath, draftName)
if (await CheckFileOrDirExist(draftPath)) {
@ -194,26 +197,26 @@ class JianyingService {
// 开始处理数据
let draftJsonPath = path.resolve(draftPath, 'draft_content.json')
if (!(await CheckFileOrDirExist(draftJsonPath))) {
throw new Error('剪映草稿文件数据文件不存在,请先检查')
throw new Error(t('剪映草稿文件数据文件不存在,请先检查'))
}
// 读取草稿文件内容
let draftJsonString = await fs.promises.readFile(draftJsonPath, 'utf-8')
if (!ValidateJson(draftJsonString)) {
throw new Error('剪映草稿文件格式错误,请检查')
throw new Error(t('剪映草稿文件格式错误,请检查'))
}
let draftJson = JSON.parse(draftJsonString)
let draftTracks = draftJson.tracks
if (draftTracks.length <= 0) {
throw new Error('剪映草稿文件格式错误,没有轨道,请检查')
throw new Error(t('剪映草稿文件格式错误,没有轨道,请检查'))
}
let videoTrack = draftTracks[0] // 只处理主轨道
if (videoTrack.type !== 'video') {
throw new Error('剪映草稿文件格式错误主轨道不是Video请检查')
throw new Error(t('剪映草稿文件格式错误主轨道不是Video请检查'))
}
let videoTrackSegments = videoTrack.segments
if (videoTrackSegments.length <= 0) {
throw new Error('剪映草稿文件格式错误,主轨道没有对应的图片节点,请检查')
throw new Error(t("剪映草稿文件格式错误,主轨道没有对应的图片节点,请检查"))
}
let segmentMaterialNodes: any = []
for (let i = 0; i < videoTrackSegments.length; i++) {
@ -224,14 +227,14 @@ class JianyingService {
}
if (images.length !== segmentMaterialNodes.length) {
throw new Error('当前新增的任务对应的图片数量和依赖的草稿的主轨道的图片数量不一致,请检查')
throw new Error(t('当前新增的任务对应的图片数量和依赖的草稿的主轨道的图片数量不一致,请检查'))
}
// 直接修改
for (let i = 0; i < segmentMaterialNodes.length; i++) {
const element = segmentMaterialNodes[i]
if (!images[i]) {
throw new Error('图片数量不对应,请检查')
throw new Error(t('图片数量不对应,请检查'))
}
element.path = images[i]
}
@ -248,16 +251,16 @@ class JianyingService {
public async ReplaceDraftMaterialImageToVideo(draftName: string, replaceOnject: ReplaceOnject[]) {
let draftPath = path.resolve(global.config.draft_path, draftName)
if (!(await CheckFileOrDirExist(draftPath))) {
throw new Error('草稿文件不存在,请检查')
throw new Error(t('草稿文件不存在,请检查'))
}
let draftJsonPath = path.resolve(draftPath, 'draft_content.json')
if (!(await CheckFileOrDirExist(draftJsonPath))) {
throw new Error('剪映草稿文件数据文件不存在,请先检查')
throw new Error(t('剪映草稿文件数据文件不存在,请先检查'))
}
// 读取草稿文件内容
let draftJsonString = await fs.promises.readFile(draftJsonPath, 'utf-8')
if (!ValidateJson(draftJsonString)) {
throw new Error('剪映草稿文件格式错误,请检查')
throw new Error(t('剪映草稿文件格式错误,请检查'))
}
let draftJson = JSON.parse(draftJsonString)
let materialNodes = draftJson.materials.videos
@ -291,7 +294,7 @@ class JianyingService {
private FindNode(nodes: any[], type: string, value: any) {
let res = nodes.filter((node: any) => node[type] === value)
if (res.length === 0) {
throw new Error('没有找到对应的节点')
throw new Error(t('没有找到对应的节点'))
}
return res[0]
}

View File

@ -7,6 +7,7 @@ import { isEmpty } from 'lodash'
import { BookBackTaskStatus } from '@/define/enum/bookEnum'
import { MJ } from '@/define/model/mj'
import { define } from '@/define/define'
import { t } from '@/i18n'
/**
* MidJourney API
@ -129,6 +130,7 @@ export class MJApiService extends MJBasic {
imagineUrl!: string
fetchTaskUrl!: string
describeUrl!: string
videoUrl!: string
token!: string
constructor() {
@ -140,13 +142,17 @@ export class MJApiService extends MJBasic {
/**
* MJ设置
*/
async InitMJSetting(): Promise<void> {
async InitMJSetting(outputMode?: ImageGenerateMode): Promise<void> {
await this.GetMJGeneralSetting()
// 获取当前机器人类型
this.bootType =
this.mjGeneralSetting?.robot == MJRobotType.NIJI ? 'NIJI_JOURNEY' : 'MID_JOURNEY'
if (outputMode) {
this.mjGeneralSetting!.outputMode = outputMode
}
// 再 MJ API 模式下 获取对应的数据
if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.MJ_API) {
await this.GetApiSetting()
@ -155,14 +161,19 @@ export class MJApiService extends MJBasic {
isEmpty(this.mjApiSetting.apiUrl) ||
isEmpty(this.mjApiSetting.apiKey)
) {
throw new Error('没有找到对应的API的配置请检查 ‘设置 -> MJ设置 配置!')
throw new Error(t("没有找到对应的API的配置请检查 {data} 配置!", {
data: t('设置 -> MJ设置')
}))
}
let apiProvider = GetApiDefineDataById(this.mjApiSetting.apiUrl as string)
if (apiProvider.mj_url == null) {
throw new Error('当前API不支持MJ出图请检查 ‘设置 -> MJ设置 配置!')
throw new Error(t("当前API不支持MJ出图请检查 {data} 配置!", {
data: t('设置 -> MJ设置')
}))
}
this.imagineUrl = apiProvider.mj_url.imagine
this.describeUrl = apiProvider.mj_url.describe
this.videoUrl = apiProvider.mj_url.video ?? ''
this.fetchTaskUrl = apiProvider.mj_url.once_get_task
this.token = this.mjApiSetting.apiKey
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.MJ_PACKAGE) {
@ -173,20 +184,27 @@ export class MJApiService extends MJBasic {
isEmpty(this.mjPackageSetting.packageToken)
) {
throw new Error(
'没有找到对应的生图包的配置或配置有误,请检查 ‘设置 -> MJ设置 -> 生图包模式’ 配置!'
t("没有找到对应的生图包的配置或配置有误,请检查 {data} 配置!", {
data: t('设置 -> MJ设置 -> 生图包模式')
})
)
}
let mjProvider = GetApiDefineDataById(this.mjPackageSetting.selectPackage)
if (!mjProvider.isPackage) {
throw new Error('当前选择的包不支持,请检查 ‘设置 -> MJ设置 -> 生图包模式’ 配置!')
throw new Error(t("当前选择的包不支持,请检查 {data} 配置!", {
data: t('设置 -> MJ设置 -> 生图包模式')
}))
}
if (mjProvider.mj_url == null) {
throw new Error('当前选择的包不支持,请检查 ‘设置 -> MJ设置 -> 生图包模式’ 配置!')
throw new Error(t("当前选择的包不支持,请检查 {data} 配置!", {
data: t('设置 -> MJ设置 -> 生图包模式')
}))
}
this.imagineUrl = mjProvider.mj_url.imagine
this.describeUrl = mjProvider.mj_url.describe
this.videoUrl = mjProvider.mj_url.video ?? ''
this.fetchTaskUrl = mjProvider.mj_url.once_get_task
this.token = this.mjPackageSetting.packageToken
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.LOCAL_MJ) {
@ -197,7 +215,9 @@ export class MJApiService extends MJBasic {
isEmpty(this.mjLocalSetting.token)
) {
throw new Error(
'本地代理模式的设置不完善或配置错误,请检查 ‘设置 -> MJ设置 -> 本地代理模式’ 配置!'
t("本地代理模式的设置不完善或配置错误,请检查 {data} 配置!", {
data: t('设置 -> MJ设置 -> 本地代理模式')
})
)
}
this.mjLocalSetting.requestUrl.endsWith('/')
@ -206,6 +226,7 @@ export class MJApiService extends MJBasic {
this.imagineUrl = this.mjLocalSetting.requestUrl + '/mj/submit/imagine'
this.describeUrl = this.mjLocalSetting.requestUrl + '/mj/submit/describe'
this.videoUrl = this.mjLocalSetting.requestUrl + '/mj/submit/video'
this.fetchTaskUrl = this.mjLocalSetting.requestUrl + '/mj/task/${id}/fetch'
this.token = this.mjLocalSetting.token
} else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.REMOTE_MJ) {
@ -213,10 +234,13 @@ export class MJApiService extends MJBasic {
this.imagineUrl = define.remotemj_api + 'mj/submit/imagine'
this.describeUrl = define.remotemj_api + 'mj/submit/describe'
this.videoUrl = ""
this.fetchTaskUrl = define.remotemj_api + 'mj/task/${id}/fetch'
this.token = define.remote_token
} else {
throw new Error('当前的MJ出图模式不支持请检查 ‘设置 -> MJ设置 配置!')
throw new Error(t("当前的MJ出图模式不支持请检查 {data} 配置!", {
data: t('设置 -> MJ设置')
}))
}
}
@ -336,7 +360,7 @@ export class MJApiService extends MJBasic {
res = await this.SubmitMJDescribeAPI(param)
break
default:
throw new Error('MJ反推的类型不支持反推只支持API和代理模式')
throw new Error(t('MJ反推的类型不支持反推只支持API和代理模式'))
}
return res
}
@ -485,7 +509,7 @@ export class MJApiService extends MJBasic {
res = await this.SubmitMJImagineAPI(taskId, prompt)
break
default:
throw new Error('MJ出图的类型不支持')
throw new Error(t('MJ出图的类型不支持'))
}
return res
}
@ -551,7 +575,7 @@ export class MJApiService extends MJBasic {
delete data.accountFilter.instanceId
useTransfer = this.mjRemoteSetting?.isForward ?? false
} else {
throw new Error('不支持的MJ出图类型')
throw new Error(t('不支持的MJ出图类型'))
}
console.log('useTransfer', useTransfer)
return {
@ -591,7 +615,7 @@ export class MJApiService extends MJBasic {
async SubmitMJImagineAPI(taskId: string, prompt: string): Promise<string> {
// 这边校验是不是在提示词包含不正确的链接
if (prompt.includes('feishu.cn')) {
throw new Error('提示词里面出现了 feishu.cn 飞书的链接,请检查并复制正确的链接')
throw new Error(t("提示词里面出现了 feishu.cn 飞书的链接,请检查并复制正确的链接"))
}
let { body, config } = this.GenerateImagineRequestBody(prompt)
@ -607,7 +631,7 @@ export class MJApiService extends MJBasic {
}
if (resData == null) {
throw new Error('返回的数据为空')
throw new Error(t('返回的数据为空'))
}
// 某些API的返回的code为23表示队列已满需要重新请求

View File

@ -7,6 +7,7 @@ import { BookTaskService } from '@/define/db/service/book/bookTaskService'
import { BookService } from '@/define/db/service/book/bookService'
import { PresetRealmService } from '@/define/db/service/presetService'
import { TaskListService } from '@/define/db/service/book/taskListService'
import { t } from '@/i18n'
export class MJBasic {
optionRealmService!: OptionRealmService
@ -73,7 +74,7 @@ export class MJBasic {
)
this.mjGeneralSetting = optionSerialization<SettingModal.MJGeneralSettings>(
generalSetting,
'设置 -> MJ设置'
t('设置 -> MJ设置')
)
}
@ -93,7 +94,7 @@ export class MJBasic {
let apiSetting = this.optionRealmService.GetOptionByKey(OptionKeyName.Midjourney.ApiSetting)
this.mjApiSetting = optionSerialization<SettingModal.MJApiSettings>(
apiSetting,
'‘设置 -> MJ设置 -> API设置'
t('设置 -> MJ设置 -> API模式')
)
}
@ -116,7 +117,7 @@ export class MJBasic {
)
this.mjPackageSetting = optionSerialization<SettingModal.MJPackageSetting>(
packageSetting,
'‘设置 -> MJ设置 -> 生图包设置’'
t("设置 -> MJ设置 -> 生图包模式")
)
}
@ -140,7 +141,7 @@ export class MJBasic {
)
this.mjRemoteSetting = optionSerialization<SettingModal.MJRemoteSetting>(
remoteSetting,
'‘设置 -> MJ设置 -> 代理模式设置’'
t("设置 -> MJ设置 -> 代理模式")
)
}
@ -162,7 +163,7 @@ export class MJBasic {
let localSetting = this.optionRealmService.GetOptionByKey(OptionKeyName.Midjourney.LocalSetting)
this.mjLocalSetting = optionSerialization<SettingModal.MJLocalSetting>(
localSetting,
'‘设置 -> MJ设置 -> 本地代理设置’'
t("设置 -> MJ设置 -> 本地代理模式")
)
}
}

View File

@ -32,6 +32,7 @@ import {
import { ImageSplit } from '@/define/Tools/image'
import { getProjectPath } from '../option/optionCommonService'
import { PresetBasicService } from '../preset/presetBasicService'
import { t } from '@/i18n'
export class MJServiceHandle extends MJBasic {
mjApiService: MJApiService
@ -81,7 +82,7 @@ export class MJServiceHandle extends MJBasic {
bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: id
})
bookTask = await this.bookTaskService.GetBookTaskDataById(id)
bookTask = (await this.bookTaskService.GetBookTaskDataById(id, true)) as Book.SelectBookTask
// 判断是不是有为空的
let emptyName = [] as string[]
for (let i = 0; i < bookTaskDetail.length; i++) {
@ -91,32 +92,31 @@ export class MJServiceHandle extends MJBasic {
}
}
if (emptyName.length > 0) {
throw new Error(`${emptyName.join('')} 的提示词为空,请先推理`)
throw new Error(t("{emptyName} 的提示词为空,请先推理提示词", {
emptyName: emptyName.join(', ')
}))
}
} else if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
let tempBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (tempBookTaskDetail == null) {
throw new Error('没有找到要合并的分镜数据请检查ID是否正确')
}
let tempBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id, true)
if (isEmpty(tempBookTaskDetail.gptPrompt)) {
throw new Error('当前分镜没有推理提示词,请先生成')
throw new Error(t("{emptyName} 的提示词为空,请先推理提示词", {
emptyName: tempBookTaskDetail.name as string
}))
}
bookTaskDetail = [tempBookTaskDetail]
bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail[0].bookTaskId as string
)
bookTask = (await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail[0].bookTaskId as string,
true
)) as Book.SelectBookTask
} else {
throw new Error('未知的合并类型')
throw new Error(t('未知类型'))
}
if (bookTaskDetail.length <= 0) {
throw new Error('没有找到对应的需要合并的分镜数据')
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
throw new Error('没有找到对应的小说数据请检查小说ID是否正确')
throw new Error(t('未找到对应的小说分镜信息'))
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string, true)
// 获取通用的后缀
let suffixParam = this.mjGeneralSetting?.commandSuffix
@ -209,10 +209,12 @@ export class MJServiceHandle extends MJBasic {
prompt: promptStr
})
}
return successMessage(result, 'MJ模式合并数据成功', 'MJOpt_MergePrompt')
return successMessage(result, t("MJ模式合并数据成功"), 'MJOpt_MergePrompt')
} catch (error: any) {
return errorMessage(
'MJ合并提示词失败错误信息如下' + error.message,
t("MJ模式合并数据失败{error}", {
error: error.message
}),
'MJServiceHandle_MergeMJPrompt'
)
}
@ -248,25 +250,19 @@ export class MJServiceHandle extends MJBasic {
): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
if (isEmpty(task.bookTaskDetailId)) {
throw new Error('MJ出图没有找到对应的分镜信息')
throw new Error(t('小说分镜ID不能为空'))
}
await this.GetMJGeneralSetting()
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
task.bookTaskDetailId as string
task.bookTaskDetailId as string, true
)
if (bookTaskDetail == null) {
throw new Error('没有找到对应的分镜信息')
}
let book = await this.bookService.GetBookDataById(bookTaskDetail.bookId as string)
if (book == null) {
throw new Error('没有找到对应的小说信息')
}
let book = await this.bookService.GetBookDataById(bookTaskDetail.bookId as string, true)
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
bookTaskDetail.bookTaskId as string, true
)
if (bookTask == null) {
throw new Error('没有找到对应的任务信息')
}
// 调用方法合并提示词
let mergeRes = await this.MergeMJPrompt(
task.bookTaskDetailId as string,
@ -280,7 +276,9 @@ export class MJServiceHandle extends MJBasic {
let prompt = bookTaskDetail.prompt
if (isEmpty(prompt) || prompt == undefined) {
throw new Error(`${bookTaskDetail.name} 没有找到对应的提示词`)
throw new Error(t("{emptyName} 的提示词为空,请先推理提示词", {
emptyName: bookTaskDetail.name
}))
}
// 这个就是任务ID
let reqRes = await this.mjApiService.SubmitMJImagine(task.id as string, prompt)
@ -321,7 +319,9 @@ export class MJServiceHandle extends MJBasic {
// throw new Error(`任务队列过多,${task.bookTaskDetailId} 重新提交排队`);
return successMessage(
null,
`任务队列过多,${task.bookTaskDetailId} 重新提交排队`,
t("任务队列过多,{id} 重新提交排队", {
id: task.bookTaskDetailId
}),
'MJServiceHandle_MJImagine'
)
}
@ -355,12 +355,17 @@ export class MJServiceHandle extends MJBasic {
await this.FetchImageTask(task, reqRes, book, bookTask, bookTaskDetail)
return successMessage(
null,
`MJ生图成功分镜ID${task.bookTaskDetailId}任务ID${task.id}`,
t("MJ生图成功分镜ID{bookTaskDetailId}任务ID${taskId}", {
bookTaskDetailId: task.bookTaskDetailId,
taskId: task.id
}),
'MJServiceHandle_MJImagine'
)
} catch (error: any) {
console.log(error.toString())
let errorMsg = 'MJ生图失败失败信息如下' + error.toString()
let errorMsg = t("MJ生图失败{error}", {
error: error.message
})
this.taskListService.UpdateTaskStatus({
id: task.id as string,
status: BookBackTaskStatus.FAIL,
@ -448,7 +453,9 @@ export class MJServiceHandle extends MJBasic {
status: BookTaskStatus.IMAGE_FAIL
}
)
let errorMsg = `MJ生成图片失败失败信息如下${task_res.message}`
let errorMsg = t("MJ生图失败{error}", {
error: task_res.message
})
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
@ -514,7 +521,7 @@ export class MJServiceHandle extends MJBasic {
)
)
if (imageArray && imageArray.length < 4) {
throw new Error('图片裁剪失败')
throw new Error(t('图片裁剪失败'))
}
} else {
for (let i = 0; i < task_res.imageUrls.length; i++) {

View File

@ -2,6 +2,7 @@ import { OptionRealmService } from '@/define/db/service/optionService'
import { OptionKeyName } from '@/define/enum/option'
import { optionSerialization } from './optionSerialization'
import { SettingModal } from '@/define/model/setting'
import { t } from '@/i18n'
/**
*
@ -12,7 +13,7 @@ export async function getProjectPath() {
let projectOption = optionRealmService.GetOptionByKey(OptionKeyName.Software.ProjectPath)
let projectPath: string = optionSerialization(
projectOption,
'设置-> 通用设置 -> 项目地址'
t('设置-> 通用设置 -> 项目地址')
) as string
return projectPath
}
@ -28,7 +29,7 @@ export async function getGeneralSetting() {
)
let generalSetting: SettingModal.GeneralSettings = optionSerialization(
generalSettingOption,
'设置-> 通用设置'
t('设置 -> 通用设置')
) as SettingModal.GeneralSettings
return generalSetting
}

View File

@ -1,6 +1,7 @@
import { OptionRealmService } from '@/define/db/service/optionService'
import { OptionType } from '@/define/enum/option'
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
import { t } from '@/i18n'
import { errorMessage, successMessage } from '@/public/generalTools'
export class OptionOptions {
optionRealmService!: OptionRealmService
@ -22,10 +23,15 @@ export class OptionOptions {
try {
await this.InitService()
let res = this.optionRealmService.GetOptionByKey(key)
return successMessage(res, '获取成功 OptionKey: ' + key, 'OptionOptions.GetOptionByKey')
return successMessage(res, t("获取成功 OptionKey: {key}", {
key: key
}), 'OptionOptions.GetOptionByKey')
} catch (error: any) {
return errorMessage(
'获取失败 OptionKey: ' + key + ',失败信息如下 : ' + error.message,
t("获取失败 OptionKey: {key},失败原因:{error}", {
key: key,
error: error.message
}),
'OptionOptions.GetOptionByKey'
)
}
@ -47,10 +53,15 @@ export class OptionOptions {
value = value.toString()
}
let res = this.optionRealmService.ModifyOptionByKey(key, value, type)
return successMessage(res, '修改成功 OptionKey: ' + key, 'OptionOptions.ModifyOptionByKey')
return successMessage(res, t("修改成功 OptionKey: {key}", {
key: key
}), 'OptionOptions.ModifyOptionByKey')
} catch (error: any) {
return errorMessage(
`修改失败 OptionKey: ${key} 失败信息如下: ${error.message}`,
t("修改失败 OptionKey: {key},失败原因:{error}", {
key: key,
error: error.message
}),
'OptionOptions.ModifyOptionByKey'
)
}

View File

@ -1,4 +1,5 @@
import { getOptionType, OptionType } from '@/define/enum/option'
import { t } from '@/i18n'
import { isEmpty } from 'lodash'
/**
@ -8,10 +9,12 @@ import { isEmpty } from 'lodash'
* @returns
*/
export function convertStringToType<T>(value: string, type: OptionType, checkString?: string): T {
let checkErrorString = '请到 ' + checkString + ' 检查设置!'
let checkErrorString = t("请到 “{checkString}” 检查设置!", {
checkString: checkString
})
// 如果值为空,直接报错
if (value === undefined || value === null || value === '') {
throw new Error('当前值为空!' + checkString ? checkErrorString : '')
throw new Error(t('当前值为空!') + checkString ? checkErrorString : '')
}
try {
@ -54,13 +57,13 @@ export const optionSerialization = <T>(
defaultValue?: T
): T => {
if (option == null) {
if (defaultValue) {
if (defaultValue != null) {
return defaultValue
}
throw new Error('未找到选项对象,请检查所有的选项设置是否存在!')
throw new Error(t("未找到选项对象,请检查所有的选项设置是否存在!"))
}
if (option.value == null || option.value == undefined || isEmpty(option.value)) {
if (defaultValue) {
if (defaultValue != null) {
return defaultValue
}
throw new Error('option value is null')

View File

@ -2,6 +2,7 @@ import { PresetCategory } from '@/define/data/presetData'
import { PresetBasic } from './presetBasic'
import { PromptMergeType } from '@/define/enum/bookEnum'
import { isEmpty } from 'lodash'
import { t } from '@/i18n'
export class PresetBasicService extends PresetBasic {
constructor() {
@ -72,7 +73,7 @@ export class PresetBasicService extends PresetBasic {
}
return { characterString: result, characterUrl: '' }
} else {
throw new Error('不支持的合并类型')
throw new Error(t('不支持的合并类型'))
}
}
}

View File

@ -6,6 +6,7 @@ import { Base64ToFile, GetImageTypeFromBase64 } from '@/define/Tools/image'
import { errorMessage, successMessage } from '@/public/generalTools'
import path from 'path'
import { PresetBasic } from './presetBasic'
import { t } from '@/i18n'
/**
*
@ -37,11 +38,13 @@ export class PresetServiceHandle extends PresetBasic {
try {
await this.InitPresetBasic()
let res = this.presetRealmService.GetPresetByCondition(queryCondition)
return successMessage(res, '获取预设成功!', 'PresetServiceHandle_GetPresetByCondition')
return successMessage(res, t("获取预设列表成功!"), 'PresetServiceHandle_GetPresetByCondition')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取预设失败,失败原因如下:${error.message}`,
t("获取预设列表失败,{error}", {
error: error.message
}),
'PresetServiceHandle_GetPresetByCondition'
)
}
@ -60,11 +63,13 @@ export class PresetServiceHandle extends PresetBasic {
try {
await this.InitPresetBasic()
let res = this.presetRealmService.GetPresetById(id)
return successMessage(res, '获取预设成功!', 'PresetServiceHandle_GetPresetById')
return successMessage(res, t('获取预设成功!'), 'PresetServiceHandle_GetPresetById')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`获取预设失败,失败原因如下:${error.message}`,
t("获取预设失败,{error}", {
error: error.message
}),
'PresetServiceHandle_GetPresetById'
)
}
@ -91,7 +96,7 @@ export class PresetServiceHandle extends PresetBasic {
// 判断base64是什么图片类型
let ext = GetImageTypeFromBase64(element)
if (ext != '.png' && ext != '.jpg' && ext != '.webp') {
throw new Error('图片格式不合法!')
throw new Error(t("图片格式不合法!只支持 png、jpg、webp 格式的图片!"))
}
let imagePath = JoinPath(
@ -106,11 +111,13 @@ export class PresetServiceHandle extends PresetBasic {
}
let res = this.presetRealmService.AddPreset(preset)
return successMessage(res, '添加预设成功!', 'PresetServiceHandle_AddPreset')
return successMessage(res, t('添加预设成功!'), 'PresetServiceHandle_AddPreset')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`添加预设失败,失败原因如下:${error.message}`,
t("添加预设失败,{error}", {
error: error.message
}),
'PresetServiceHandle_AddPreset'
)
}
@ -142,7 +149,7 @@ export class PresetServiceHandle extends PresetBasic {
// 判断base64是什么图片类型
let ext = GetImageTypeFromBase64(element)
if (ext != '.png' && ext != '.jpg' && ext != '.webp') {
throw new Error('图片格式不合法!')
throw new Error(t("图片格式不合法!只支持 png、jpg、webp 格式的图片!"))
}
let imagePath = JoinPath(
@ -166,11 +173,13 @@ export class PresetServiceHandle extends PresetBasic {
preset.showImage = iamges
}
let res = this.presetRealmService.ModifyPreset(id, preset)
return successMessage(res, '修改预设成功!', 'PresetServiceHandle_ModifyPreset')
return successMessage(res, t("修改预设成功!"), 'PresetServiceHandle_ModifyPreset')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`修改预设失败,失败原因如下:${error.message}`,
t("修改预设失败,{error}", {
error: error.message
}),
'PresetServiceHandle_ModifyPreset'
)
}
@ -189,11 +198,13 @@ export class PresetServiceHandle extends PresetBasic {
try {
await this.InitPresetBasic()
this.presetRealmService.DeletePreset(id)
return successMessage(null, '删除预设成功!', 'PresetServiceHandle_DeletePreset')
return successMessage(null, t("删除预设成功!"), 'PresetServiceHandle_DeletePreset')
} catch (error: any) {
// 处理错误,返回错误信息
return errorMessage(
`删除预设失败,失败原因如下:${error.message}`,
t('删除预设失败,{error}', {
error: error.message
}),
'PresetServiceHandle_DeletePreset'
)
}

View File

@ -20,6 +20,7 @@ import { ImageGenerateMode } from '@/define/data/mjData'
import path from 'path'
import { getProjectPath } from '../option/optionCommonService'
import { SDServiceHandle } from './sdServiceHandle'
import { t } from '@/i18n'
export class ComfyUIServiceHandle extends SDServiceHandle {
constructor() {
@ -31,21 +32,15 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
await this.InitSDBasic()
let comfyUISettingCollection = await this.GetComfyUISetting()
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
task.bookTaskDetailId as string
task.bookTaskDetailId as string, true
)
if (bookTaskDetail == null) {
throw new Error('未找到对应的小说分镜')
}
let book = await this.bookService.GetBookDataById(bookTaskDetail.bookId as string)
if (book == null) {
throw new Error('未找到对应的小说')
}
let book = await this.bookService.GetBookDataById(bookTaskDetail.bookId as string, true)
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
bookTaskDetail.bookTaskId as string, true
)
if (bookTask == null) {
throw new Error('未找到对应的小说任务')
}
// 调用方法合并提示词
let mergeRes = await this.MergeSDPrompt(
@ -82,11 +77,11 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
SendReturnMessage(
{
code: 1,
message: '任务已提交',
message: t('任务已提交'),
id: task.bookTaskDetailId as string,
data: {
status: 'submited',
message: '任务已提交',
message: t('任务已提交'),
id: task.bookTaskDetailId as string
} as any
},
@ -102,7 +97,9 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
comfyUISettingCollection
)
} catch (error: any) {
let errorMsg = 'ComfyUI 生图失败,失败信息如下:' + error.toString()
let errorMsg = t("ComfyUI生图失败{error}", {
error: error.message()
})
this.taskListService.UpdateTaskStatus({
id: task.id as string,
status: BookBackTaskStatus.FAIL,
@ -153,7 +150,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
)
result['comfyuiSimpleSetting'] = optionSerialization<SettingModal.ComfyUISimpleSettingModel>(
comfyuiSimpleSettingOption,
'设置 -> ComfyUI 设置'
t("设置 -> ComfyUI 设置")
)
let comfyuiWorkFlowSettingOption = optionRealmService.GetOptionByKey(
@ -162,12 +159,12 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
let comfyuiWorkFlowList = optionSerialization<SettingModal.ComfyUIWorkFlowSettingModel[]>(
comfyuiWorkFlowSettingOption,
'设置 -> ComfyUI 设置'
t("设置 -> ComfyUI 设置")
)
result['comfyuiWorkFlowSetting'] = comfyuiWorkFlowList
if (comfyuiWorkFlowList.length <= 0) {
throw new Error('ComfyUI的工作流设置为空请检查是否正确设置')
throw new Error(t('ComfyUI的工作流设置为空请检查是否正确设置'))
}
// 获取选中的工作流
@ -175,12 +172,12 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
(item) => item.id == result.comfyuiSimpleSetting.selectedWorkflow
)
if (selectedWorkflow == null) {
throw new Error('未找到选中的工作流,请检查是否正确设置!!')
throw new Error(t('未找到选中的工作流,请检查是否正确设置!!'))
}
// 判断工作流对应的文件是不是存在
if (!(await CheckFileOrDirExist(selectedWorkflow.workflowPath))) {
throw new Error('本地未找到选中的工作流文件地址,请检查是否正确设置!!')
throw new Error(t('本地未找到选中的工作流文件地址,请检查是否正确设置!!'))
}
result['comfyuiSelectedWorkflow'] = selectedWorkflow
@ -204,7 +201,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
let jsonContentString = await fs.promises.readFile(workflowPath, 'utf-8')
if (!ValidateJson(jsonContentString)) {
throw new Error('工作流文件内容不是有效的JSON格式请检查是否正确设置')
throw new Error(t('工作流文件内容不是有效的JSON格式请检查是否正确设置'))
}
let jsonContent = JSON.parse(jsonContentString)
@ -214,10 +211,10 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
for (const key in jsonContent) {
let element = jsonContent[key]
if (element && element.class_type === 'CLIPTextEncode') {
if (element._meta?.title === '正向提示词') {
if (element._meta?.title === '正向提示词' || element._meta?.title === 'Positive Prompt') {
jsonContent[key].inputs.text = prompt
}
if (element._meta?.title === '反向提示词') {
if (element._meta?.title === '反向提示词' || element._meta?.title === 'Negative Prompt') {
jsonContent[key].inputs.text = negativePrompt
}
}
@ -236,7 +233,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
}
}
} else {
throw new Error('工作流文件内容不是有效的JSON对象格式请检查是否正确设置')
throw new Error(t('工作流文件内容不是有效的JSON对象格式请检查是否正确设置'))
}
let result = JSON.stringify({
prompt: jsonContent
@ -281,7 +278,10 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
errorNode += key + ', '
}
}
let msg = '错误信息:' + resData.error.message + '错误节点:' + errorNode
let msg = t("错误信息:{error},错误节点:{node}", {
error: resData.error.message,
node: errorNode
})
throw new Error(msg)
}
// 没有错误 判断是不是成功
@ -289,7 +289,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
// 成功
return resData
} else {
throw new Error('未知错误未获取到请求ID请检查是否正确设置')
throw new Error(t('未知错误未获取到请求ID请检查是否正确设置'))
}
}
@ -318,7 +318,9 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
status: BookTaskStatus.IMAGE_FAIL
}
)
let errorMsg = `MJ生成图片失败失败信息如下${resData.message}`
let errorMsg = t("ComfyUI生图失败{error}", {
error: resData.message
})
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
{
@ -365,7 +367,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
messageId: promptId,
action: MJAction.IMAGINE,
status: 'running',
message: '任务正在执行中'
message: t('任务正在执行中')
}
)
@ -376,7 +378,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
id: task.bookTaskDetailId as string,
data: {
status: 'running',
message: '任务正在执行中',
message: t('任务正在执行中'),
id: task.bookTaskDetailId
}
},
@ -414,18 +416,18 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
messageId: promptId,
action: MJAction.IMAGINE,
status: 'success',
message: 'ComfyUI 生成图片成功'
message: t("ComfyUI 生成图片成功!")
}
)
SendReturnMessage(
{
code: 1,
message: 'ComfyUI 生成图片成功',
message: t("ComfyUI 生成图片成功!"),
id: task.bookTaskDetailId as string,
data: {
status: 'success',
message: 'ComfyUI 生成图片成功',
message: t("ComfyUI 生成图片成功!"),
id: task.bookTaskDetailId,
outImagePath: res.outImagePath + '?t=' + new Date().getTime(),
subImagePath: res.subImagePath.map((item) => item + '?t=' + new Date().getTime())
@ -456,10 +458,10 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
comfyUISettingCollection: SettingModal.ComfyUISettingCollection
): Promise<any> {
if (isEmpty(promptId)) {
throw new Error('未获取到请求ID请检查是否正确设置')
throw new Error(t("ComfyUI生图失败未获取到请求ID请检查是否正确设置"))
}
if (isEmpty(comfyUISettingCollection.comfyuiSimpleSetting.requestUrl)) {
throw new Error('未获取到ComfyUI的请求地址请检查是否正确设置')
throw new Error(t('未获取到ComfyUI的请求地址请检查是否正确设置'))
}
let url = comfyUISettingCollection.comfyuiSimpleSetting.requestUrl?.replace(
@ -489,7 +491,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
return {
progress: 0,
status: 'in_progress',
message: '任务正在执行中'
message: t('任务正在执行中')
}
}
let completed = data.status?.completed
@ -514,7 +516,7 @@ export class ComfyUIServiceHandle extends SDServiceHandle {
return {
progress: 0,
status: 'error',
message: '生图失败,详细失败信息看启动器控制台'
message: t('ComfyUI 生图失败,详细失败信息看启动器控制台')
}
}
}

View File

@ -15,6 +15,7 @@ import { MJAction, MJRespoonseType } from '@/define/enum/mjEnum'
import { MJ } from '@/define/model/mj'
import { ImageGenerateMode } from '@/define/data/mjData'
import { errorMessage, SendReturnMessage, successMessage } from '@/public/generalTools'
import { t } from '@/i18n'
export class FluxServiceHandle extends SDServiceHandle {
constructor() {
@ -28,18 +29,14 @@ export class FluxServiceHandle extends SDServiceHandle {
// 开始生图
await this.GetSDImageSetting()
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
task.bookTaskDetailId as string
task.bookTaskDetailId as string, true
)
if (bookTaskDetail == null) {
throw new Error('未找到对应的分镜')
}
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
bookTaskDetail.bookTaskId as string, true
)
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
throw new Error('未找到对应的小说')
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string, true)
// 调用方法合并提示词
let mergeRes = await this.MergeSDPrompt(
@ -159,7 +156,7 @@ export class FluxServiceHandle extends SDServiceHandle {
status: 'success',
outImagePath: outImagePath + '?t=' + new Date().getTime(),
subImagePath: subImagePath.map((item) => item + '?t=' + new Date().getTime()),
message: 'FLUX FORGE 生成图片成功'
message: t("FLUX FORGE 生成图片成功!")
} as MJ.MJResponseToFront
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
@ -168,7 +165,7 @@ export class FluxServiceHandle extends SDServiceHandle {
SendReturnMessage(
{
code: 1,
message: 'FLUX FORGE 生成图片成功',
message: t("FLUX FORGE 生成图片成功!"),
id: bookTaskDetail.id as string,
data: {
...resp
@ -178,11 +175,13 @@ export class FluxServiceHandle extends SDServiceHandle {
)
return successMessage(
resp,
'FLUX FORGE 生成图片成功',
t("FLUX FORGE 生成图片成功!"),
'FluxServiceHandle_FluxForgeImageGenerate'
)
} catch (error: any) {
let errorMsg = 'FLUX FORGE 生成图片失败,错误信息如下:' + error.toString()
let errorMsg = t("FLUX FORGE 生成图片失败,{error}", {
error: error.message
})
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId as string, {
mjApiUrl: this.sdImageSetting.requestUrl,
progress: 0,

View File

@ -32,6 +32,7 @@ import { getProjectPath } from '../option/optionCommonService'
import { MJRespoonseType } from '@/define/enum/mjEnum'
import { ImageGenerateMode } from '@/define/data/mjData'
import { MJ } from '@/define/model/mj'
import { t } from '@/i18n'
export class SDServiceHandle extends SDBasic {
presetBasicService!: PresetBasicService
@ -91,7 +92,7 @@ export class SDServiceHandle extends SDBasic {
bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataByCondition({
bookTaskId: id
})
bookTask = await this.bookTaskService.GetBookTaskDataById(id)
bookTask = await this.bookTaskService.GetBookTaskDataById(id, true)
// 判断是不是有为空的
let emptyName = [] as string[]
for (let i = 0; i < bookTaskDetail.length; i++) {
@ -101,30 +102,28 @@ export class SDServiceHandle extends SDBasic {
}
}
if (emptyName.length > 0) {
throw new Error(`${emptyName.join('')} 的提示词为空,请先推理`)
throw new Error(t("{emptyName} 的提示词为空,请先推理提示词", {
emptyName: emptyName.join('')
}))
}
} else if (operateBookType == OperateBookType.BOOKTASKDETAIL) {
let tempBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id)
if (tempBookTaskDetail == null) {
throw new Error('未找到对应的分镜')
}
let tempBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(id, true)
if (isEmpty(tempBookTaskDetail.gptPrompt)) {
throw new Error('当前分镜没有推理提示词,请先生成')
throw new Error(t("{emptyName} 的提示词为空,请先推理提示词", {
emptyName: tempBookTaskDetail.name
}))
}
bookTaskDetail = [tempBookTaskDetail]
bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail[0].bookTaskId as string
bookTaskDetail[0].bookTaskId as string, true
)
} else {
throw new Error('未知的合并类型')
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
throw new Error('未找到对应的小说')
throw new Error(t('未知的合并类型'))
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string, true)
// 获取SD的通用前缀
let sdGlobalPrompt = this.sdImageSetting.positivePrompt
let result: any[] = [] // 返回前端的数据数组
for (let i = 0; i < bookTaskDetail.length; i++) {
@ -213,9 +212,11 @@ export class SDServiceHandle extends SDBasic {
})
}
return successMessage(result, 'SD和并提示词数据成功', 'SDOpt_MergePrompt')
return successMessage(result, t("SD合并提示词成功"), 'SDOpt_MergePrompt')
} catch (error: any) {
return errorMessage('SD合并提示词错误信息如下' + error.toString(), 'SDOpt_MergePrompt')
return errorMessage(t("SD合并提示词失败{error}", {
error: (error as Error).message
}), 'SDOpt_MergePrompt')
}
}
@ -256,18 +257,14 @@ export class SDServiceHandle extends SDBasic {
// 开始生图
await this.GetSDImageSetting()
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
task.bookTaskDetailId as string
task.bookTaskDetailId as string, true
)
if (bookTaskDetail == null) {
throw new Error('未找到对应的分镜')
}
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string
bookTaskDetail.bookTaskId as string, true
)
let book = await this.bookService.GetBookDataById(bookTask.bookId as string)
if (book == null) {
throw new Error('未找到对应的小说')
}
let book = await this.bookService.GetBookDataById(bookTask.bookId as string, true)
// 调用方法合并提示词
let mergeRes = await this.MergeSDPrompt(
@ -383,7 +380,7 @@ export class SDServiceHandle extends SDBasic {
status: 'success',
outImagePath: outImagePath + '?t=' + new Date().getTime(),
subImagePath: subImagePath.map((item) => item + '?t=' + new Date().getTime()),
message: 'SD生成图片成功'
message: t('SD生成图片成功')
} as MJ.MJResponseToFront
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(
task.bookTaskDetailId as string,
@ -392,7 +389,7 @@ export class SDServiceHandle extends SDBasic {
SendReturnMessage(
{
code: 1,
message: 'SD生成图片成功',
message: t('SD生成图片成功'),
id: bookTaskDetail.id as string,
data: {
...resp
@ -400,9 +397,11 @@ export class SDServiceHandle extends SDBasic {
},
task.messageName as string
)
return successMessage(resp, 'SD生成图片成功', 'SDServiceHandle_SDImageGenerate')
return successMessage(resp, t('SD生成图片成功'), 'SDServiceHandle_SDImageGenerate')
} catch (error: any) {
let errorMsg = 'SD生成图片失败错误信息如下' + error.toString()
let errorMsg = t("SD生成图片失败{error}", {
error: (error as Error).message
})
this.bookTaskDetailService.UpdateBookTaskDetailMjMessage(task.bookTaskDetailId as string, {
mjApiUrl: sdSetting ? this.sdImageSetting.requestUrl : '',
progress: 0,

View File

@ -7,6 +7,7 @@ import fs from 'fs'
import { ValidateJson } from '@/define/Tools/validate'
import { isEmpty } from 'lodash'
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
import { t } from '@/i18n'
export class SettingService {
constructor() { }
@ -34,12 +35,12 @@ export class SettingService {
)
let rootMetaInfoPath = path.resolve(defaultJianyingDraftPath, 'root_meta_info.json')
if (!(await CheckFileOrDirExist(rootMetaInfoPath))) {
throw new Error('未找到剪映相关数据,请手动填写或选择')
throw new Error(t("未找到剪映相关数据,请手动填写或选择"))
}
// 读取文件内容,判断是否是剪映的草稿地址
let fileContent = await fs.promises.readFile(rootMetaInfoPath, 'utf-8')
if (!ValidateJson(fileContent)) {
throw new Error('剪映草稿地址数据错误,请手动填写或选择')
throw new Error(t('剪映草稿地址数据错误,请手动填写或选择'))
}
let jsonContent = JSON.parse(fileContent)
let all_draft_store = jsonContent.all_draft_store
@ -49,18 +50,20 @@ export class SettingService {
if (draft_root_path && !isEmpty(draft_root_path)) {
return successMessage(
draft_root_path,
'成功',
t('成功'),
'SettingService_GetDefaultJianyingDraftPath'
)
} else {
throw new Error('剪映草稿地址数据错误,请手动填写或选择')
throw new Error(t('剪映草稿地址数据错误,请手动填写或选择'))
}
} else {
throw new Error('剪映草稿地址数据错误,请手动填写或选择')
throw new Error(t('剪映草稿地址数据错误,请手动填写或选择'))
}
} catch (error: any) {
return errorMessage(
'获取默认剪映草稿地址失败, ' + error.message,
t("获取默认剪映草稿地址失败,{error}", {
error: error.message
}),
'SettingService_GetDefaultJianyingDraftPath'
)
}

View File

@ -1,15 +1,17 @@
import { dialog, nativeTheme, shell } from 'electron'
import { CheckFileOrDirExist, CopyFileOrFolder } from '../../../define/Tools/file'
import path from 'path'
import fs from 'fs/promises'
import { errorMessage, successMessage } from '../../../public/generalTools'
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
import { t } from '@/i18n'
/** 打开指定的文件夹的方法 */
export type OpenFolderParams = {
/** 是不是基于项目文件,是的话,会在项目文件夹的基础上进行拼接 */
baseProject: boolean
/** 判断是不是打开父文件夹 */
dirFloder: boolean
dirFolder: boolean
/** 文件路径baseProject 为false需要设置完整的文件路径 */
folderPath: string
}
@ -25,10 +27,14 @@ export default class ElectronInterface {
*/
public async OpenFile(value: string): Promise<ErrorItem | SuccessItem> {
if (!(await CheckFileOrDirExist(value))) {
return errorMessage('文件/文件夹 不存在', 'SystemIpc_OPEN_FILE')
return errorMessage(t("目的文件/文件夹不存在,{data}", {
data: value
}), 'SystemIpc_OPEN_FILE')
}
await shell.openPath(value)
return successMessage(null, '打开指定的文件成功', 'SystemIpc_OPEN_FILE')
return successMessage(null, t("打开文件/文件夹成功", {
data: value
}), 'SystemIpc_OPEN_FILE')
}
/**
@ -45,10 +51,12 @@ export default class ElectronInterface {
// 使用更完善的复制方法
await CopyFileOrFolder(source, destination, false)
return successMessage(null, '复制文件夹内容成功', 'SystemIpc_COPY_FOLDER_CONTENTS')
return successMessage(null, t('复制文件夹成功'), 'SystemIpc_COPY_FOLDER_CONTENTS')
} catch (error: any) {
return errorMessage(
'复制文件夹内容错误,错误信息如下:' + error.message,
t("复制文件夹失败,{error}", {
error: error.message
}),
'SystemIpc_COPY_FOLDER_CONTENTS'
)
}
@ -65,7 +73,7 @@ export default class ElectronInterface {
if (params.baseProject) {
openFolder = path.join(global.config.project_path, params.folderPath)
}
if (params.dirFloder) {
if (params.dirFolder) {
openFolder = path.dirname(params.folderPath)
}
if (!openFolder) {
@ -74,12 +82,16 @@ export default class ElectronInterface {
// 判断文件夹是不是存在
let isExist = await CheckFileOrDirExist(openFolder)
if (!isExist) {
throw new Error('文件夹不存在,请检查')
throw new Error(t("目的文件/文件夹不存在,{data}", {
data: openFolder
}))
}
shell.openPath(openFolder)
return successMessage(null, '打开成功')
return successMessage(null, t('打开文件/文件夹成功'))
} catch (error: any) {
return errorMessage('打开文件夹错误,错误信息如下:' + error.message, 'SystemIpc_OPEN_FOLDER')
return errorMessage(t("打卡开文件/文件夹失败,{error}", {
error: error.message
}), 'SystemIpc_OPEN_FOLDER')
}
}
@ -94,12 +106,14 @@ export default class ElectronInterface {
filters: [{ name: 'fileName', extensions: value }]
})
if (filePaths.length === 0) {
throw new Error('没有选择的文件')
throw new Error(t('没有选择的文件或文件夹'))
}
return successMessage(filePaths[0], '选择文件成功', 'SystemIpc_SelectSingleFile')
return successMessage(filePaths[0], t("选择文件/文件夹成功"), 'SystemIpc_SelectSingleFile')
} catch (error: any) {
return errorMessage(
'选择文件错误,错误信息如下:' + error.message,
t("选择文件/文件夹失败,{error}", {
error: error.message
}),
'SystemIpc_SelectSingleFile'
)
}
@ -118,14 +132,16 @@ export default class ElectronInterface {
})
if (filePaths.length === 0) {
throw new Error('没有选择的文件')
throw new Error(t('没有选择的文件或文件夹'))
}
return successMessage(filePaths, '选择文件成功', 'SystemIpc_SelectMultipleFile')
return successMessage(filePaths, t("选择文件/文件夹成功"), 'SystemIpc_SelectMultipleFile')
} catch (error: any) {
console.error('选择文件错误:', error) // 记录错误日志
return errorMessage(
'选择文件错误,错误信息如下:' + error.message,
t("选择文件/文件夹失败,{error}", {
error: error.message
}),
'SystemIpc_SelectMultipleFile'
)
}
@ -156,18 +172,20 @@ export default class ElectronInterface {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory'],
defaultPath: defaultPath,
title: '选择文件夹',
buttonLabel: '选择文件夹'
title: t('选择文件夹'),
buttonLabel: t('选择文件夹')
})
if (filePaths.length === 0) {
throw new Error('没有选择任何文件夹')
throw new Error(t('没有选择的文件或文件夹'))
}
return successMessage(filePaths[0], '选择文件夹成功', 'SystemIpc_SelectSingleFolder')
return successMessage(filePaths[0], t("选择文件/文件夹成功"), 'SystemIpc_SelectSingleFolder')
} catch (error: any) {
return errorMessage(
'选择文件夹错误,错误信息如下:' + error.message,
t("选择文件/文件夹失败,{error}", {
error: error.message
}),
'SystemIpc_SelectSingleFolder'
)
}
@ -183,15 +201,15 @@ export default class ElectronInterface {
// 使用消息框让用户选择类型
const choice = await dialog.showMessageBox({
type: 'question',
title: '选择类型',
message: '请选择要选择的类型:',
buttons: ['选择文件', '选择文件夹', '取消'],
title: t('选择类型'),
message: t('请选择要选择的类型:'),
buttons: [t('选择文件'), t('选择文件夹'), t('取消')],
defaultId: 0,
cancelId: 2
})
if (choice.response === 2) {
throw new Error('用户取消选择')
throw new Error(t("取消操作"))
}
if (choice.response === 0) {
@ -205,31 +223,33 @@ export default class ElectronInterface {
{ name: 'All Files', extensions: ['*'] }
]
: [{ name: 'All Files', extensions: ['*'] }],
title: '选择文件'
title: t('选择文件')
})
if (result.filePaths.length === 0) {
throw new Error('没有选择文件')
throw new Error(t('没有选择文件或文件夹'))
}
return successMessage(result.filePaths[0], '选择文件成功', 'SystemIpc_SelectFolderOrFile')
return successMessage(result.filePaths[0], t("选择文件/文件夹成功"), 'SystemIpc_SelectFolderOrFile')
} else {
// 选择文件夹
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
title: '选择文件夹'
title: t('选择文件夹')
})
if (result.filePaths.length === 0) {
throw new Error('没有选择文件夹')
throw new Error(t('没有选择的文件或文件夹'))
}
return successMessage(result.filePaths[0], '选择文件夹成功', 'SystemIpc_SelectFolderOrFile')
return successMessage(result.filePaths[0], t("选择文件/文件夹成功"), 'SystemIpc_SelectFolderOrFile')
}
} catch (error: any) {
console.error('选择文件或文件夹错误:', error)
return errorMessage(
'选择文件或文件夹错误,错误信息如下:' + error.message,
t("选择文件/文件夹失败,{error}", {
error: error.message
}),
'SystemIpc_SelectFolderOrFile'
)
}
@ -242,4 +262,60 @@ export default class ElectronInterface {
public OpenUrl(url: string) {
shell.openExternal(url)
}
/**
*
* @param filePath
* @returns
*/
public async ReadTextFile(filePath: string): Promise<SuccessItem | ErrorItem> {
try {
// 定义支持的文本文件格式
const supportedExtensions = [
'.txt', '.json', '.xml', '.html', '.htm', '.css', '.js', '.ts',
'.jsx', '.tsx', '.vue', '.md', '.yml', '.yaml', '.csv', '.log',
'.ini', '.conf', '.config', '.py', '.java', '.c', '.cpp', '.h',
'.php', '.rb', '.go', '.rs', '.swift', '.kt', '.scala', '.sql',
'.sh', '.bat', '.ps1', '.dockerfile', '.gitignore', '.env'
]
// 检查文件是否存在
if (!(await CheckFileOrDirExist(filePath))) {
throw new Error(t('文件不存在'))
}
// 获取文件扩展名
const ext = path.extname(filePath).toLowerCase()
// 检查文件格式是否支持
if (!supportedExtensions.includes(ext)) {
throw new Error(t("不支持的文件格式: {ext}。支持的格式: {supportedExt}", {
ext: ext,
supportedExt: supportedExtensions.join(', ')
}))
}
// 读取文件内容
const content = await fs.readFile(filePath, 'utf-8')
return successMessage(
{
content: content,
filePath: filePath,
size: Buffer.byteLength(content, 'utf-8'),
extension: ext
},
t('读取文件成功!'),
'SystemIpc_ReadTextFile'
)
} catch (error: any) {
console.error('读取文件错误:', error)
return errorMessage(
t("读取文件失败,{error}", {
error: error.message
}),
'SystemIpc_ReadTextFile'
)
}
}
}

View File

@ -1,3 +1,4 @@
import { t } from '@/i18n'
import { errorMessage } from '@/public/generalTools'
export class UserSoftware {
@ -19,7 +20,7 @@ export class UserSoftware {
}
console.log('授权信息', global.am)
} catch (error) {
errorMessage('同步授权信息失败', 'SystemIpc_SyncAuthorization')
errorMessage(t("同步授权信息失败"), 'SystemIpc_SyncAuthorization')
}
}
}

View File

@ -1,4 +1,4 @@
import { BookBackTaskStatus, BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
import { BookBackTaskStatus } from '@/define/enum/bookEnum'
import { TaskServiceHandle } from './taskServiceHandle'
import { TaskModal } from '@/define/model/task'
import { Book } from '@/define/model/book/book'
@ -13,22 +13,7 @@ export class TaskHandle {
await this.taskServiceHandle.StartTaskQueue(isGiveUp)
/** 添加单个个任务 */
AddOneTask = async (
bookId: string,
taskType: BookBackTaskType,
executeType: TaskExecuteType = TaskExecuteType.AUTO,
bookTaskId: string | undefined = undefined,
bookTaskDetailId: string | undefined = undefined,
responseMessageName?: string
) =>
await this.taskServiceHandle.AddOneTask(
bookId,
taskType,
executeType,
bookTaskId,
bookTaskDetailId,
responseMessageName
)
AddOneTask = async (task: TaskModal.Task) => await this.taskServiceHandle.AddOneTask(task)
/** 添加多个任务 */
AddMultiTask = async (tasks: TaskModal.Task[]) => await this.taskServiceHandle.AddMultiTask(tasks)

View File

@ -9,6 +9,8 @@ import { SDHandle } from '../sd'
import { OptionRealmService } from '@/define/db/service/optionService'
import { OptionKeyName } from '@/define/enum/option'
import { optionSerialization } from '../option/optionSerialization'
import { bookHandle } from '../book'
import { t } from '@/i18n'
export class TaskManager {
isExecuting: boolean = false
@ -352,19 +354,18 @@ export class TaskManager {
// }
/** 添加图片转视频后台人物 */
// async AddImageToVideo(task: TaskModal.Task) {
// let batch = task.messageName
// global.taskQueue.enqueue(
// async () => {
// this.videoGlobal = new VideoGlobal()
// await this.videoGlobal.ImageToVideo(task)
// },
// `${batch}_${task.id}`,
// batch,
// `${batch}_${task.id}_${new Date().getTime()}`,
// this.taskListService.SetMessageNameTaskToFail
// )
// }
async AddImageToVideo(task: TaskModal.Task) {
let batch = task.messageName
global.taskQueue.enqueue(
async () => {
await bookHandle.MediaToVideo(task)
},
`${batch}_${task.id}`,
batch,
`${batch}_${task.id}_${new Date().getTime()}`,
this.taskListService.SetMessageNameTaskToFail
)
}
// /**
// * 添加 FLUX api 到内存队列中
@ -436,11 +437,13 @@ export class TaskManager {
// case BookBackTaskType.RUNWAY_VIDEO:
// case BookBackTaskType.LUMA_VIDEO:
// case BookBackTaskType.KLING_VIDEO:
// this.AddImageToVideo(task)
// break
case BookBackTaskType.MJ_VIDEO:
case BookBackTaskType.MJ_VIDEO_EXTEND:
this.AddImageToVideo(task)
break
default:
throw new Error('未知的任务类型')
throw new Error(t('未知的任务类型'))
}
// 是不是要添加自动任务
// await this.AddTaskHandle(task, true);
@ -452,7 +455,10 @@ export class TaskManager {
})
return successMessage(
updateRes,
`${task.name}_${task.id} 任务添加调度完成`,
t("{taskName}_{taskId} 任务添加调度完成", {
taskName: task.name,
taskId: task.id
}),
'TaskManager_AddQueue'
)
} catch (error: any) {
@ -460,11 +466,15 @@ export class TaskManager {
this.taskListService.UpdateTaskStatus({
id: task.id as string,
status: BookBackTaskStatus.FAIL,
errorMessage: '任务调度失败,请手动重试'
errorMessage: t('任务调度失败,请手动重试')
})
return errorMessage(
`处理 ${task.type} 类型任务 ${task.name} 失败,失败信息如下:${error.message}`,
t("处理 {taskType} 类型任务 {taskName} 失败,失败信息如下,{error}", {
taskType: task.type,
taskName: task.name,
error: error.message
}),
'TaskManager_handleTask'
)
}

View File

@ -1,10 +1,11 @@
import { TaskListService } from '@/define/db/service/book/taskListService'
import { BookBackTaskStatus, BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
import { BookBackTaskStatus, BookBackTaskType } from '@/define/enum/bookEnum'
import { GeneralResponse } from '@/define/model/generalResponse'
import { errorMessage, successMessage } from '@/public/generalTools'
import { TaskManager } from './taskManage'
import { TaskModal } from '@/define/model/task'
import { Book } from '@/define/model/book/book'
import { t } from '@/i18n'
export class TaskServiceHandle {
taskListService!: TaskListService
@ -55,61 +56,64 @@ export class TaskServiceHandle {
await global.taskManager.InitListeners() // 启动监听
// 重新设置
}
return successMessage(null, '启动后台任务成功', 'BackTaskService_StartBackTask')
return successMessage(null, t('启动后台任务成功'), 'BackTaskService_StartBackTask')
} catch (error: any) {
return errorMessage('启动任务队列失败,' + error.message, 'TaskServiceHandle.StartTaskQueue')
return errorMessage(t("启动后台任务失败,{error}", {
error: error.message
}), 'TaskServiceHandle.StartTaskQueue')
}
}
/**
*
*
*
*
*
*
*
*
*
* @param {string} bookId - ID
* @param {BookBackTaskType} taskType -
* @param {TaskExecuteType} [executeType=TaskExecuteType.AUTO] -
* @param {string | null} [bookTaskId=null] - ID
* @param {string | null} [bookTaskDetailId=null] - ID
* @param {string} [responseMessageName] -
* @param {TaskModal.Task} task -
* - bookId: 书籍ID
* - type:
* - executeType: 执行类型
* - bookTaskId: 关联的书籍任务ID
* - bookTaskDetailId: 关联的分镜ID
* - messageName: 任务完成后通知的消息通道名称
*
* @returns {Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem>}
* ID
*
* - ID和成功消息
* -
*
* @throws {Error}
*
* @example
* // 添加一个自动执行的MJ图像生成任务
* const result = await taskService.AddBookBackTask(
* "book-123",
* BookBackTaskType.MJ_GENERATE_IMAGE,
* TaskExecuteType.AUTO,
* "booktask-456",
* "detail-789",
* "mj-channel"
* );
* // 添加一个自动执行的图像生成任务
* const result = await taskService.AddOneTask({
* bookId: "book-123",
* type: BookBackTaskType.MJ_GENERATE_IMAGE,
* executeType: TaskExecuteType.AUTO,
* bookTaskId: "task-456",
* bookTaskDetailId: "detail-789",
* messageName: "task-channel"
* });
*/
async AddOneTask(
bookId: string,
taskType: BookBackTaskType,
executeType: TaskExecuteType = TaskExecuteType.AUTO,
bookTaskId: string | undefined = undefined,
bookTaskDetailId: string | undefined = undefined,
responseMessageName?: string
task: TaskModal.Task
): Promise<GeneralResponse.ErrorItem | GeneralResponse.SuccessItem> {
try {
await this.InitTaskServiceHandle()
let res = this.taskListService.AddOneTask(
bookId,
taskType,
executeType,
bookTaskId,
bookTaskDetailId,
responseMessageName
task.bookId as string,
task.type as BookBackTaskType,
task.executeType,
task.bookTaskId,
task.bookTaskDetailId,
task.messageName
)
return successMessage(res, '添加后台任务成功', 'TaskServiceHandle.AddBookBackTask')
return successMessage(res, t('添加后台任务成功'), 'TaskServiceHandle.AddBookBackTask')
} catch (error: any) {
return errorMessage('添加后台任务失败,' + error.message, 'TaskServiceHandle.AddBookBackTask')
return errorMessage(t("添加后台任务失败,{error}", {
error: error.message
}), 'TaskServiceHandle.AddBookBackTask')
}
}
@ -162,10 +166,12 @@ export class TaskServiceHandle {
element.messageName
)
}
return successMessage(null, `添加多个任务成功`, 'TaskIpc_AddMultiBookBackTask')
return successMessage(null, t(`添加多个任务成功`), 'TaskIpc_AddMultiBookBackTask')
} catch (error: any) {
return errorMessage(
'添加多个后台任务失败,' + error.message,
t("添加多个任务失败,{error}", {
error: error.message
}),
'TaskServiceHandle.AddMultiTask'
)
}
@ -203,12 +209,14 @@ export class TaskServiceHandle {
let res = this.taskListService.GetAssignStatusTaskCount(status)
return successMessage(
res,
'获取指定状态的任务成功',
t('获取指定状态的任务成功'),
'TaskServiceHandle.GetAssignStatusTaskCount'
)
} catch (error: any) {
return errorMessage(
'获取指定状态的任务失败,' + error.message,
t("获取指定状态的任务失败,{error}", {
error: error.message
}),
'TaskServiceHandle.GetAssignStatusTaskCount'
)
}
@ -244,10 +252,12 @@ export class TaskServiceHandle {
try {
await this.InitTaskServiceHandle()
let res = this.taskListService.GetTaskCollection(queryTaskCondition)
return successMessage(res, '获取后台任务集合成功', 'TaskServiceHandle.GetTaskCollection')
return successMessage(res, t('获取后台任务集合成功'), 'TaskServiceHandle.GetTaskCollection')
} catch (error: any) {
return errorMessage(
'获取后台任务集合失败,' + error.message,
t("获取后台任务集合失败,{error}", {
error: error.message
}),
'TaskServiceHandle.GetTaskCollection'
)
}
@ -278,10 +288,12 @@ export class TaskServiceHandle {
try {
await this.InitTaskServiceHandle()
let res = this.taskListService.UpdateTaskStatus(bookBackTask)
return successMessage(res, '更新后台任务状态成功', 'TaskServiceHandle.UpdateTaskStatus')
return successMessage(res, t('更新后台任务状态成功'), 'TaskServiceHandle.UpdateTaskStatus')
} catch (error: any) {
return errorMessage(
'更新后台任务状态失败,' + error.message,
t("修改后台任务失败,{error}", {
error: error.message
}),
'TaskServiceHandle.UpdateTaskStatus'
)
}

View File

@ -17,6 +17,7 @@ import { optionSerialization } from '../option/optionSerialization'
import { SettingModal } from '@/define/model/setting'
import { GetApiDefineDataById } from '@/define/data/apiData'
import { isEmpty } from 'lodash'
import { t } from '@/i18n'
export class TranslateCommon {
/** 请求的地址 */
@ -104,7 +105,7 @@ export class TranslateCommon {
let from = value.from
let to = value.to
if (value.isSplit) {
throw new Error('使用GPT翻译不支持拆分')
throw new Error(t('使用GPT翻译不支持拆分'))
}
let model = this.translationAppId
let token = this.translationSecret
@ -189,7 +190,7 @@ export class TranslateCommon {
"In a modern city, a streamlined car is parked on the street. A man in his thirties, with short brown hair combed back, a calm, confident look, tall and thin in a clean white shirt and black pants, sits in the car. The interior of the car is clean and modern, and the background is blurred to highlight the man's calm demeanor. The man's cell phone is ringing. The scene is set in the present."
})
} else {
throw new Error('GPT翻译只支持中英互译')
throw new Error(t('GPT翻译只支持中英互译'))
}
data.messages.push({
@ -228,7 +229,11 @@ export class TranslateCommon {
to: to,
data: res_data
},
`GPT${from == 'en' ? '英' : '中'}${from == 'en' ? '英' : '中'}翻译成功`,
t("AI翻译 {source} 译 {target} 成功", {
source: from == 'en' ? '英' : '中',
target: to == 'en' ? '英' : '中'
})
,
'Translate_TranslateReturnNowGPT'
)
} catch (error) {

View File

@ -12,6 +12,7 @@ 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 { t } from '@/i18n'
/**
*
@ -123,7 +124,7 @@ export class TranslateServiceHandle {
switch (value.type) {
case TranslateType.REVERSE_PROMPT_TRANSLATE:
if (value.reversePromptId == null || isEmpty(value.reversePromptId)) {
throw new Error('反推提示词的ID不能为空')
throw new Error(t('反推提示词的ID不能为空'))
}
await this.TranslateProcessReversePrompt(
value.bookTaskDetailId as string,
@ -142,7 +143,7 @@ export class TranslateServiceHandle {
)
break
default:
throw new Error('未知的翻译类型')
throw new Error(t('未知的翻译类型'))
}
}
@ -168,7 +169,7 @@ export class TranslateServiceHandle {
)
let generalSetting = optionSerialization<SettingModal.GeneralSettings>(
generalSettingOption,
'设置 -> 通用设置'
t('设置 -> 通用设置')
)
for (let i = 0; i < value.length; i++) {
@ -182,7 +183,7 @@ export class TranslateServiceHandle {
let srcString = ''
if (element.isSplit) {
// let dstStrs = []
throw new Error('拆分翻译的暂时不支持')
throw new Error(t('使用GPT翻译不支持拆分'))
} else {
// 没有拆分的,只有一句
srcString = data.data[0].dst
@ -217,10 +218,12 @@ export class TranslateServiceHandle {
}
await ExecuteConcurrently(tasks, global.am.isPro ? (generalSetting.concurrency ?? 1) : 1)
// 将翻译后的数据返回,前端进行修改
return successMessage(null, '全部翻译完成', 'TranslateService_TranslateNowReturn')
return successMessage(null, t('全部翻译完成'), 'TranslateService_TranslateNowReturn')
} catch (error: any) {
return errorMessage(
'翻译失败,失败信息如下:' + error.toString(),
t("翻译失败,{error}", {
error: error.message
}),
'TranslateService_TranslateNowReturn'
)
}

View File

@ -0,0 +1,15 @@
import { TaskModal } from '@/define/model/task'
import { MJVideoService } from './mjVideo'
export class VideoHandle {
mjVideoService: MJVideoService
// 这里可以添加 VideoHandle 特有的方法
constructor() {
// mixin 装饰器会处理初始化
this.mjVideoService = new MJVideoService()
}
/** MJ图片转视频处理方法 将指定的图片通过Midjourney API转换为视频 */
MJImageToVideo(task: TaskModal.Task) {
return this.mjVideoService.MJImageToVideo(task)
}
}

View File

@ -0,0 +1,30 @@
import { TaskModal } from '@/define/model/task'
import { MJBasic } from '../mj/mjBasic'
/**
* Luma
* Luma AI
*/
export class LumaVideoService extends MJBasic {
constructor() {
super()
}
/**
* Luma图片转视频处理方法
* @param task
*/
async LumaImageToVideo(task: TaskModal.Task) {
// TODO: 实现 Luma 图片转视频功能
console.log('LumaImageToVideo called with task:', task.id)
}
/**
* Luma文本转视频处理方法
* @param task
*/
async LumaTextToVideo(task: TaskModal.Task) {
// TODO: 实现 Luma 文本转视频功能
console.log('LumaTextToVideo called with task:', task.id)
}
}

View File

@ -0,0 +1,442 @@
import { TaskModal } from '@/define/model/task'
import { MJApiService } from '../mj/mjApiService'
import { ImageGenerateMode } from '@/define/data/mjData'
import { cloneDeep, isEmpty } from 'lodash'
import { t } from '@/i18n'
import { ValidateJson } from '@/define/Tools/validate'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import {
MappingTaskTypeToVideoModel,
MJVideoBatchSize,
MJVideoMotion,
MJVideoType,
VideoStatus
} from '@/define/enum/video'
import axios from 'axios'
import { SendReturnMessage, successMessage } from '@/public/generalTools'
import { ResponseMessageType } from '@/define/enum/softwareEnum'
import { Book } from '@/define/model/book/book'
import { BookBackTaskStatus, BookTaskStatus } from '@/define/enum/bookEnum'
import path from 'path'
import { CheckFolderExistsOrCreate, CopyFileOrFolder } from '@/define/Tools/file'
import { DownloadFile } from '@/define/Tools/common'
import { getProjectPath } from '../option/optionCommonService'
export class MJVideoService extends MJApiService {
constructor() {
super()
}
//#region MJImageToVideo
/**
* MJ图片转视频处理方法
* Midjourney API转换为视频
* @param task ID等信息
* @returns Promise<void>
* @throws API调用失败时抛出异常
*/
async MJImageToVideo(task: TaskModal.Task) {
try {
await this.InitMJBasic()
// 加载设置
await this.InitMJSetting(ImageGenerateMode.MJ_API)
// 检查是否支持视频功能
if (this.videoUrl == null || isEmpty(this.videoUrl)) {
throw new Error(t('当前Midjourney模式不支持视频生成功能请更换为MJ API或本地代理模式后重试'))
}
// 检查 token
if (this.token == null || isEmpty(this.token)) {
throw new Error(
t("对应Midjourney模式的Token不能为空请前往 {settingPath} 中配置", { settingPath: t("设置 -> MJ设置") })
)
}
// 开始处理小说数据
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
task.bookTaskDetailId as string, true
)
// 获取视频配置信息
let videoMessage = bookTaskDetail.videoMessage
if (videoMessage == null || videoMessage == undefined) {
throw new Error(t("小说批次任务的分镜数据的转视频配置为空,请检查"))
}
// 获取 MJ Video 的options
let mjVideoOptionsString = bookTaskDetail.videoMessage?.mjVideoOptions as string
if (!ValidateJson(mjVideoOptionsString)) {
throw new Error(t("当前分镜数据的MJ图转视频参数为空或参数校验失败请检查"))
}
let mjVideoOptions: BookTaskDetail.MjVideoOptions = JSON.parse(mjVideoOptionsString)
let imageUrl = videoMessage.imageUrl?.trim() || mjVideoOptions.image?.trim() || ''
let prompt = videoMessage.prompt?.trim()
let motion: MJVideoMotion =
mjVideoOptions.motion === MJVideoMotion.High ? MJVideoMotion.High : MJVideoMotion.Low
let videoType = mjVideoOptions.videoType ?? MJVideoType.HD
let batchSize = mjVideoOptions.batchSize ?? MJVideoBatchSize.FOUR
let endImageUrl = mjVideoOptions.endImageUrl?.trim() || ''
let loop = mjVideoOptions.loop ?? false
let raw = mjVideoOptions.raw ?? false
// 判断 图片是不是网络图片,不是网络图片的话判断当前图片再本地是不是存在,存在的话讲图片转为 base64
if (
!imageUrl.startsWith('http') ||
(!isEmpty(endImageUrl) && !endImageUrl.startsWith('http'))
) {
throw new Error(t("不支持的图片链接,仅支持网络图片链接!"))
}
// 在提示词后面添加 --raw
if (!isEmpty(prompt) && raw) {
prompt = prompt + ' --raw'
}
prompt = imageUrl + ' ' + prompt
// 添加 批次信息
prompt = prompt + ' --bs ' + batchSize
if (loop) {
prompt = prompt + ' --end loop'
} else {
if (!isEmpty(endImageUrl)) {
prompt = prompt + ' --end ' + endImageUrl
}
}
let body: {
prompt: string
motion?: MJVideoMotion
videoType: MJVideoType
} = {
prompt: prompt,
motion: motion,
videoType: videoType
}
// 开始请求
let res = await axios.post(this.videoUrl, body, {
headers: {
Authorization: this.token
}
})
console.log('MJImageToVideo response', res.data)
let resData = res.data
let id = resData.result
// 修改Task, 将数据写入
this.taskListService.UpdateBackTaskData(task.id as string, {
taskId: id as string,
taskMessage: JSON.stringify(resData)
})
// 修改videoMessage
videoMessage.taskId = id
videoMessage.status = VideoStatus.WAIT
videoMessage.messageData = JSON.stringify(resData)
videoMessage.msg = ''
delete videoMessage.imageUrl // 不要修改原本的图片地址
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
videoMessage
)
// 添加任务成功 返回前端任务事件
SendReturnMessage(
{
code: 1,
id: task.bookTaskDetailId as string,
message: t('已成功提交Midjourney图转视频任务任务ID{taskId}', { taskId: id }),
type: ResponseMessageType.MJ_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
// 开始循环查询任务状态
await this.FetchMJVideoResult(bookTaskDetail, task, id)
return successMessage(
t('Midjourney图转视频任务执行完成。'),
'MJVideoService_MJImageToVideo'
)
} catch (error: any) {
throw new Error(
t('Midjourney图转视频任务执行失败失败信息如下{error}', { error: error.message })
)
}
}
//#endregion
//#region FetchMJVideoResult
/**
* MJ视频生成结果
*
* MJ视频任务的状态
*
*
* @param {Book.SelectBookTaskDetail} bookTaskDetail -
* @param {TaskModal.Task} task -
* @param {string} taskId - MJ API返回的任务ID
* @param {boolean} useTransfer - 使false使
*
* @returns {Promise<void>} Promise
*
* @throws {Error} API调用失败时抛出异常
*
* @description
*
* 1. MJ API查询任务状态
* 2.
* - failure/cancel: 标记为失败
* - success且进度100%: URL
* - 其他状态: 标记为处理中
* 3.
* - videoMessage到数据库
* -
* -
* 4. 20
*
* @example
* await this.FetchMJVideoResult(bookTaskDetail, task, "task_123456");
*/
async FetchMJVideoResult(
bookTaskDetail: Book.SelectBookTaskDetail,
task: TaskModal.Task,
taskId: string,
useTransfer: boolean = false
) {
console.log(useTransfer)
while (true) {
let fetchUrl = this.fetchTaskUrl.replace('${id}', taskId)
let res = await axios.get(fetchUrl, {
headers: {
Authorization: this.token
}
})
console.log('FetchMJVideoResult response', res.data)
let resData = res.data
// 判断状态
let status = resData.status.toLowerCase()
let code = status == 'failure' || status == 'cancel' ? 0 : 1
let progress =
resData.progress && resData.progress.length > 0
? parseInt(resData.progress.slice(0, -1))
: 0
if (code == 0) {
// 任务失败
// 修改小说分镜的 videoMessage
let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {}
videoMessage.status = VideoStatus.FAIL
videoMessage.msg = resData.failReason
videoMessage.taskId = taskId
videoMessage.messageData = JSON.stringify(resData)
delete videoMessage.imageUrl
// 修改 videoMessage数据
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
bookTaskDetail.id as string,
videoMessage
)
// 修改TASK
this.taskListService.UpdateBackTaskData(task.id as string, {
taskId: taskId,
taskMessage: JSON.stringify(resData)
})
// 返回前端数据
SendReturnMessage(
{
code: 0,
id: bookTaskDetail.id as string,
message: t('Midjourney图转视频任务执行失败失败信息如下{error}', {
error: resData.failReason
}),
type: ResponseMessageType.MJ_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
throw new Error(resData.failReason)
} else {
// 请求成功 但是需要判断状态和返回的进度
if (progress == 100 && status == 'success') {
// 任务成功 修改 videoMessage
let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {}
videoMessage.status = VideoStatus.SUCCESS
videoMessage.taskId = taskId
if (resData.videoUrls && resData.videoUrls.length > 0) {
videoMessage.videoUrls = []
resData.videoUrls.forEach((item: any) => {
videoMessage.videoUrls?.push(item.url)
})
videoMessage.videoUrl = videoMessage.videoUrls[0]
}
videoMessage.messageData = JSON.stringify(resData)
delete videoMessage.imageUrl
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
videoMessage
)
// 修改小说分镜状态
this.bookTaskDetailService.ModifyBookTaskDetailById(task.bookTaskDetailId as string, {
status: BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS
})
// 修改任务状态
this.taskListService.UpdateBackTaskData(task.id as string, {
status: BookBackTaskStatus.DONE,
taskId: taskId,
taskMessage: JSON.stringify(resData)
})
// 下载 视频
await this.DownloadMJVideo(videoMessage.videoUrls || [], task, bookTaskDetail)
SendReturnMessage(
{
code: 1,
id: bookTaskDetail.id as string,
message: t('Midjourney图转视频任务执行完成。'),
type: ResponseMessageType.MJ_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
break
}
}
// 没有失败 没有成功 在执行中
// 再执行中
let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {}
videoMessage.status = VideoStatus.PROCESSING
videoMessage.taskId = taskId
videoMessage.messageData = JSON.stringify(resData)
delete videoMessage.imageUrl
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
videoMessage
)
SendReturnMessage(
{
code: 1,
id: bookTaskDetail.id as string,
message: t('Midjourney图转视频任务执行中...'),
type: ResponseMessageType.MJ_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
// 没有成功 等待二十秒后继续执行
await new Promise((resolve) => setTimeout(resolve, 20000))
}
}
//#endregion
//#region DownloadVideo
/**
*
* @param videoUrl URL地址
* @param savePath
*/
async DownloadMJVideo(
videoUrls: string[],
task: TaskModal.Task,
bookTaskDetail: Book.SelectBookTaskDetail
) {
// 处理完成 开始下载指定的图片
let bookTask = await this.bookTaskService.GetBookTaskDataById(
bookTaskDetail.bookTaskId as string,
true
)
let tempVideoUrls = bookTaskDetail.subVideoPath || []
let newVideoUrls: string[] = []
let outVideoPath: string = ''
const project_path = await getProjectPath()
// 开始下载所有视频
for (let i = 0; i < videoUrls.length; i++) {
const videoUrl = videoUrls[i]
// 处理文件地址和下载
let videoPath = path.join(
bookTask.imageFolder as string,
`video/subVideo/${bookTaskDetail.name}/${new Date().getTime()}_${i}.mp4`
)
await CheckFolderExistsOrCreate(path.dirname(videoPath))
await DownloadFile(videoUrl, videoPath)
// 处理返回数据信息
// 开始修改信息
// 将信息添加到里面
let a = {
localPath: path.relative(project_path, videoPath),
remotePath: videoUrl,
taskId: bookTaskDetail.videoMessage?.taskId,
index: i,
type: MappingTaskTypeToVideoModel(task.type as string)
}
newVideoUrls.push(JSON.stringify(a))
if (i == 0) {
outVideoPath = path.join(
bookTask.imageFolder as string,
'video',
bookTaskDetail.name + path.extname(videoPath)
)
await CopyFileOrFolder(videoPath, outVideoPath as string)
}
}
// 处理
// 开始处理数据
// 将原有的视频路径合并到新数组中
newVideoUrls.push(...tempVideoUrls)
await this.bookTaskDetailService.ModifyBookTaskDetailById(bookTaskDetail.id as string, {
subVideoPath: newVideoUrls,
generateVideoPath: outVideoPath != '' ? outVideoPath : ''
})
let newBookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(
task.bookTaskDetailId as string,
true
)
// 讲数据返回前端
SendReturnMessage(
{
code: 1,
id: task.bookTaskDetailId as string,
message: t('视频生成成功'),
type: ResponseMessageType.VIDEO_SUCESS,
data: JSON.stringify(newBookTaskDetail)
},
task.messageName as string
)
}
//#endregion
}

View File

@ -9,6 +9,7 @@ import { define } from '@/define/define'
import { DEFINE_STRING } from '@/define/ipcDefineString'
import axios from 'axios'
import { GetOpenAISuccessResponse, GetRixApiErrorResponse } from '@/define/response/openAIResponse'
import { t } from '@/i18n'
export class CopyWritingServiceHandle extends BookBasicHandle {
constructor() {
@ -34,17 +35,17 @@ export class CopyWritingServiceHandle extends BookBasicHandle {
let simpleSetting = optionSerialization<SettingModal.CopyWritingSimpleSettings>(
simpleSettingOption,
' 文案处理->设置 '
t('文案处理->设置')
)
if (isEmpty(simpleSetting.gptType) || isEmpty(simpleSetting.gptData)) {
throw new Error('设置数据不完整,请检查提示词类型,提示词预设数据是否完整')
throw new Error(t('设置数据不完整,请检查提示词类型,提示词预设数据是否完整'))
}
let wordStruct = simpleSetting.wordStruct
let filterWordStruct = wordStruct.filter((item) => ids.includes(item.id))
if (filterWordStruct.length === 0) {
throw new Error('没有找到需要处理的文案ID对应的数据请检查数据是否正确')
throw new Error(t('没有找到需要处理的文案ID对应的数据请检查数据是否正确'))
}
let apiSettingOption = this.optionRealmService.GetOptionByKey(
@ -53,10 +54,10 @@ export class CopyWritingServiceHandle extends BookBasicHandle {
let apiSetting = optionSerialization<SettingModal.CopyWritingAPISettings>(
apiSettingOption,
' 文案处理->设置 '
t('文案处理->设置')
)
if (isEmpty(apiSetting.apiKey) || isEmpty(apiSetting.gptUrl) || isEmpty(apiSetting.model)) {
throw new Error('文案处理API设置不完整请检查API地址密钥和模型是否设置正确')
throw new Error(t('文案处理API设置不完整请检查API地址密钥和模型是否设置正确'))
}
return {
@ -118,7 +119,7 @@ export class CopyWritingServiceHandle extends BookBasicHandle {
{
code: 1,
id: wordStruct.id,
message: '文案生成成功',
message: t('文案生成成功'),
data: {
oldWord: wordStruct.oldWord,
newWord: resData
@ -164,7 +165,9 @@ export class CopyWritingServiceHandle extends BookBasicHandle {
// 判断返回的状态,如果是失败的话直接返回错误信息
if (axiosRes.status != 200) {
throw new Error('请求失败')
throw new Error(t("请求失败,状态码:{statusCode}", {
statusCode: axiosRes.status
}))
}
let dataRes = axiosRes.data
if (dataRes.code == 1) {
@ -174,7 +177,7 @@ export class CopyWritingServiceHandle extends BookBasicHandle {
} else {
// 系统报错
if (dataRes.code == 5000) {
throw new Error('系统错误,错误信息如下:' + dataRes.message)
throw new Error(dataRes.message)
} else {
// 处理不同类型的错误消息
throw new Error(GetRixApiErrorResponse(dataRes.data))
@ -185,7 +188,7 @@ export class CopyWritingServiceHandle extends BookBasicHandle {
async CopyWritingAIGeneration(ids: string[]) {
try {
if (ids.length === 0) {
throw new Error('没有需要处理的文案ID')
throw new Error(t('没有需要处理的文案ID'))
}
let { apiSetting, simpleSetting } = await this.getCopyWritingSetting(ids)
@ -193,7 +196,7 @@ export class CopyWritingServiceHandle extends BookBasicHandle {
let wordStruct = simpleSetting.wordStruct
let filterWordStruct = wordStruct.filter((item) => ids.includes(item.id))
if (filterWordStruct.length === 0) {
throw new Error('没有找到需要处理的文案ID对应的数据请检查数据是否正确')
throw new Error(t('没有找到需要处理的文案ID对应的数据请检查数据是否正确'))
}
// 开始循环请求AI
@ -229,7 +232,7 @@ export class CopyWritingServiceHandle extends BookBasicHandle {
{
code: 1,
id: element.id,
message: '文案生成成功',
message: t('文案生成成功'),
data: {
oldWord: element.oldWord,
newWord: returnData
@ -243,12 +246,14 @@ export class CopyWritingServiceHandle extends BookBasicHandle {
// 处理完毕 返回数据。这边不做任何的保存动作
return successMessage(
wordStruct,
'AI处理文案成功',
t('AI处理文案成功'),
'CopywritingAIGenerationService_CopyWritingAIGeneration'
)
} catch (error: any) {
return errorMessage(
'AI处理文案失败失败原因如下' + error.message,
t("AI处理文案失败{error}", {
error: error.message
}),
'CopyWritingServiceHandle_CopyWritingAIGeneration'
)
}

View File

@ -4,6 +4,7 @@ import { bookTaskDetailPreload } from './bookProload/bookTaskDetailPreload'
import { bookPromptPreload } from './bookProload/bookPromptPreload'
import { bookImagePreload } from './bookProload/bookImagePreload'
import { bookExportPreload } from './bookProload/bookExportPreload'
import { bookVideoPreload } from './bookProload/bookVideoPreload'
const book = {
...bookDataPreload,
@ -16,7 +17,11 @@ const book = {
...bookImagePreload,
...bookExportPreload
...bookExportPreload,
video: {
...bookVideoPreload
}
}
export { book }

View File

@ -0,0 +1,27 @@
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { ipcRenderer } from 'electron'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
export const bookVideoPreload = {
/** 获取指定条件的小说图转视频数据,包含子批次 */
GetVideoBookInfoList: async (condition: BookVideo.BookVideoInfoListQuertCondition) => {
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.GET_VIDEO_BOOK_INFO_LIST, condition)
},
/** 初始化视频消息数据 */
InitVideoMessage: async (bookTaskId: string) => {
return await ipcRenderer.invoke(DEFINE_STRING.BOOK.INIT_VIDEO_MESSAGE, bookTaskId)
},
/** 更新小说分镜的视频消息 */
UpdateBookTaskDetailVideoMessage: async (
bookTaskDetailId: string,
videoMessage: Partial<BookTaskDetail.VideoMessage>
) => {
return await ipcRenderer.invoke(
DEFINE_STRING.BOOK.UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE,
bookTaskDetailId,
videoMessage
)
}
}

View File

@ -42,6 +42,10 @@ const system = {
SelectFolderOrFile: (extensions?: string[]) =>
ipcRenderer.invoke(DEFINE_STRING.SYSTEM.SELECT_FOLDER_OR_FILE, extensions),
/** 读取文本文件内容 */
ReadTextFile: (filePath: string) =>
ipcRenderer.invoke(DEFINE_STRING.SYSTEM.READ_TEXT_FILE, filePath),
//#endregion
//#region 系统相关

View File

@ -1,4 +1,5 @@
import { GeneralResponse } from '@/define/model/generalResponse'
import { t } from '@/i18n'
/**
* codedatamessage
@ -13,7 +14,7 @@ function successMessage(
service?: string
): GeneralResponse.SuccessItem {
if (service) {
global.logger.success(service, message ? message : '成功返回数据')
global.logger.success(service, message ? message : t('成功返回数据'))
}
return {
code: 1,
@ -30,7 +31,7 @@ function successMessage(
*/
function errorMessage(message: string, service?: string): GeneralResponse.ErrorItem {
if (service) {
global.logger.error(service, message ? message : '未知报错,没有捕获的错误')
global.logger.error(service, message ? message : t('未知报错,没有捕获的错误'))
}
return {
code: 0,

View File

@ -10,7 +10,7 @@ declare module 'vue' {
export interface GlobalComponents {
AddOrModifyPreset: typeof import('./src/components/Preset/AddOrModifyPreset.vue')['default']
AIGroup: typeof import('./src/components/Original/Copywriter/AIGroup.vue')['default']
AISetting: typeof import('./src/components/Setting/AISetting.vue')['default']
AISetting: typeof import('./src/components/Setting/InferenceSetting/AISetting.vue')['default']
AllImagePreview: typeof import('./src/components/Original/BookTaskDetail/AllImagePreview.vue')['default']
APIIcon: typeof import('./src/components/common/Icon/APIIcon.vue')['default']
AppearanceSettings: typeof import('./src/components/Setting/AppearanceSettings.vue')['default']
@ -25,8 +25,7 @@ declare module 'vue' {
CopyWritingCategoryMenu: typeof import('./src/components/CopyWriting/CopyWritingCategoryMenu.vue')['default']
CopyWritingContent: typeof import('./src/components/CopyWriting/CopyWritingContent.vue')['default']
CopyWritingShowAIGenerate: typeof import('./src/components/CopyWriting/CopyWritingShowAIGenerate.vue')['default']
CopyWritingSimpleSetting: typeof import('./src/components/CopyWriting/CopyWritingSimpleSetting.vue')['default']
CustomInferencePreset: typeof import('./src/components/Setting/CustomInferencePreset.vue')['default']
CustomInferencePreset: typeof import('./src/components/Setting/InferenceSetting/CustomInferencePreset.vue')['default']
CWInputWord: typeof import('./src/components/CopyWriting/CWInputWord.vue')['default']
DataTableAction: typeof import('./src/components/Original/BookTaskDetail/DataTableAction.vue')['default']
DatatableAfterGpt: typeof import('./src/components/Original/BookTaskDetail/DatatableAfterGpt.vue')['default']
@ -53,6 +52,19 @@ declare module 'vue' {
JianyingKeyFrameSetting: typeof import('./src/components/Setting/JianyingKeyFrameSetting.vue')['default']
LoadingComponent: typeof import('./src/components/common/LoadingComponent.vue')['default']
ManageAISetting: typeof import('./src/components/CopyWriting/ManageAISetting.vue')['default']
MediaToVideoInfoBasicInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue')['default']
MediaToVideoInfoConfig: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue')['default']
MediaToVideoInfoEmptyState: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoEmptyState.vue')['default']
MediaToVideoInfoHome: typeof import('./src/components/MediaToVideo/MediaToVideoInfoHome.vue')['default']
MediaToVideoInfoMJVideoExtend: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue')['default']
MediaToVideoInfoMJVideoImageToVideo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoImageToVideo.vue')['default']
MediaToVideoInfoMJVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoInfo.vue')['default']
MediaToVideoInfoMJVideoSelectParentTask: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoSelectParentTask.vue')['default']
MediaToVideoInfoTaskDetail: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskDetail.vue')['default']
MediaToVideoInfoTaskList: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskList.vue')['default']
MediaToVideoInfoTaskOptions: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskOptions.vue')['default']
MediaToVideoInfoVideoConfig: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoConfig.vue')['default']
MediaToVideoInfoVideoListInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue')['default']
MenuOpenRound: typeof import('./src/components/common/Icon/MenuOpenRound.vue')['default']
MessageAndProgress: typeof import('./src/components/Original/BookTaskDetail/MessageAndProgress.vue')['default']
MJAccountDialog: typeof import('./src/components/Setting/MJSetting/MJAccountDialog.vue')['default']
@ -121,7 +133,6 @@ declare module 'vue' {
OriginalViewBookInfo: typeof import('./src/components/Original/MainHome/OriginalViewBookInfo.vue')['default']
OriginalViewBookTaskInfo: typeof import('./src/components/Original/MainHome/OriginalViewBookTaskInfo.vue')['default']
PresetShowCard: typeof import('./src/components/Preset/PresetShowCard.vue')['default']
ProjectItem: typeof import('./src/components/Original/MainHome/ProjectItem.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SceneAnalysis: typeof import('./src/components/Original/Analysis/SceneAnalysis.vue')['default']
@ -132,15 +143,12 @@ declare module 'vue' {
SelectStylePreset: typeof import('./src/components/Preset/SelectStylePreset.vue')['default']
StylePreset: typeof import('./src/components/Preset/StylePreset.vue')['default']
TextEllipsis: typeof import('./src/components/common/TextEllipsis.vue')['default']
ToolBoxHome: typeof import('./src/components/ToolBox/ToolBoxHome.vue')['default']
ToolGrid: typeof import('./src/components/ToolBox/ToolGrid.vue')['default']
TooltipButton: typeof import('./src/components/common/TooltipButton.vue')['default']
TooltipDropdown: typeof import('./src/components/common/TooltipDropdown.vue')['default']
TopMenuButtons: typeof import('./src/components/Original/BookTaskDetail/TopMenuButtons.vue')['default']
UploadRound: typeof import('./src/components/common/Icon/UploadRound.vue')['default']
UserAnalysis: typeof import('./src/components/Original/Analysis/UserAnalysis.vue')['default']
Versions: typeof import('./src/components/Versions.vue')['default']
ViewBookInfo: typeof import('./src/components/Original/MainHome/ViewBookInfo.vue')['default']
WechatGroup: typeof import('./src/components/SoftHome/WechatGroup.vue')['default']
WordGroup: typeof import('./src/components/Original/Copywriter/WordGroup.vue')['default']
}

View File

@ -4,10 +4,22 @@
<meta charset="UTF-8" />
<title>LaiTool PRO</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
<!-- <meta
http-equiv="Content-Security-Policy"
content="img-src 'self' data: blob: file: https: http:; default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
/>
content="
default-src 'self';
img-src 'self' data: blob: file: https: http:;
media-src 'self' data: blob: file: https: http:;
video-src 'self' data: blob: file: https: http:;
audio-src 'self' data: blob: file: https: http:;
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
font-src 'self' data:;
connect-src 'self' https: http: ws: wss:;
object-src 'none';
base-uri 'self';
"
/> -->
</head>
<body>

View File

@ -53,7 +53,6 @@ import { darkTheme } from 'naive-ui'
import LoadingScreen from '@renderer/views/LoadingScreen.vue'
import Authorization from '@renderer/views/Authorization.vue'
import { createActiveColor, createHoverColor } from '@/renderer/src/common/color'
import { define } from '@/define/define'
import { useAuthorization } from '@/renderer/src/hooks/useAuthorization'
const themeStore = useThemeStore()

View File

@ -6,6 +6,7 @@ import { errorMessage, successMessage } from '@/public/generalTools'
import { usePresetStore } from '@renderer/stores'
import { isEmpty } from 'lodash'
import { checkImageExists } from './image'
import { t } from '@/i18n'
const presetStore = usePresetStore()
/**
@ -129,7 +130,7 @@ export async function checBookTaskDetailImageExist(
)
})
if (hasAllImage != -1) {
return errorMessage('分镜的图片没有全部出完,不能继续该操作!!')
return errorMessage(t('分镜的图片没有全部出完,不能继续该操作!!'))
}
// 检查所有的图片是否存在
let imageCheck = false
@ -137,7 +138,7 @@ export async function checBookTaskDetailImageExist(
for (let i = 0; i < bookTaskDetails.length; i++) {
const element = bookTaskDetails[i]
if (element.outImagePath == undefined || element.outImagePath == null) {
return errorMessage('分镜的图片没有全部出完,不能继续该操作!!')
return errorMessage(t('分镜的图片没有全部出完,不能继续该操作!!'))
}
let c = await checkImageExists(element.outImagePath.split('?t')[0])
if (!c) {
@ -147,8 +148,11 @@ export async function checBookTaskDetailImageExist(
}
if (imageCheck) {
return errorMessage(
`分镜 ${notFoundImage.join('')} 图片在本地未找到,不能继续该操作,请检查对应分镜的图片路径是否正确`
t("分镜 {name} 图片在本地未找到,不能继续该操作,请检查对应分镜的图片路径是否正确", {
name: notFoundImage.join('')
})
)
}
return successMessage(null, '分镜的图片全部存在,可以进行高清处理')
return successMessage(null, t("分镜的图片全部存在,可以进行高清处理"))
}

View File

@ -1,10 +1,12 @@
import { getAPIOptions } from '@/define/data/apiData'
import { ImageCategory, ImageToVideoCategory } from '@/define/data/imageData'
import { ImageCategory } from '@/define/data/imageData'
import { getMJSpeedOptions, ImageGenerateMode, MJRobotType } from '@/define/data/mjData'
import { JianyingKeyFrameEnum } from '@/define/enum/jianyingEnum'
import { OptionKeyName, OptionType } from '@/define/enum/option'
import { ImageToVideoModels } from '@/define/enum/video'
import { SettingModal } from '@/define/model/setting'
import { ValidateJson, ValidateJsonAndParse } from '@/define/Tools/validate'
import { t } from '@/i18n'
import { optionSerialization } from '@/main/service/option/optionSerialization'
import { isEmpty } from 'lodash'
@ -16,8 +18,8 @@ export const defaultGeneralSetting: SettingModal.GeneralSettings = {
concurrency: 1, // 系统并发数 (1-16)
defaultImgGenMethod: ImageCategory.Midjourney, // 默认生图方式
hdScale: 2, // 高清倍数 (1-4)
defaultImg2Video: ImageToVideoCategory.RUNWAY, // 默认图转视频方式
language: 'zh-CN' // 系统语言
defaultImg2Video: ImageToVideoModels.RUNWAY, // 默认图转视频方式
language: 'zh-cn' // 系统语言
}
/**
@ -67,7 +69,9 @@ export async function InitGeneralSetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化通用设置失败')
throw new Error(t("初始化通用设置失败,{error}", {
error: res.message
}))
}
} catch (error) {
throw error
@ -152,7 +156,9 @@ export async function InitMJSetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化MJ通用设置失败')
throw new Error(t("初始化MJ通用设置失败{error}", {
error: res.message
}))
}
// 初始化API设置
@ -182,7 +188,9 @@ export async function InitMJSetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化MJ API设置失败')
throw new Error(t("初始化MJ API设置失败{error}", {
error: res.message
}))
}
// 初始化生图包设置
@ -213,7 +221,9 @@ export async function InitMJSetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化MJ生图包设置失败')
throw new Error(t("初始化MJ生图包设置失败{error}", {
error: res.message
}))
}
// 初始化 代理模式设置
@ -242,7 +252,9 @@ export async function InitMJSetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化MJ代理模式设置失败')
throw new Error(t("初始化MJ代理模式设置失败{error}", {
error: res.message
}))
}
// 初始化 本地代理模式设置
@ -271,7 +283,9 @@ export async function InitMJSetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化MJ本地代理模式设置失败')
throw new Error(t("初始化MJ本地代理模式设置失败{error}", {
error: res.message
}))
}
} catch (error) {
throw error
@ -325,7 +339,9 @@ export async function InitInferenceAISetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化推理设置失败')
throw new Error(t("初始化推理设置失败,{error}", {
error: res.message
}))
}
} catch (error) {
throw error
@ -397,7 +413,9 @@ export async function InitSDSettingAndADetailerSetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化SD设置失败')
throw new Error(t("初始化SD设置失败{error}", {
error: res.message
}))
}
// 修手/修脸模型设置
@ -416,7 +434,9 @@ export async function InitSDSettingAndADetailerSetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化修手/修脸模型设置失败')
throw new Error(t("初始化修手/修脸模型设置失败,{error}", {
error: res.message
}))
}
}
} catch (error) {
@ -486,7 +506,9 @@ export async function InitJianyingKeyFrameSetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化剪映关键帧设置失败')
throw new Error(t("初始化剪映关键帧设置失败,{error}", {
error: res.message
}))
}
} catch (error) {
throw error
@ -536,7 +558,9 @@ export async function InitComfyUISetting() {
OptionType.JSON
)
if (res.code != 1) {
throw new Error('初始化Comfy UI设置失败')
throw new Error(t("初始化ComfyUI设置失败{error}", {
error: res.message
}))
}
let comfyuiWorkFlowSetting = await window.option.GetOptionByKey(
@ -556,7 +580,9 @@ export async function InitComfyUISetting() {
)
}
if (res.code != 1) {
throw new Error('初始化Comfy UI设置失败')
throw new Error(t("初始化ComfyUI工作流设置失败{error}", {
error: res.message
}))
}
} catch (error) {
throw error
@ -584,10 +610,12 @@ export async function InitSpecialCharacters() {
OptionType.STRING
)
if (saveRes.code != 1) {
throw new Error('初始化特殊符号字符串失败: ' + saveRes.message)
throw new Error(t("初始化特殊符号字符串失败,{error}", {
error: saveRes.message
}))
}
} catch (error) {
throw new Error('初始化特殊符号字符串失败: ' + error)
throw error
}
}

View File

@ -3,10 +3,77 @@ import { BookBackTaskStatus, BookBackTaskType, TaskExecuteType } from '@/define/
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { Book } from '@/define/model/book/book'
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
import { TaskModal } from '@/define/model/task'
import { t } from '@/i18n/main'
import { errorMessage, successMessage } from '@/public/generalTools'
import { useBookStore } from '@renderer/stores'
const bookStore = useBookStore()
//#region AddOneTask
/**
*
*
*
*
*
*
* @param {TaskModal.Task} task -
* - bookId: 关联的书籍ID
* - type:
* - executeType: 执行模式
* - bookTaskId: 关联的书籍批次任务ID
* - bookTaskDetailId: 关联的具体分镜ID
* - messageName: 任务完成后的消息通道名称
* -
*
* @returns {Promise<SuccessItem | ErrorItem>}
* - SuccessItem对象
* - ErrorItem对象
*
* @example
* // 添加Midjourney图像生成任务
* const mjImageTask = {
* bookId: bookStore.selectBook.id,
* type: BookBackTaskType.MJ_IMAGE,
* executeType: TaskExecuteType.AUTO,
* bookTaskId: bookStore.selectBookTask.id,
* bookTaskDetailId: frameId,
* messageName: DEFINE_STRING.BOOK.MJ_IMAGE_GENERATE_RETURN
* };
* const result = await AddOneTask(mjImageTask);
*
* // 添加视频生成任务
* const videoTask = {
* bookId: bookStore.selectBook.id,
* type: BookBackTaskType.MJ_VIDEO,
* executeType: TaskExecuteType.AUTO,
* bookTaskId: bookStore.selectBookTask.id,
* bookTaskDetailId: frameId,
* messageName: DEFINE_STRING.BOOK.MJ_VIDEO_GENERATE_RETURN
* };
* const result = await AddOneTask(videoTask);
*/
export async function AddOneTask(task: TaskModal.Task): Promise<SuccessItem | ErrorItem> {
try {
const result = await window.task.AddOneTask(task)
if (result.code === 1) {
return successMessage(
result.data,
t("任务添加成功,任务名称:{taskName}", { taskName: result.data?.name })
)
} else {
return errorMessage(t("任务添加失败,失败信息如下:{error}", { error: result.message }))
}
} catch (error: any) {
return errorMessage(t("任务添加失败,失败信息如下:{error}", { error: error.message }))
}
}
//#endregion
//#region 添加图片生成任务
/**
*
*
@ -41,7 +108,7 @@ export async function AddMultiImageTask(
): Promise<SuccessItem | ErrorItem> {
let res: SuccessItem | ErrorItem
if (bookTaskDetails.length <= 0) {
return errorMessage('添加出图任务失败,未找到可生成图片的分镜,或者分镜都被锁定')
return errorMessage(t("添加出图任务失败,未找到可生成图片的分镜,或者分镜都被锁定"))
}
if (imageCategory == ImageCategory.Midjourney) {
@ -133,11 +200,14 @@ export async function AddMultiImageTask(
}
res = await window.task.AddMultiTask(tasksList)
} else {
return errorMessage('添加出图任务失败,不支持的出图类型')
return errorMessage(t("添加出图任务失败,不支持的出图类型"))
}
return res
}
//#endregion
//#region 停止指定类型的任务
/**
*
*
@ -162,7 +232,7 @@ export async function AddMultiImageTask(
*/
export async function StopImageTask(type: string): Promise<SuccessItem | ErrorItem> {
if (type != 'all' && type != 'this') {
return errorMessage('停止出图任务失败,参数错误')
return errorMessage(t('停止出图任务失败,参数错误'))
}
let taskTypes = [
BookBackTaskType.MJ_IMAGE,
@ -200,7 +270,7 @@ export async function StopImageTask(type: string): Promise<SuccessItem | ErrorIt
}
if (stopTasks.length <= 0) {
return errorMessage('停止出图任务失败,没有需要停止的任务')
return errorMessage(t('停止出图任务失败,没有需要停止的任务'))
}
// 开始执行停止的任务
@ -209,15 +279,17 @@ export async function StopImageTask(type: string): Promise<SuccessItem | ErrorIt
let res = await window.task.UpdateTaskStatus({
id: element.id,
status: BookBackTaskStatus.FAIL,
errorMessage: '用户手动取消了任务'
errorMessage: t('用户手动取消了任务')
})
if (res.code != 1) {
return res
}
}
if (type == 'all') {
return successMessage(null, '停止所有批次的出图任务成功', 'StopImageTask')
return successMessage(null, t('停止所有批次的出图任务成功'), 'StopImageTask')
} else {
return successMessage(null, '停止当前批次的出图任务成功', 'StopImageTask')
return successMessage(null, t('停止当前批次的出图任务成功'), 'StopImageTask')
}
}
//#endregion

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