diff --git a/.gitignore b/.gitignore index 47af336..feb5082 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json index 84a0e26..7853f0b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" diff --git a/package.json b/package.json index 4a480c8..80d2075 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/define/Tools/common.ts b/src/define/Tools/common.ts index 71abc7b..c40385e 100644 --- a/src/define/Tools/common.ts +++ b/src/define/Tools/common.ts @@ -1,3 +1,4 @@ +import { t } from '@/i18n' import { escapeRegExp, isEmpty } from 'lodash' //#region 检查字符串中是不是包含中文或者标点符号 /** @@ -35,15 +36,21 @@ export async function RetryWithBackoff( // 这边记下日志吧 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 { 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 + })) } } diff --git a/src/define/Tools/image.ts b/src/define/Tools/image.ts index a8e73b1..660b37c 100644 --- a/src/define/Tools/image.ts +++ b/src/define/Tools/image.ts @@ -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 { 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确保不超出边界 diff --git a/src/define/Tools/logger.ts b/src/define/Tools/logger.ts index 8c8017d..3ca7ae8 100644 --- a/src/define/Tools/logger.ts +++ b/src/define/Tools/logger.ts @@ -29,7 +29,8 @@ export class Logger { datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '10m', - maxFiles: '14d' + maxFiles: '14d', + options: { flags: 'a', encoding: 'utf8' } }) ] }) diff --git a/src/define/Tools/validate.ts b/src/define/Tools/validate.ts index 5d0b84b..fd2d4f1 100644 --- a/src/define/Tools/validate.ts +++ b/src/define/Tools/validate.ts @@ -1,3 +1,5 @@ +import { t } from "@/i18n" + /** * 校验是不是可以进行JSON解析 * @param str 要解析的字符串 @@ -21,12 +23,12 @@ export function ValidateJson(str: string): boolean { export function ValidateJsonAndParse(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 } diff --git a/src/define/Tools/write.ts b/src/define/Tools/write.ts index 35a1c78..6cd6894 100644 --- a/src/define/Tools/write.ts +++ b/src/define/Tools/write.ts @@ -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 + })) } } diff --git a/src/define/data/aiData/aiData.ts b/src/define/data/aiData/aiData.ts index b57cc6a..1c887fb 100644 --- a/src/define/data/aiData/aiData.ts +++ b/src/define/data/aiData/aiData.ts @@ -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] } diff --git a/src/define/data/apiData.ts b/src/define/data/apiData.ts index 467ff3e..88f10b5 100644 --- a/src/define/data/apiData.ts +++ b/src/define/data/apiData.ts @@ -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] } diff --git a/src/define/data/imageData.ts b/src/define/data/imageData.ts index 34e884e..2c92a99 100644 --- a/src/define/data/imageData.ts +++ b/src/define/data/imageData.ts @@ -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 diff --git a/src/define/data/mjData.ts b/src/define/data/mjData.ts index f75242b..71e65c9 100644 --- a/src/define/data/mjData.ts +++ b/src/define/data/mjData.ts @@ -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 } ] diff --git a/src/define/data/presetData.ts b/src/define/data/presetData.ts index afe227d..48c73c1 100644 --- a/src/define/data/presetData.ts +++ b/src/define/data/presetData.ts @@ -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('未知类型') } } } diff --git a/src/define/data/softwareData.ts b/src/define/data/softwareData.ts index b8cd409..1687be3 100644 --- a/src/define/data/softwareData.ts +++ b/src/define/data/softwareData.ts @@ -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', diff --git a/src/define/db/model/bookTaskDetail.ts b/src/define/db/model/bookTaskDetail.ts index 6143b53..ea7dfae 100644 --- a/src/define/db/model/bookTaskDetail.ts +++ b/src/define/db/model/bookTaskDetail.ts @@ -63,6 +63,7 @@ export class VideoMessage extends Realm.Object { 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 { runwayOptions: 'string?', lumaOptions: 'string?', klingOptions: 'string?', + mjVideoOptions: 'string?', messageData: 'string?' }, primaryKey: 'id' @@ -166,6 +168,7 @@ export class BookTaskDetailModel extends Realm.Object { 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 { bookTaskId: { type: 'string', indexed: true }, videoPath: 'string?', generateVideoPath: 'string?', // 生成视频地址 + subVideoPath: 'string[]', // 生成的批次视频的地址 audioPath: 'string?', word: 'string?', oldImage: 'string?', diff --git a/src/define/db/model/taskList.ts b/src/define/db/model/taskList.ts index 2a9888b..dc5fff8 100644 --- a/src/define/db/model/taskList.ts +++ b/src/define/db/model/taskList.ts @@ -16,6 +16,8 @@ export class TaskListModel extends Realm.Object { 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 { updateTime: 'date', startTime: 'int', endTime: 'int', - messageName: 'string?' + messageName: 'string?', + taskId: 'string?', // 任务ID,可能是视频生成任务的ID + taskMessage: 'string?' // 任务消息,可能是视频生成任务的消息 }, primaryKey: 'id' } diff --git a/src/define/db/service/base/realmBase.ts b/src/define/db/service/base/realmBase.ts index d76c5c7..1628fd9 100644 --- a/src/define/db/service/base/realmBase.ts +++ b/src/define/db/service/base/realmBase.ts @@ -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) diff --git a/src/define/db/service/book/bookService.ts b/src/define/db/service/book/bookService.ts index a22f27a..c970a45 100644 --- a/src/define/db/service/book/bookService.ts +++ b/src/define/db/service/book/bookService.ts @@ -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 { + async GetBookDataById(bookId: string, notEmpty: true): Promise + async GetBookDataById(bookId: string, notEmpty?: false): Promise + async GetBookDataById( + bookId: string, + notEmpty: boolean = false + ): Promise { 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('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 { 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 { 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) diff --git a/src/define/db/service/book/bookTaskDetailService.ts b/src/define/db/service/book/bookTaskDetailService.ts index 939c0e7..068923d 100644 --- a/src/define/db/service/book/bookTaskDetailService.ts +++ b/src/define/db/service/book/bookTaskDetailService.ts @@ -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 { try { if (condition == null) { - throw new Error('查询小说分镜信息,查询条件不能为空') + throw new Error(t("查询小说分镜信息,查询条件不能为空!")) } let tasksToDelete = this.realm.objects('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 + public async GetBookTaskDetailDataById( + bookTaskDetailId: string, + notEmpty?: false + ): Promise + public async GetBookTaskDetailDataById( + bookTaskDetailId: string, + notEmpty: boolean = false + ): Promise { + 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 { - 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 { 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,35 +300,39 @@ export class BookTaskDetailService extends RealmBaseService { */ async UpdateBookTaskDetailVideoMessage( bookTaskDetailId: string, - videoMessage: BookTaskDetail.VideoMessage + videoMessage: Partial ): Promise { 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 { - for (const key in videoMessage) { - if (key == 'id') { - continue + // 更新现有视频消息 + 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') { + videoMessageRes[key] = videoMessage[key] + } } - if ( - key == 'imageUrl' && - videoMessage[key] != null && - !videoMessage[key].startsWith('http') - ) { - videoMessage[key] = path.relative(projectPath, videoMessage[key]) - } - 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('缺少必要的条件,必须传入id,bookId或者bookTaskId其中一个')) } let tasksToDelete = this.realm.objects('BookTaskDetail') if (condition.id) { @@ -363,7 +415,7 @@ export class BookTaskDetailService extends RealmBaseService { .objects('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) diff --git a/src/define/db/service/book/bookTaskService.ts b/src/define/db/service/book/bookTaskService.ts index 6fe08cd..b309307 100644 --- a/src/define/db/service/book/bookTaskService.ts +++ b/src/define/db/service/book/bookTaskService.ts @@ -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 { + async GetBookTaskDataById(bookTaskId: string, notEmpty: true): Promise + async GetBookTaskDataById( + bookTaskId: string, + notEmpty?: false + ): Promise + + async GetBookTaskDataById( + bookTaskId: string, + notEmpty: boolean = false + ): Promise { 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) { @@ -462,7 +480,7 @@ export class BookTaskService extends RealmBaseService { } if (bookTaskDetail.reversePrompt) { - ;(bookTaskDetail.reversePrompt as any[]).forEach((item) => { + ; (bookTaskDetail.reversePrompt as any[]).forEach((item) => { this.realm.delete(item) }) } diff --git a/src/define/db/service/book/taskListService.ts b/src/define/db/service/book/taskListService.ts index 7b1b4e5..9f9732e 100644 --- a/src/define/db/service/book/taskListService.ts +++ b/src/define/db/service/book/taskListService.ts @@ -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,15 +240,16 @@ 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' - }-${taskType}` + let name = `${book.name}-${bookTask ? bookTask.name : 'default'}-${bookTaskDetail ? bookTaskDetail.name : 'default' + }-${taskType}` let bookBackTask = { id: crypto.randomUUID(), @@ -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): 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] + } + }) + } + /** * 删除满足条件的数据,包含 id、bookId、bookTaskId * 上面的条件,至少要有一个 @@ -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('任务被丢弃') } }) } diff --git a/src/define/db/service/optionService.ts b/src/define/db/service/optionService.ts index db822cf..df2efd5 100644 --- a/src/define/db/service/optionService.ts +++ b/src/define/db/service/optionService.ts @@ -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,16 +25,17 @@ export class OptionRealmService extends RealmBaseService { return OptionRealmService.instance } + //#region GetOptionByKey /** * 获取指定的Option,通过key,不存在返回null - * @param key - * @returns + * @param key + * @returns */ public GetOptionByKey(key: string): OptionModel.OptionModel | null { 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,24 +46,27 @@ export class OptionRealmService extends RealmBaseService { }) return resData[0] as OptionModel.OptionModel } else { - return null; + return null } } + //#endregion + + //#region ModifyOptionByKey /** * 修改指定的Option,通过key,不存在则创建 - * @param key - * @param value + * @param key + * @param value */ public ModifyOptionByKey(key: string, value: string, type: OptionType = OptionType.STRING) { if (isEmpty(key)) { return false } - let option = this.realm.objectForPrimaryKey('Option', key); + 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(optionKey: string, description: string, defaultValue?: T): T { + try { + // 获取设置数据 + let res = this.GetOptionByKey(optionKey) + + // 开始解析 + let optionData = optionSerialization(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) + } + } } - - diff --git a/src/define/db/service/presetService.ts b/src/define/db/service/presetService.ts index 3f88d3e..4dce5cd 100644 --- a/src/define/db/service/presetService.ts +++ b/src/define/db/service/presetService.ts @@ -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 { 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('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) diff --git a/src/define/enum/bookEnum.ts b/src/define/enum/bookEnum.ts index d764ab6..aecb6d0 100644 --- a/src/define/enum/bookEnum.ts +++ b/src/define/enum/bookEnum.ts @@ -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' } } diff --git a/src/define/enum/jianyingEnum.ts b/src/define/enum/jianyingEnum.ts index ca96249..3c4a1fd 100644 --- a/src/define/enum/jianyingEnum.ts +++ b/src/define/enum/jianyingEnum.ts @@ -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 } ] diff --git a/src/define/enum/option.ts b/src/define/enum/option.ts index eb88908..8ba8aae 100644 --- a/src/define/enum/option.ts +++ b/src/define/enum/option.ts @@ -103,5 +103,12 @@ export const OptionKeyName = { /** Comfy UI 基础设置 */ ComfyUISimpleSetting: 'SD_ComfyUISimpleSetting' + }, + Video: { + /** 是否开启右侧控制面板 */ + ShowRightPanel: 'Video_ShowRightPanel', + + /** 是否开启分页 */ + ShowPagination: 'Video_ShowPagination' } } diff --git a/src/define/enum/softwareEnum.ts b/src/define/enum/softwareEnum.ts index e8497d5..cb50edd 100644 --- a/src/define/enum/softwareEnum.ts +++ b/src/define/enum/softwareEnum.ts @@ -53,30 +53,31 @@ export enum SoftColor { // 错误红色 ERROR_RED = '#c8161d' - } export enum ResponseMessageType { GET_TEXT = 'getText', // 获取文案 - REMOVE_WATERMARK = "REMOVE_WATERMARK",// 删除水印 - MJ_REVERSE = 'MJ_REVERSE',// MJ反推,返回反推结果 - SD_REVERSE = 'SD_REVERSE',// MJ反推,返回反推结果 - REVERSE_PROMPT_TRANSLATE = 'REVERSE_PROMPT_TRANSLATE',// 反推提示词翻译 + REMOVE_WATERMARK = 'REMOVE_WATERMARK', // 删除水印 + MJ_REVERSE = 'MJ_REVERSE', // MJ反推,返回反推结果 + SD_REVERSE = 'SD_REVERSE', // MJ反推,返回反推结果 + REVERSE_PROMPT_TRANSLATE = 'REVERSE_PROMPT_TRANSLATE', // 反推提示词翻译 GPT_PROMPT_TRANSLATE = 'GPT_PROMPT_TRANSLATE', // GPT提示词翻译 - MJ_IMAGE = 'MJ_IMAGE',// MJ 生成图片 - ComfyUI_IMAGE = 'ComfyUI_IMAGE',// ComfyUI 生成图片 - HD_IMAGE = 'HD_IMAGE',// HD 生成图片 - RUNWAY_VIDEO = "RUNWAY_VIDEO",// Runway生成视频 - LUMA_VIDEO = "LUMA_VIDEO",// Luma生成视频 - KLING_VIDEO = "KLING_VIDEO",// Kling生成视频 - VIDEO_SUCESS = "VIDEO_SUCESS" //视频生成成功 + 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生成视频 + 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', - -} \ No newline at end of file + SUBTITLE_SETTING = 'subtitle_setting' +} diff --git a/src/define/enum/video.ts b/src/define/enum/video.ts index fb34ad0..8a655e6 100644 --- a/src/define/enum/video.ts +++ b/src/define/enum/video.ts @@ -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,80 +44,88 @@ 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 '未知' } } /** * 获取图像转视频模型选项的函数 - * + * * 该函数返回一个包含所有可用图像转视频模型的选项数组。 * 每个选项包含一个标签(label)和一个值(value)。 * 标签通过调用 GetImageToVideoModelsLabel 函数获得,而值则直接使用 ImageToVideoModels 枚举值。 - * + * * @returns 图像转视频模型选项数组,每个选项包含 label 和 value 属性 */ 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,52 +163,151 @@ export enum KlingMode { * 对视频任务进行操作。不为空时,index、taskId必填 */ 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视频动作幅度的标签 - * + * * @param model MJ视频动作幅度枚举值或字符串 * @returns 返回对应的中英文标签 */ 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('未知') } } /** * 获取MJ视频动作幅度的选项列表 - * + * * @returns 返回包含标签和值的选项数组,用于下拉选择框等UI组件 */ 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 } ] } diff --git a/src/define/ipc/subIpc/bookIPC/bookVideoIpc.ts b/src/define/ipc/subIpc/bookIPC/bookVideoIpc.ts new file mode 100644 index 0000000..07fe1f7 --- /dev/null +++ b/src/define/ipc/subIpc/bookIPC/bookVideoIpc.ts @@ -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) => + await bookHandle.UpdateBookTaskDetailVideoMessage(bookTaskDetailId, videoMessage) + ) +} diff --git a/src/define/ipc/subIpc/bookIpc.ts b/src/define/ipc/subIpc/bookIpc.ts index 7d1be31..107408c 100644 --- a/src/define/ipc/subIpc/bookIpc.ts +++ b/src/define/ipc/subIpc/bookIpc.ts @@ -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 diff --git a/src/define/ipc/subIpc/systemIpc.ts b/src/define/ipc/subIpc/systemIpc.ts index a82c586..45afe0a 100644 --- a/src/define/ipc/subIpc/systemIpc.ts +++ b/src/define/ipc/subIpc/systemIpc.ts @@ -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 } diff --git a/src/define/ipc/subIpc/taskIpc.ts b/src/define/ipc/subIpc/taskIpc.ts index 22b1b23..122c21b 100644 --- a/src/define/ipc/subIpc/taskIpc.ts +++ b/src/define/ipc/subIpc/taskIpc.ts @@ -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) ) /** 添加多个任务 */ diff --git a/src/define/ipcDefineString/subDefineString/bookDefineString.ts b/src/define/ipcDefineString/subDefineString/bookDefineString.ts index 5e66256..f0be7ec 100644 --- a/src/define/ipcDefineString/subDefineString/bookDefineString.ts +++ b/src/define/ipcDefineString/subDefineString/bookDefineString.ts @@ -138,10 +138,27 @@ 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 } diff --git a/src/define/ipcDefineString/subDefineString/systemDefineString.ts b/src/define/ipcDefineString/subDefineString/systemDefineString.ts index 7838c98..5f738d5 100644 --- a/src/define/ipcDefineString/subDefineString/systemDefineString.ts +++ b/src/define/ipcDefineString/subDefineString/systemDefineString.ts @@ -48,6 +48,9 @@ const SYSTEM = { /** 选择文件夹或指定后缀的文件 */ SELECT_FOLDER_OR_FILE: 'SELECT_FOLDER_OR_FILE', + /** 读取文本文件内容 */ + READ_TEXT_FILE: 'READ_TEXT_FILE', + //#endregion /** 打开指定的url */ diff --git a/src/define/model/book/book.d.ts b/src/define/model/book/book.d.ts index 1b4a65f..f168d3a 100644 --- a/src/define/model/book/book.d.ts +++ b/src/define/model/book/book.d.ts @@ -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 // 文案 diff --git a/src/define/model/book/bookTaskDetail.d.ts b/src/define/model/book/bookTaskDetail.d.ts index 0e572c3..f85fbb4 100644 --- a/src/define/model/book/bookTaskDetail.d.ts +++ b/src/define/model/book/bookTaskDetail.d.ts @@ -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 { + /** + * 对视频任务进行操作。不为空时,index、taskId必填 + */ + 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 小说文案相关 diff --git a/src/define/model/book/bookVideo.d.ts b/src/define/model/book/bookVideo.d.ts new file mode 100644 index 0000000..848a322 --- /dev/null +++ b/src/define/model/book/bookVideo.d.ts @@ -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 + } +} diff --git a/src/define/model/setting.d.ts b/src/define/model/setting.d.ts index 704373e..7606818 100644 --- a/src/define/model/setting.d.ts +++ b/src/define/model/setting.d.ts @@ -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 @@ -155,7 +157,7 @@ declare namespace SettingModal { interface MJLocalSetting { /** 服务地址 */ requestUrl: string - + /** 访问令牌 */ token: string diff --git a/src/define/model/task.d.ts b/src/define/model/task.d.ts index a06c171..0ebf238 100644 --- a/src/define/model/task.d.ts +++ b/src/define/model/task.d.ts @@ -21,6 +21,8 @@ declare namespace TaskModal { startTime?: number endTime?: number messageName?: string + taskId?: string // 任务ID,可能是第三方服务的任务ID + taskMessage?: string // 任务消息,可能是第三方服务的任务消息 } interface TaskCondition { diff --git a/src/define/quene.ts b/src/define/quene.ts index b9f8f64..d96c051 100644 --- a/src/define/quene.ts +++ b/src/define/quene.ts @@ -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) { diff --git a/src/i18n/README.md b/src/i18n/README.md new file mode 100644 index 0000000..7fe0876 --- /dev/null +++ b/src/i18n/README.md @@ -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 + + + +``` + +## 📚 可用的键对象 + +导入键对象后,您可以享受完整的代码提示: + +```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 + +

{{ t('menu.home') }}

+ + +

{{ t(menu.home) }}

+ ``` + +## 🔧 开发指南 + +### 添加新的翻译键 + +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 + + + +``` + +这就是您的多语言系统的完整使用指南!🎉 + +## 🔀 翻译文本合并功能 + +新增了翻译文本合并功能,可以根据不同语言使用相应的分隔符来合并多个翻译文本。 + +### 主要功能 + +#### 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 + + + +``` + +这个完整的多语言系统现在包含了基础翻译功能和高级的文本合并功能!🚀 diff --git a/src/i18n/constants.ts b/src/i18n/constants.ts new file mode 100644 index 0000000..52321bb --- /dev/null +++ b/src/i18n/constants.ts @@ -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 = { + '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 } + } +} + diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..e3ff03a --- /dev/null +++ b/src/i18n/index.ts @@ -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 = { + '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 = {} 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 { + 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 { + 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 { + 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' diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts new file mode 100644 index 0000000..800c889 --- /dev/null +++ b/src/i18n/locales/en.ts @@ -0,0 +1,1846 @@ +export default { + //#region 通用 + "是": "Yes", + "否": 'No', + "操作": 'Action', + "清空": 'Clear', + "生成": 'Generate', + "确认": 'Confirm', + "继续": 'Continue', + "取消": 'Cancel', + "提示": 'Tip', + "导入": 'Import', + "复制": 'Copy', + "保存": 'Save', + "分页": 'Pagination', + "图片": 'Image', + "查看": 'View', + "状态": 'Status', + "名称": 'Name', + "编辑": 'Edit', + "删除": 'Delete', + "成功": 'Success', + "失败": 'Failed', + "关闭": 'Close', + "信息": 'Information', + "添加": 'Add', + "返回": 'Back', + "编号": 'Number', + "打开": 'Open', + "搜索": 'Search', + "重置": 'Reset', + "购买": 'Purchase', + "隐藏": "Hide", + "预览": "Preview", + "禁用": "Disable", + "启用": "Enable", + "备注": 'Remark', + "查询": "Query", + "更新": "Update", + "全部": "All", + "确定": "OK", + "警告": "Warning", + "序号": "Serial Number", + "重新加载": 'Reload', + "图片链接": 'Image Link', + "文件大小": "File Size", + "右侧面板": 'Right Panel', + "一键格式化": 'One-Click Format', + "请输入内容": 'Please enter content', + "操作提示": 'Operation Tips', + "重要提示": 'Important Notice', + "更多操作": 'More Actions', + "操作确认": 'Confirm Operation', + "取消操作": 'Cancel Operation', + "保存成功": 'Save Successful', + "保存失败": 'Save Failed', + "未知错误": 'Unknown Error', + "未知类型": 'Unknown Type', + "未知操作": 'Unknown Operation', + "下载成功": 'Download Successful', + "下载失败": 'Download Failed', + "页面不存在": "Page Not Found", + "复制的内容为空": 'Copied content is empty', + "图片未找到": 'Image not found', + "下载失败,{error}": "Download failed, {error}", + '操作失败:{error}': 'Operation failed: {error}', + '保存失败:{error}': 'Save failed: {error}', + '请输入 {data}': 'Please enter {data}', + '请选择 {data}': 'Please select {data}', + '请输入图片链接...': 'Please enter image link...', + "请输入图片地址": 'Please enter image URL', + '组件初始化失败: {error}': 'Component initialization failed: {error}', + "跳转失败,{error}": "Navigation failed, {error}", + "{data} 不能为空": "{data} cannot be empty", + "不是一个合法的url地址": "Not a valid URL address", + "浏览器不支持流式下载": "Browser does not support streaming download", + "目的文件/文件夹不存在,{data}": "Destination file/folder does not exist, {data}", + "源文件或文件夹不存在,{data}": "Source file or folder does not exist, {data}", + "目的文件或文件夹的父文件夹不存在,{data}": "Parent folder of destination file/folder does not exist, {data}", + "输入的不是有效的文件夹地址": "Input is not a valid folder path", + "下载的文件为空": "Downloaded file is empty", + "下载图片超时 ({timeout}秒),已重试{maxRetries}次,失败信息:{errorMessage}": "Image download timeout ({timeout} seconds), retried {maxRetries} times, error: {errorMessage}", + "网络连接失败,无法访问图片地址,已重试 {maxRetries}次,失败信息:{errorMessage}": "Network connection failed, unable to access image URL, retried {maxRetries} times, error: {errorMessage}", + "连接超时,服务器响应缓慢,已重试{maxRetries}次,失败信息:{errorMessage}": "Connection timeout, server response slow, retried {maxRetries} times, error: {errorMessage}", + "下载图片失败,已重试{maxRetries}次,失败信息: ${errorMessage}": "Image download failed, retried {maxRetries} times, error: ${errorMessage}", + "输入的文件地址不是图片文件地址,支持jpg、jpeg、png": "Input file path is not an image file, supports jpg, jpeg, png", + "文件不存在": "File does not exist", + "不支持的文件格式: {ext}。支持的格式: {supportedExt}": "Unsupported file format: {ext}. Supported formats: {supportedExt}", + "获取图片的宽高失败": "Failed to get image dimensions", + "将base64转换为文件失败,{error}": "Failed to convert base64 to file, {error}", + "数据不能为空": "Data cannot be empty", + "数据解析失败,请检查数据格式": "Data parsing failed, please check data format", + "验证错误": "Validation Error", + "请修正以下错误,{error}": "Please correct the following errors, {error}", + "格式化文本失败,{error}": "Text formatting failed, {error}", + //#endregion + + + //#region 小说 + "分镜计算": 'Storyboard Calculation', + "分割视频": "Video Segmentation", + "提取音频": "Audio Extraction", + "识别字幕": "Subtitle Recognition", + "抽帧": "Frame Extraction", + "MJ生成图片": "MJ Image Generation", + "SD生成图片": "SD Image Generation", + "flux forge生成图片": "Flux Forge Image Generation", + 'flux api生成图片': 'Flux API Image Generation', + "D3生成图片": "DALL-E 3 Image Generation", + "推理": "Inference", + "翻译": "Translation", + "runway生成视频": "Runway Video Generation", + "luma生成视频": "Luma Video Generation", + "kling生成视频": "Kling Video Generation", + "等待": "Waiting", + "分镜计算中": 'Calculating Storyboard', + "分镜计算失败": "Storyboard Calculation Failed", + "分镜计算完成": 'Storyboard Calculation Completed', + "分割视频中": "Segmenting Video", + "分割视频失败": "Video Segmentation Failed", + "分割视频完成": "Video Segmentation Completed", + "提取音频中": "Extracting Audio", + "提取音频失败": "Audio Extraction Failed", + "提取音频完成": "Audio Extraction Completed", + "识别字幕中": "Recognizing Subtitles", + "识别字幕失败": "Subtitle Recognition Failed", + "识别字幕完成": "Subtitle Recognition Completed", + "抽帧中": "Extracting Frames", + "抽帧失败": "Frame Extraction Failed", + "抽帧完成": "Frame Extraction Completed", + "反推中": "Reverse Engineering", + "反推失败": "Reverse Engineering Failed", + "反推完成": "Reverse Engineering Completed", + "生成图片中": "Generating Images", + "生成图片失败": "Image Generation Failed", + "生成图片完成": "Image Generation Completed", + "高清中": "Upscaling", + "高清失败": "Upscaling Failed", + "高清完成": "Upscaling Completed", + "合成视频中": "Compositing Video", + "合成视频失败": "Video Composition Failed", + "合成视频完成": "Video Composition Completed", + "添加草稿完成": "Draft Added Successfully", + "添加草稿失败": "Failed to Add Draft", + "图转视频失败": "Image-to-Video Failed", + "图转视频成功": "Image-to-Video Successful", + "未知": "Unknown", + "运行中": "Running", + "暂停": "Paused", + "完成": 'Completed', + "重连": "Reconnect", + "可灵": "Kling", + "MJ视频": "MJ Video", + "处理中": "Processing", + "高 (High)": "High", + '低 (Low)': 'Low', + '标清 (SD 480p)': "Standard Definition (SD 480p)", + '高清 (HD 720p)': 'High Definition (HD 720p)', + "1个视频": "1 Video", + "2个视频": "2 Videos", + "4个视频": '4 Videos', + "小说ID": "Novel ID", + "小说数据为空,无法修改": "Novel data is empty, cannot modify", + "反推必须传入视频": "Reverse engineering requires video input", + "小说名字 {bookName} 已经存在,请更换小说名字": "Novel name {bookName} already exists, please change the novel name", + "未知的小说类型": "Unknown novel type", + "修改小说数据失败,缺少小说ID": "Failed to modify novel data, missing novel ID", + "修改小说数据失败,缺少小说数据": "Failed to modify novel data, missing novel data", + "修改小说数据失败,小说ID对应的数据不存在": "Failed to modify novel data, data corresponding to novel ID does not exist", + "获取修改后的小说数据失败,小说ID对应的数据不存在": "Failed to get modified novel data, data corresponding to novel ID does not exist", + "未找到指定ID的小说数据": "Novel data with specified ID not found", + "草稿名称不能和任务名称相同,请修改任务名称": 'Draft name cannot be the same as task name, please modify the task name', + "导出剪映草稿成功!": "JianYing draft exported successfully!", + "没有找到导出剪映的执行文件,请检查": "JianYing export executable not found, please check", + "添加/修改小说信息成功!": "Novel information added/modified successfully!", + "添加/修改小说信息失败,{error}": "Failed to add/modify novel information, {error}", + "获取小说数据成功!": "Novel data retrieved successfully!", + "获取小说数据失败,{error}": "Failed to retrieve novel data, {error}", + "修改小说数据成功!": "Novel data modified successfully!", + "修改小说数据失败,{error}": "Failed to modify novel data, {error}", + "重置小说数据成功!": "Novel data reset successfully!", + "重置小说数据失败,{error}": "Failed to reset novel data, {error}", + "删除小说数据成功!": "Novel data deleted successfully!", + "删除小说数据失败,{error}": "Failed to delete novel data, {error}", + "获取小说数据错误,未找到指定条件的小说数据": "Error retrieving novel data, no novel data found matching specified criteria", + //#endregion + + //#region 小说任务 + '修改小说批次任务信息失败,{error}': 'Failed to modify novel batch task information, {error}', + "未找到对应的小说批次任务": 'Corresponding novel batch task not found', + "部分分镜的输出图片路径为空": "Some storyboard output image paths are empty", + "部分分镜的子图片路径数量不足或为空": "Some storyboard sub-image paths are insufficient or empty", + "没有找到对应的小说批次任务的图片输出地址,请检查": "Image output path for corresponding novel batch task not found, please check", + "图片文件 {sourceImagePath} 不存在,请检查": "Image file {sourceImagePath} does not exist, please check", + "将指定的图片放到主图中成功": 'Successfully set specified image as main image', + "将指定的图片放到主图中失败,{error}": "Failed to set specified image as main image, {error}", + "没有要删除的分镜数据,请检查": "No storyboard data to delete, please check", + "删除所有的生图数据成功": "All generated image data deleted successfully", + "删除所有的图片数据失败,{error}": "Failed to delete all image data, {error}", + "获取小说批次任务数据成功!": "Novel batch task data retrieved successfully!", + "获取小说批次任务数据失败,{error}": "Failed to retrieve novel batch task data, {error}", + "修改小说批次任务数据成功!": "Novel batch task data modified successfully!", + "修改小说批次任务数据失败,{error}": "Failed to modify novel batch task data, {error}", + "没有找到要删除的小说批次任务数据": "No novel batch task data to delete found", + "删除小说批次任务数据成功!": "Novel batch task data deleted successfully!", + "删除小说批次任务数据失败,{error}": "Failed to delete novel batch task data, {error}", + "批量添加小说批次任务数据失败,免费版只能添加一条数据": "Failed to batch add novel batch task data, free version can only add one record", + "批量添加小说批次任务数据失败,小说ID不能为空": "Failed to batch add novel batch task data, novel ID cannot be empty", + "添加小说批次任务数据成功!": "Novel batch task data added successfully!", + "添加小说批次任务数据失败,{error}": "Failed to add novel batch task data, {error}", + "获取小说批次任务生成进度成功!": "Novel batch task generation progress retrieved successfully!", + "获取小说批次任务生成进度失败,{error}": "Failed to retrieve novel batch task generation progress, {error}", + "获取小说批次任务的第一张图片路径成功!": "First image path of novel batch task retrieved successfully!", + "获取小说批次任务的第一张图片路径失败,{error}": "Failed to retrieve first image path of novel batch task, {error}", + "没有对应的小说分镜任务,请先添加分镜任务": "No corresponding novel storyboard task, please add storyboard task first", + "检测到图片没有出完,请先检查出图": "Detected incomplete image generation, please check image generation first", + "有分镜子图数量不足,无法进行一拆四": "Insufficient sub-images in storyboard, cannot perform 1-to-4 split", + "一拆四成功!": "1-to-4 split successful!", + "一拆四失败,{error}": "1-to-4 split failed, {error}", + "重置小说批次任务数据成功!": "Novel batch task data reset successfully!", + "重置小说批次任务数据失败,{error}": "Failed to reset novel batch task data, {error}", + "重置小说分镜数据(不包含批次任务本身数据),并且初始化全新的分镜信息失败,小说分镜数据不存在": "Failed to reset novel storyboard data (excluding batch task data itself) and initialize new storyboard information, novel storyboard data does not exist", + "重置小说分镜数据(不包含批次任务本身数据),并且初始化全新的分镜信息失败,{error}": "Failed to reset novel storyboard data (excluding batch task data itself) and initialize new storyboard information, {error}", + "没有找到对应的小说数据,请先添加小说": "No corresponding novel data found, please add novel first", + "srt文件路径不能为空!": "SRT file path cannot be empty!", + "srt文件后缀不正确,请检查!": "SRT file extension incorrect, please check!", + "初始视频消息失败,未找到指定小说批次任务的分镜数据": "Failed to initialize video message, storyboard data for specified novel batch task not found", + '根据ID获取小说批次任务信息失败,ID不能为空!': 'Failed to retrieve novel batch task information by ID, ID cannot be empty!', + "未找到对应的小说批次任务信息,请检查!": "Corresponding novel batch task information not found, please check!", + //#endregion + + //#region 小说分镜 + "没有找到要更新的小说分镜信息": "No novel storyboard information to update found", + "修改小说分镜的VideoMessage成功!": "Novel storyboard VideoMessage modified successfully!", + "修改小说分镜的VideoMessage失败,{error}": "Failed to modify novel storyboard VideoMessage, {error}", + "查询小说分镜信息,查询条件不能为空!": "Query novel storyboard information, query conditions cannot be empty!", + '未找到小说分镜数据,请检查!': 'Novel storyboard data not found, please check!', + "没有找到可处理的小说分镜信息": 'No processable novel storyboard information found', + "当前分镜数据的MJ图转视频参数为空或参数校验失败,请检查": "Current storyboard data MJ image-to-video parameters are empty or validation failed, please check", + '当前Midjourney模式不支持视频生成功能,请更换为MJ API或本地代理模式后重试!': 'Current Midjourney mode does not support video generation, please switch to MJ API or local proxy mode and try again!', + 'Midjourney图转视频任务执行失败,失败信息如下:{error}': 'Midjourney image-to-video task execution failed, error details: {error}', + 'Midjourney图转视频任务执行完成。': 'Midjourney image-to-video task execution completed.', + 'Midjourney图转视频任务执行中...': 'Midjourney image-to-video task executing...', + '已成功提交Midjourney图转视频任务,任务ID:{taskId}': 'Successfully submitted Midjourney image-to-video task, Task ID: {taskId}', + "小说批次任务的分镜数据的转视频配置为空,请检查": "Video conversion configuration for storyboard data of novel batch task is empty, please check", + "分镜的图片没有全部出完,不能继续该操作!!": "Storyboard images are not all generated, cannot continue this operation!!", + "分镜 {name} 图片在本地未找到,不能继续该操作,请检查对应分镜的图片路径是否正确": "Storyboard {name} image not found locally, cannot continue this operation, please check if the corresponding storyboard image path is correct", + "分镜的图片全部存在,可以进行高清处理": "All storyboard images exist, upscaling can be performed", + "未找到指定ID的小说分镜信息,ID: {id}": "Novel storyboard information with specified ID not found, ID: {id}", + "未找到对应的属性,属性: {property}": "Corresponding property not found, property: {property}", + "数据不完整,缺少小说ID或者小说批次任务ID": "Data incomplete, missing novel ID or novel batch task ID", + "没有找到要更新的出图信息": "Image generation information to update not found", + "未找到执行的翻译的小说分镜数据,无法写回": "Novel storyboard data for translation execution not found, unable to write back", + '缺少必要的条件,必须传入id,bookId或者bookTaskId其中一个': 'Missing required conditions, must provide one of id, bookId or bookTaskId', + "未找到对应的小说分镜信息": "Corresponding novel storyboard information not found", + "所有分镜文案内容为空,无法进行AI合并": "All storyboard content is empty, unable to perform AI merging", + "不支持的分镜合并类型": "Unsupported storyboard merge type", + "上传图片,并修改小说信息成功": "Upload image and modify novel information successfully", + "上传图片,并修改小说信息失败,{error}": "Failed to upload image and modify novel information, {error}", + "未检测到分镜的输出图片,无法进行高清处理": 'No storyboard output images detected, unable to perform upscaling', + "分镜高清图片成功": "Storyboard image upscaling successful", + "分镜高清图片失败,{error}": "Storyboard image upscaling failed, {error}", + "图片裁剪失败": 'Image cropping failed', + "下载指定的图片地址并且分割成功": "Successfully downloaded and split specified image URL", + "下载指定的图片地址并且分割失败,{error}": "Failed to download and split specified image URL, {error}", + "只有MJ模式下才能使用这个功能": "This function can only be used in MJ mode", + "只有MJ API模式下才能使用这个功能": "This function can only be used in MJ API mode", + "分镜中没有MJ的消息数据,请检查分镜数据": "No MJ message data in storyboard, please check storyboard data", + "没有找到对应分镜的MJ Task ID,请检查分镜数据": "Corresponding storyboard MJ Task ID not found, please check storyboard data", + "没有找到对应的分镜的MJ图片链接,请检查分镜数据": "Corresponding storyboard MJ image link not found, please check storyboard data", + "获取图片链接并且下载成功": "Successfully retrieved image link and downloaded", + "获取图片链接并且下载失败,{error}": "Failed to retrieve image link and download, {error}", + "没有找到要推理的分镜数据": "No storyboard data to infer found", + "推理所有数据完成": 'All data inference completed', + "推理所有数据失败,{error}": "All data inference failed, {error}", + "AI分镜头合并成功": 'AI storyboard merging successful', + "未知的合并模式,请检查": 'Unknown merge mode, please check', + "合并提示词失败,{error}": "Merge prompts failed, {error}", + "分析的类型只能是角色或场景,请检查": "Analysis type can only be character or scene, please check", + "没有找到要分析的分镜数据,请先导入文案或者时srt!": "No storyboard data to analyze found, please import content or SRT file first!", + "未知的分析类型,请检查": 'Unknown analysis type, please check', + "自动分析角色或场景成功": 'Automatic character or scene analysis successful', + "自动分析角色或场景失败,{error}": "Automatic character or scene analysis failed, {error}", + "获取小说分镜数据成功!": "Novel storyboard data retrieved successfully!", + "获取小说分镜数据失败,{error}": "Failed to retrieve novel storyboard data, {error}", + "修改小说分镜数据成功!": "Novel storyboard data modified successfully!", + "修改小说分镜数据失败,{error}": "Failed to modify novel storyboard data, {error}", + "保存小说分镜数据成功!": "Novel storyboard data saved successfully!", + "保存小说分镜数据失败,{error}": "Failed to save novel storyboard data, {error}", + "目前只支持对小说批次任务的文案保存": "Currently only supports content saving for novel batch tasks", + "重置小说分镜数据成功!": "Novel storyboard data reset successfully!", + "重置小说分镜数据失败,{error}": "Failed to reset novel storyboard data, {error}", + "初始化分镜视频消息失败,{error}": "Failed to initialize storyboard video message, {error}", + "小说分镜ID不能为空": "Novel storyboard ID cannot be empty", + "使用GPT翻译不支持拆分": "GPT translation does not support splitting", + "GPT翻译只支持中英互译": "GPT translation only supports Chinese-English mutual translation", + "AI翻译 {source} 译 {target} 成功": "AI translation {source} to {target} successful", + "反推提示词的ID不能为空": "Reverse prompt ID cannot be empty", + "全部翻译完成": "All translation completed", + "翻译失败,{error}": "Translation failed, {error}", + //#endregion + + //#region 出图 + "ComfyUI生图失败,{error}": "ComfyUI image generation failed, {error}", + 'ComfyUI的工作流设置为空,请检查是否正确设置!!': "ComfyUI workflow configuration is empty, please check if properly configured!!", + "未找到选中的工作流,请检查是否正确设置!!": "Selected workflow not found, please check if properly configured!!", + "本地未找到选中的工作流文件地址,请检查是否正确设置!!": "Selected workflow file path not found locally, please check if properly configured!!", + "工作流文件内容不是有效的JSON格式,请检查是否正确设置!!": "Workflow file content is not valid JSON format, please check if properly configured!!", + "工作流文件内容不是有效的JSON对象格式,请检查是否正确设置!!": "Workflow file content is not valid JSON object format, please check if properly configured!!", + "错误信息:{error},错误节点:{node}": "Error message: {error}, Error node: {node}", + "未知错误,未获取到请求ID,请检查是否正确设置!!": "Unknown error, request ID not obtained, please check if properly configured!!", + "ComfyUI 生成图片成功!": "ComfyUI image generation successful!", + "ComfyUI生图失败,未获取到请求ID,请检查是否正确设置!!": "ComfyUI image generation failed, request ID not obtained, please check if properly configured!!", + "未获取到ComfyUI的请求地址,请检查是否正确设置!!": "ComfyUI request URL not obtained, please check if properly configured!!", + "ComfyUI 生图失败,详细失败信息看启动器控制台": 'ComfyUI image generation failed, see launcher console for detailed error information', + "FLUX FORGE 生成图片成功!": "FLUX FORGE image generation successful!", + "FLUX FORGE 生成图片失败,{error}": "FLUX FORGE image generation failed, {error}", + "未知的合并类型": "Unknown merge type", + "SD合并提示词成功!": "SD prompt merge successful!", + "SD合并提示词失败,{error}": "SD prompt merge failed, {error}", + 'SD生成图片成功!': "SD image generation successful!", + "SD生成图片失败,{error}": "SD image generation failed, {error}", + //#endregion + + //#region 关于 + "软件信息": "Software Information", + "LaiTool PRO是一款AI说推文工具,帮助用户提升工作效率,快速高效的完成AI漫画推文的制作": "LaiTool PRO is an AI storytelling tool that helps users improve work efficiency and quickly complete AI comic content creation", + "专业的AI创作工具套件,集成图像生成、智能文案、语音合成等多项前沿AI技术,助力创作者高效产出优质内容": "Professional AI creation toolkit integrating image generation, intelligent copywriting, voice synthesis and other cutting-edge AI technologies to help creators efficiently produce high-quality content", + "授权信息": "License Information", + "机器码": "Machine Code", + "授权码": 'License Key', + "授权状态": "License Status", + "激活": "Activated", + "停用": "Deactivated", + "授权时间": 'License Time', + "未授权": "Unlicensed", + "到期时间": "Expiration Time", + "© 2025 LaiTool PRO. 保留所有权利": "© 2025 LaiTool PRO. All rights reserved", + "您访问的页面不存在或已被移除": "The page you are looking for does not exist or has been removed", + "返回首页": "Return to Home", + "版本 {version}": "Version {version}", + "快速开始": "Quick Start", + "使用文档": "Documentation", + "核心功能": "Core Features", + '更新日志': "Changelog", + "最新版本:{version}": "Latest Version: {version}", + '查看全部': "View All", + '最新': "Latest", + '联系支持': "Contact Support", + "需要帮助?": "Need Help?", + '我们提供多种方式为您解答疑问': "We provide multiple ways to answer your questions", + '友情链接': "Friendly Links", + '推荐的工具和资源': "Recommended Tools and Resources", + '关于软件': "About Software", + '使用条款': "Terms of Use", + '隐私政策': "Privacy Policy", + '问题反馈': "Feedback", + "聚合API平台,提供多种API服务": "Integrated API platform providing various API services", + "B站-向北": "Bilibili-Xiangbei", + "向北的B站频道,提供LaiTool相关视频教程": "Xiangbei's Bilibili channel providing LaiTool-related video tutorials", + 'AI艺术创作平台': "AI Art Creation Platform", + '开源AI绘画工具': "Open Source AI Drawing Tool", + '系统状态': "System Status", + '正常运行': "Running Normally", + '已激活': "Activated", + '性能状态': "Performance Status", + '良好': "Good", + '服务状态': "Service Status", + '在线': "Online", + 'AI图像生成': "AI Image Generation", + "支持MJ、SD等多种AI绘图模型": "Supports various AI drawing models such as MJ and SD", + '智能文案创作': "Intelligent Content Creation", + '基于AI的文案生成与优化': "AI-based content generation and optimization", + '语音合成': "Voice Synthesis", + '高质量的AI语音生成服务': "High-quality AI voice generation service", + '项目管理': "Project Management", + '统一管理创作项目和素材': "Unified management of creative projects and materials", + '批量生成': "Batch Generation", + '支持批量处理和生成任务': "Support batch processing and generation tasks", + '二创反推': "Secondary Creation Reverse", + '从现有作品反推创作参数': "Reverse creation parameters from existing works", + '任务队列管理': "Task Queue Management", + '管理和监控后台任务执行': "Manage and monitor background task execution", + '新增': "Added", + '优化': 'Optimized', + '修复': "Fixed", + '其他': "Other", + '打开首页': "Open Homepage", + '返回软件首页': "Return to software homepage", + '邮箱支持': "Email Support", + '发送邮件咨询': 'Send email inquiry', + '联系开发者': 'Contact Developer', + '添加开发者微信获取支持': "Add developer WeChat for support", + '微信交流群': "WeChat Group", + '仅限永久VIP用户可加入': "Only permanent VIP users can join", + '加入用户交流群': "Join user group", + "如有问题或建议,欢迎通过邮箱与我们联系:{data}": "If you have any questions or suggestions, please contact us via email: {data}", + '发送邮件': 'Send Email', + '已复制到剪贴板': 'Copied to clipboard', + "更新提醒!": "Update Reminder!", + "当前版本为 {currentVersion} ,最新版本为 {latestVersion} ,请及时更新!": "Current version is {currentVersion}, latest version is {latestVersion}, please update in time!", + "更新内容!": "Update Content!", + //#endregion + + //#region 任务 + '失败原因': "Failure Reason", + "添加出图任务失败,未找到可生成图片的分镜,或者分镜都被锁定": "Failed to add image generation task, no available storyboards found or all storyboards are locked", + "添加出图任务失败,不支持的出图类型": "Failed to add image generation task, unsupported image generation type", + "停止出图任务失败,参数错误": "Failed to stop image generation task, parameter error", + "停止出图任务失败,没有需要停止的任务": "Failed to stop image generation task, no tasks to stop", + "用户手动取消了任务": "User manually cancelled the task", + "停止所有批次的出图任务成功": "Successfully stopped all batch image generation tasks", + "停止当前批次的出图任务成功": "Successfully stopped current batch image generation tasks", + "成功返回数据": "Successfully returned data", + "未知报错,没有捕获的错误": "Unknown error, uncaught exception", + "未找到指定的后台任务数据": "Specified background task data not found", + '任务被丢弃': "Task was discarded", + "任务队列过多,{id} 重新提交排队": "Too many tasks in queue, {id} resubmitted to queue", + "MJ生图成功,分镜ID:{bookTaskDetailId},任务ID:${taskId}": "MJ image generation successful, Storyboard ID: {bookTaskDetailId}, Task ID: ${taskId}", + "第 {attempts} 请求失败,开始下一次重试,失败信息如下,{error}": "Request {attempts} failed, starting next retry, error details: {error}", + "失败次数超过 {retries} 错误信息如下,{error}": "Failure count exceeded {retries}, error details: {error}", + '所有重试失败': "All retries failed", + "请求失败,状态码:{statusCode}": "Request failed, status code: {statusCode}", + "试用时间已到,请联系客服": "Trial period expired, please contact customer service", + "生图失败,共5次尝试,目前第 {retryCount} 次": "Image generation failed, 5 attempts total, currently attempt {retryCount}", + "任务 {taskId} 执行失败,错误信息:{error},开始清理整个批次,批次ID:{batchId}": "Task {taskId} execution failed, error: {error}, starting to clean entire batch, Batch ID: {batchId}", + "修改后台队列任务失败,数据不完整,缺少必要字段": "Failed to modify background queue task, incomplete data, missing required fields", + "缺少必要的删除条件,至少需要id、bookId、bookTaskId其中一个": "Missing required deletion conditions, need at least one of id, bookId, bookTaskId", + "任务已提交": "Task submitted", + "任务正在执行中": "Task is executing", + "未知的任务类型": "Unknown task type", + "{taskName}_{taskId} 任务添加调度完成": "{taskName}_{taskId} task scheduling completed", + "任务调度失败,请手动重试": "Task scheduling failed, please retry manually", + "处理 {taskType} 类型任务 {taskName} 失败,失败信息如下,{error}": "Failed to process {taskType} type task {taskName}, error details: {error}", + "启动后台任务成功": "Background task started successfully", + "启动后台任务失败,{error}": "Failed to start background task, {error}", + "添加后台任务成功": 'Background task added successfully', + "添加后台任务失败,{error}": "Failed to add background task, {error}", + "添加多个任务成功": "Multiple tasks added successfully", + "添加多个任务失败,{error}": "Failed to add multiple tasks, {error}", + "获取指定状态的任务成功": "Successfully retrieved tasks with specified status", + "获取指定状态的任务失败,{error}": "Failed to retrieve tasks with specified status, {error}", + "获取后台任务集合成功": "Background task collection retrieved successfully", + "获取后台任务集合失败,{error}": "Failed to retrieve background task collection, {error}", + "修改后台任务成功": "Background task modified successfully", + "修改后台任务失败,{error}": "Failed to modify background task, {error}", + "任务添加成功,任务名称:{taskName}": "Task added successfully, Task name: {taskName}", + "任务添加失败,失败信息如下:{error}": "Task addition failed, error details: {error}", + '未找到对应ID的任务,任务ID:{taskId}': 'Task with corresponding ID not found, Task ID: {taskId}', + //#endregion + + //#region 加载 + "正在初始化系统...": "Initializing system...", + "加载授权信息配置...": "Loading license configuration...", + '连接服务器...': 'Connecting to server...', + '准备应用资源...': 'Preparing application resources...', + '即将完成...': 'Almost done...', + "软件加载完成,即将进入主界面...": 'Software loading completed, entering main interface...', + '启动任务队列失败': "Failed to start task queue", + '任务队列启动成功': 'Task queue started successfully', + '加载失败,请重试...': 'Loading failed, please retry...', + "错误信息,{error}": "Error message, {error}", + '加载系统外观设置失败': "Failed to load system appearance settings", + "获取机器码失败,请重启软件或者检查对应权限!!": "Failed to get machine code, please restart software or check permissions!!", + "授权验证失败,即将前往授权界面进行授权!": "License verification failed, going to license interface for authorization!", + "加载授权码配置失败,{error}": "Failed to load license code configuration, {error}", + "初始化通用设置失败,{error}": "Failed to initialize general settings, {error}", + "初始化MJ通用设置失败,{error}": "Failed to initialize MJ general settings, {error}", + "初始化MJ API设置失败,{error}": "Failed to initialize MJ API settings, {error}", + "初始化MJ生图包设置失败,{error}": "Failed to initialize MJ image generation package settings, {error}", + "初始化MJ代理模式设置失败,{error}": "Failed to initialize MJ proxy mode settings, {error}", + "初始化MJ本地代理模式设置失败,{error}": "Failed to initialize MJ local proxy mode settings, {error}", + "初始化推理设置失败,{error}": "Failed to initialize inference settings, {error}", + "初始化SD设置失败,{error}": "Failed to initialize SD settings, {error}", + "初始化修手/修脸模型设置失败,{error}": "Failed to initialize hand/face fix model settings, {error}", + "初始化剪映关键帧设置失败,{error}": "Failed to initialize JianYing keyframe settings, {error}", + "初始化ComfyUI设置失败,{error}": "Failed to initialize ComfyUI settings, {error}", + "初始化ComfyUI工作流设置失败,{error}": "Failed to initialize ComfyUI workflow settings, {error}", + "初始化特殊符号字符串失败,{error}": "Failed to initialize special symbol string, {error}", + //#endregion + + //#region 授权 + '软件授权': "Software License", + '机器码自动生成': "Machine code auto-generated", + '授权文档': "License Documentation", + '立即前往': "Go Now", + '确认授权': 'Confirm License', + '授权说明': "License Instructions", + '机器码已复制到剪贴板': "Machine code copied to clipboard", + '授权验证失败': "License verification failed", + '同步授权信息失败': "Failed to sync license information", + "授权失败,{error}": "License failed, {error}", + '机器码为空,无法复制': 'Machine code is empty, cannot copy', + ",即将前往授权界面进行授权!": ", going to license interface for authorization!", + "机器码校验错误,即将前往授权界面进行授权!": "Machine code verification error, going to license interface for authorization!", + "授权成功,即将进入加载界面!!": "License successful, entering loading interface!!", + "1、需要授权请查看授权文档,按照操作获取对应的授权": "1. For licensing, please check the license documentation and follow the instructions to obtain the corresponding license", + "2、机器码每次启动软件会自动生成,会检测本机的机器码、主板信息、当前登录的用户信息等": "2. Machine code is automatically generated each time the software starts, detecting local machine code, motherboard information, current user information, etc.", + //#endregion + + //#region 工具箱 + '图片压缩工具': "Image Compression Tool", + '选择图片': "Select Image", + '尺寸设置': "Size Settings", + '压缩设置': "Compression Settings", + '输出格式': "Output Format", + '原始图片': "Original Image", + '未选择图片': "No image selected", + '压缩后图片': "Compressed Image", + '压缩信息': "Compression Info", + '原始大小': "Original Size", + '压缩后大小': "Compressed Size", + '尺寸减少': "Size Reduction", + '压缩比率': "Compression Ratio", + '下载压缩图片': "Download Compressed Image", + '清空重置': "Clear and Reset", + '已清空所有数据': "All data cleared", + '刷新数据': "Refresh Data", + '导出数据': "Export Data", + '文件类型': "File Type", + '上传时间': "Upload Time", + '文件地址已复制到剪贴板': "File address copied to clipboard", + '图片名称': "Image Name", + '点击或拖拽图片到此区域上传': "Click or drag images to this area to upload", + '请等待当前文件处理完成': "Please wait for current file processing to complete", + '开始上传': "Start Upload", + '清空选择': "Clear Selection", + '上传说明': "Upload Instructions", + '请先选择文件': "Please select a file first", + '请等待当前文件上传完成': "Please wait for current file upload to complete", + '无法加载图片': "Unable to load image", + '无法读取文件': "Unable to read file", + '图片上传工具': "Image Upload Tool", + '工具总数': "Total Tools", + '分类数量': "Category Count", + '路由路径未配置': "Route path not configured", + '外部链接未配置': "External link not configured", + '没有找到相关工具': "No related tools found", + '文档中心': "Documentation Center", + '点击下方按钮在您的默认浏览器中打开文档': "Click the button below to open documentation in your default browser", + '在浏览器中打开文档': "Open documentation in browser", + '复制到剪贴板': "Copy to Clipboard", + '媒体工具': "Media Tools", + "LaiTool 图床": "LaiTool Image Host", + '转换': "Convert", + '格式': "Format", + '图片压缩助手': "Image Compression Assistant", + '压缩': "Compression", + "将图片进行压缩,支持多种图片格式,减小文件大小": "Compress images, supports multiple image formats, reduces file size", + "将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接": "Upload images to LaiTool image host, supports multiple image formats, get shareable links", + "搜索工具...": "Search tools...", + "上传图片到LaiTool图床,获取图片链接": "Upload images to LaiTool image host, get image links", + "上传失败,{error}": "Upload failed, {error}", + '未找到机器ID,请重启软件后重试!!': 'Machine ID not found, please restart software and try again!!', + '图片处理完毕,开始上传文件...': 'Image processing completed, starting file upload...', + "开始处理图片文件...": "Starting to process image file...", + "文件超出限制,请压缩后上传!!": "File exceeds limit, please compress before uploading!!", + "开始上传文件...": "Starting file upload...", + "已选择文件:{fileName}": "Selected file: {fileName}", + "单日上传次数限制:5": "Daily upload limit: 5", + "文件大小限制:最大 5MB": "File size limit: Maximum 5MB", + "支持的格式:JPG、PNG、JPEG、WebP": "Supported formats: JPG, PNG, JPEG, WebP", + "每次只能上传一个文件,并且上传的文件会留存在服务器,介意请勿用!!!": "Only one file can be uploaded at a time, uploaded files will be stored on the server, please do not use if you mind!!!", + "选择文件后点击 “开始上传” 按钮进行上传": 'After selecting a file, click the "Start Upload" button to upload', + "支持 JPG、PNG、GIF、WebP 格式,单个文件不超过 5MB": "Supports JPG, PNG, GIF, WebP formats, single file should not exceed 5MB", + "正在处理中...": "Processing...", + "{data} 导出失败,{error}": "{data} export failed, {error}", + "{data} 数据导出成功": "{data} data export successful", + "共 {count} 条": "Total {count} items", + "导出为 {data}": "Export as {data}", + "已上传图片 {count}": "Uploaded {count} images", + "压缩错误,{error}": "Compression error, {error}", + "请选择图片文件 (JPG, PNG, WebP)": "Please select image file (JPG, PNG, WebP)", + "减少 {ratio}%,节省 {saved}": "Reduced {ratio}%, saved {saved}", + '等待压缩...': 'Waiting for compression...', + "压缩中...": "Compressing...", + "图片质量:{quality}%": "Image quality: {quality}%", + "最大高度:{height}px": "Maximum height: {height}px", + "最大宽度:{width}px": "Maximum width: {width}px", + "支持 JPG、PNG、WebP 格式": "Supports JPG, PNG, WebP formats", + "快速压缩图片,本地处理,安全可靠": "Fast image compression, local processing, safe and reliable", + //#endregion + + //#region 设置 + 'AI推理': "AI Inference", + '翻译服务': "Translation Service", + "API服务商": "API Provider", + "API令牌": "API Token", + '推理模型': "Inference Model", + '推理模式': "Inference Mode", + '自定义推理预设': "Custom Inference Preset", + '翻译设置': "Translation Settings", + '翻译模型': "Translation Model", + '编辑自定义预设': "Edit Custom Preset", + '编辑当前选中的自定义推理预设': "Edit currently selected custom inference preset", + '删除自定义预设': "Delete Custom Preset", + '删除当前选中的自定义推理预设': "Delete currently selected custom inference preset", + '编辑自定义推理预设': "Edit Custom Inference Preset", + '确定删除': "Confirm Delete", + '未找到要删除的预设': "Preset to delete not found", + '未知的操作类型': "Unknown operation type", + '设置加载成功': "Settings loaded successfully", + '未知的测试类型': "Unknown test type", + '更新预设': "Update Preset", + '保存预设': "Save Preset", + '预设配置': "Preset Configuration", + '系统提示词': "System Prompt", + '用户提示词': "User Prompt", + '占位符测试': "Placeholder Test", + '清空值': "Clear Values", + '测试提示词': "Test Prompt", + '检测到的占位符': "Detected Placeholders", + '占位符值设置': "Placeholder Value Settings", + '测试状态': "Test Status", + '角色信息内容': "Character Information Content", + '场景信息内容': "Scene Information Content", + '上下文内容': "Context Content", + '没有选择API服务商': "No API provider selected", + '无效的API服务商': "Invalid API provider", + '没有设置推理模型': "No inference model set", + '占位符值已清空': "Placeholder values cleared", + '暂无预览内容': "No preview content available", + '测试结果预览': "Test Result Preview", + '新增预设': "Add Preset", + "编辑预设": "Edit Preset", + '当前修改的预设不存在': "The preset being modified does not exist", + '预设ID不存在': "Preset ID does not exist", + '预设不存在': "Preset does not exist", + '未找到对应的预设数据': 'Corresponding preset data not found', + '修改MJ账号': "Modify MJ Account", + '添加MJ账号': "Add MJ Account", + '服务器ID': "Server ID", + '频道ID': "Channel ID", + 'MJ私信ID': "MJ Direct Message ID", + 'Niji私信ID': "Niji Direct Message ID", + "并发/队列": "Concurrency/Queue", + '用户token': "User Token", + '账号并发数': "Account Concurrency", + '等待队列': "Wait Queue", + '任务超时时间': "Task Timeout", + '是否启用': "Enable/Disable", + '用户Agent': "User Agent", + '本地代理模式的请求地址不能为空': "Local proxy mode request address cannot be empty", + '本地代理模式的访问令牌不能为空': "Local proxy mode access token cannot be empty", + '修改不能没有账号实例ID': "Modification cannot be without account instance ID", + '网络请求失败': "Network request failed", + '账号修改成功': "Account modified successfully", + '账号创建成功': "Account created successfully", + '速度模式': "Speed Mode", + '注意事项': "Important Notes", + "API设置加载成功": "API settings loaded successfully", + '请求地址': "Request URL", + '访问令牌': "Access Token", + '新增账号': "Add Account", + '同步账号': "Sync Account", + '同步服务器中的账号信息到本地': "Sync account information from server to local", + '账号列表': "Account List", + '全量代理模式部署': "Full Proxy Mode Deployment", + '编辑当前账号信息': "Edit Current Account Information", + '删除本地账号': "Delete Local Account", + '删除服务账号': "Delete Service Account", + '确认同步': "Confirm Sync", + '请求服务器账号列表失败': "Failed to request server account list", + '远程服务器返回数据格式错误': "Remote server returned data format error", + '本地代理模式设置加载成功': "Local proxy mode settings loaded successfully", + '本地代理模式设置保存成功': "Local proxy mode settings saved successfully", + '生图包设置': "Image Generation Package Settings", + '生图包服务商': "Image Generation Package Provider", + '跳转到购买页面': "Go to Purchase Page", + '已打开查询页面': "Query page opened", + '生图包设置加载成功': "Image generation package settings loaded successfully", + '生图包设置保存成功': "Image generation package settings saved successfully", + '代理模式设置': "Proxy Mode Settings", + '是否国内转发': "Domestic Forwarding", + '账号删除成功': "Account deleted successfully", + '同步账号信息成功': "Account information synced successfully", + '代理模式设置加载成功': "Proxy mode settings loaded successfully", + '代理模式设置保存成功': "Proxy mode settings saved successfully", + '出图方式': "Image Generation Method", + '查看教程': "View Tutorial", + '生图机器人': "Image Generation Bot", + '机器人模型': "Bot Model", + '生图比例': "Image Aspect Ratio", + '命令后缀': "Command Suffix", + '功能未开放': "Feature Not Available", + '保存设置': "Save Settings", + '执行并发': "Execution Concurrency", + '暂无该模式的教程': "No tutorial available for this mode", + '外观设置': "Appearance Settings", + '主题颜色': "Theme Color", + '工作流名称': "Workflow Name", + '工作流文件': "Workflow File", + '检查工作流文件': "Check Workflow File", + '更新成功': "Update Successful", + '未选择任何文件': "No file selected", + '正向提示词': "Positive Prompt", + '反向提示词': "Negative Prompt", + '工作流文件检查成功通过': "Workflow file check passed successfully", + "ComfyUI 基础设置": "ComfyUI Basic Settings", + '使用工作流': "Use Workflow", + '获取ComfyUI通用设置失败': "Failed to get ComfyUI general settings", + '获取ComfyUI工作流设置失败': "Failed to get ComfyUI workflow settings", + '添加工作流': "Add Workflow", + '编辑工作流': "Edit Workflow", + '删除成功': "Deleted Successfully", + '剪映草稿目录': "CapCut Draft Directory", + '手动选择剪映草稿地址': "Manually Select CapCut Draft Path", + '自动获取剪映草稿地址': "Auto Get CapCut Draft Path", + '打开当前选择的剪映草稿文件夹': "Open Currently Selected CapCut Draft Folder", + '项目文件夹': "Project Folder", + '手动选择项目文件夹': "Manually Select Project Folder", + '打开当前选择的项目文件夹': "Open Currently Selected Project Folder", + '任务并发数': "Task Concurrency", + '默认生图方式': "Default Image Generation Method", + '高清倍数': "HD Multiplier", + '默认图转视频方式': "Default Image to Video Method", + '语言': "Language", + '项目文件复制成功': "Project files copied successfully", + "左右关键帧": "Left-Right Keyframes", + "上下关键帧": "Up-Down Keyframes", + "缩放关键帧": "Zoom Keyframes", + "随机关键帧": "Random Keyframes", + '打帧方式': "Frame Method", + '匀速关键帧': "Uniform Keyframes", + '匀速关键帧时间': "Uniform Keyframe Duration", + '上下关键帧设置': "Up-Down Keyframe Settings", + '上关键帧位置': "Up Keyframe Position", + '下关键帧位置': "Down Keyframe Position", + '缩放大小': "Zoom Size", + '左右关键帧设置': "Left-Right Keyframe Settings", + '左关键帧位置': "Left Keyframe Position", + '右关键帧位置': "Right Keyframe Position", + '缩放关键帧设置': "Zoom Keyframe Settings", + '开始缩放大小': "Start Zoom Size", + '结束缩放大小': "End Zoom Size", + "SD 基础设置": "SD Basic Settings", + '加载数据': "Load Data", + '单次出图张数': "Images Per Generation", + '种子值': "Seed Value", + '重绘幅度': "Denoising Strength", + '采样方式': "Sampling Method", + '迭代步数': "Steps", + '图片分辨率': "Image Resolution", + '已启用的模型': "Enabled Models", + '添加模型': "Add Model", + '重置设置': "Reset Settings", + '文生图': "Text to Image", + '图生图': "Image to Image", + '识别信任度': "Recognition Confidence", + 'SD远程数据加载成功': "SD remote data loaded successfully", + '请先保存或取消当前编辑的模型': "Please save or cancel the currently edited model first", + '重置模型成功': "Model reset successful", + '开发者微信': "Developer WeChat", + '二维码图片': "QR Code Image", + '请联系管理员获取': "Please contact administrator to obtain", + '联系须知': "Contact Information", + "加入VIP用户交流群": "Join VIP User Community", + '群内福利': "Group Benefits", + '获取最新版本信息和更新通知': "Get latest version info and update notifications", + '与其他用户交流使用技巧和经验': "Share tips and experiences with other users", + '快速反馈问题和建议': "Quick feedback for issues and suggestions", + '获得开发团队的技术支持': "Get technical support from development team", + '联系管理员邀请入群或者联系对应代理': "Contact administrator for group invitation or contact corresponding agent", + "注:请勿在群内发布广告或无关内容,保持良好交流氛围": "Note: Please do not post ads or irrelevant content in the group, maintain good communication atmosphere", + "添加时请备注:LAITool用户": "Please note when adding: LAITool user", + "工作时间:周一至周五 9:00-18:00": "Working hours: Monday to Friday 9:00-18:00", + "支持技术咨询、BUG反馈、功能建议": "Support technical consultation, bug feedback, feature suggestions", + "请详细描述您遇到的问题,以便快速解决": "Please describe your problems in detail for quick resolution", + "如无法扫码,可复制微信号:": "If unable to scan QR code, copy WeChat ID:", + "或发送邮件至:": "Or send email to:", + '微信号已复制到剪贴板': 'WeChat ID copied to clipboard', + "扫描下方二维码添加开发者微信,获取专业技术支持": "Scan the QR code below to add developer WeChat for professional technical support", + '识别信任度必须在0到1之间': "Recognition confidence must be between 0 and 1", + "删除模型成功: {model}": "Model deleted successfully: {model}", + "更新模型成功,模型 {model}": "Model updated successfully, model {model}", + "添加模型成功,模型名称 {name}": "Model added successfully, model name {name}", + "SD远程数据加载失败,{error}": "SD remote data loading failed, {error}", + '请输入 图片高度': 'Please enter image height', + "请输入 图片宽度": "Please enter image width", + "修脸/修手": "Face/Hand Fix", + "请选择是否开始修脸/修手": "Please select whether to start face/hand fixing", + "ADetailer 模型设置": "ADetailer Model Settings", + "请完善所有的关键帧设置!!": 'Please complete all keyframe settings!!', + "LAI API - 香港": "LAI API - Hong Kong", + 'LAI API - 美国': 'LAI API - USA', + 'LaiTool生图包': 'LaiTool Image Generation Package', + "没有找到对应的API的配置,请先检查配置": "No corresponding API configuration found, please check configuration first", + "API模式": "API Mode", + '代理模式': "Proxy Mode", + '本地代理模式(自有账号推荐)': 'Local proxy mode (recommended for own accounts)', + '快速': "Fast", + '慢速': "Slow", + "复制文件夹成功!": "Folder copied successfully!", + "复制文件夹失败,{error}": "Failed to copy folder, {error}", + "获取 {description} 失败,{error}": "Failed to get {description}, {error}", + "获取剪映草稿文件列表成功!": "CapCut draft file list retrieved successfully!", + "获取剪映草稿文件列表失败,{error}": "Failed to get CapCut draft file list, {error}", + "依赖的草稿文件不存在,请检查": "Dependent draft file does not exist, please check", + "剪映草稿文件数据文件不存在,请先检查": "CapCut draft file data file does not exist, please check first", + "剪映草稿文件格式错误,请检查": "CapCut draft file format error, please check", + "剪映草稿文件格式错误,没有轨道,请检查": "CapCut draft file format error, no tracks found, please check", + "剪映草稿文件格式错误,主轨道不是Video,请检查": "CapCut draft file format error, main track is not Video, please check", + "剪映草稿文件格式错误,主轨道没有对应的图片节点,请检查": "CapCut draft file format error, main track has no corresponding image nodes, please check", + "当前新增的任务对应的图片数量和依赖的草稿的主轨道的图片数量不一致,请检查": "The number of images for the current new task does not match the number of images in the main track of the dependent draft, please check", + "图片数量不对应,请检查": "Image count mismatch, please check", + "草稿文件不存在,请检查": 'Draft file does not exist, please check', + "没有找到对应的节点": "No corresponding node found", + "未找到剪映相关数据,请手动填写或选择": "CapCut related data not found, please manually fill in or select", + "剪映草稿地址数据错误,请手动填写或选择": "CapCut draft path data error, please manually fill in or select", + "获取默认剪映草稿地址失败,{error}": "Failed to get default CapCut draft path, {error}", + "勾选匀速关键帧,当分镜时间小于设置的时间时,会按照时间的比例来调整关键帧位置,保证关键帧的匀速效果。": "Check uniform keyframes. When the shot time is less than the set time, the keyframe position will be adjusted according to the time ratio to ensure uniform keyframe effect.", + '已取消修改,恢复原始值,请重新手动保存!': 'Modification cancelled, original values restored, please save manually again!', + "复制项目文件失败,{error}": "Failed to copy project files, {error}", + "正在复制项目文件...": "Copying project files...", + "检测当前的项目地址已修改,继续执行会先复制当前所有的项目文件,请确保当前的文件没有被占用和软件无后台任务执行,若是执行失败,则需手动复制就项目文件夹中的所有数据到新项目文件夹位置,是否继续?": "Project path has been modified. Continuing will copy all current project files. Please ensure files are not in use and no background tasks are running. If failed, manually copy all data from old project folder to new project folder. Continue?", + "打开文件夹失败,文件夹地址为空": "Failed to open folder, folder path is empty", + "自动获取剪映草稿地址失败,{error}": "Failed to auto-get CapCut draft path, {error}", + "删除失败,{error}": "Delete failed, {error}", + "删除失败: 未找到要删除的工作流": "Delete failed: Workflow to delete not found", + "确定要删除工作流 “{name}” 吗?此操作不可撤销。是否继续?": 'Are you sure to delete workflow "{name}"? This operation cannot be undone. Continue?', + "当前选中的工作流不存在,请重新选择": "Currently selected workflow does not exist, please reselect", + "请求地址必须以 http 或者 https 开头": "Request URL must start with http or https", + "初始化设置失败,{error}": "Initialization settings failed, {error}", + "3 图像输出节点必须是 保存图像 节点,采样器只支持简单 K采样器和K采样器(高级)": "3. Image output node must be Save Image node, sampler only supports simple K sampler and K sampler (advanced)", + "2. 标题必须对应 正向提示词和反向提示词": "2. Title must correspond to positive prompt and negative prompt", + "1. Comfy UI的工作流中正向提示词和反向提示必须为 Clip文本编码 节点": "1. Positive and negative prompts in ComfyUI workflow must be CLIP Text Encode nodes", + "工作流文件缺少正向提示词或反向提示词,请检查工作流文件,把对应的文本编码模块的标题改为正向提示词和反向提示词!!": "Workflow file is missing positive or negative prompts. Please check the workflow file and change the corresponding text encoding module titles to positive prompt and negative prompt!!", + "工作流文件的格式不正确,请检查工作流文件": "Workflow file format is incorrect, please check the workflow file", + "工作流文件内容为空,请选择工作流文件": "Workflow file content is empty, please select a workflow file", + "读取的文件内容为空,请检查文件": "File content is empty, please check the file", + '读取文件成功!': 'File read successfully!', + "读取文件失败,{error}": "Failed to read file, {error}", + "{type}失败,{error}": "{type} failed, {error}", + "工作流名称已存在,请重新输入": "Workflow name already exists, please enter a new one", + "加载设置失败,{error}": "Failed to load settings, {error}", + "间隔时间(秒)": "Interval (seconds)", + "打开不同的出图方法的教程,根据选择的出图方式打开对应的教程": "Open tutorials for different image generation methods, open corresponding tutorials based on selected method", + "保存代理模式设置失败,{error}": "Failed to save proxy mode settings, {error}", + "加载代理模式设置失败,{error}": "Failed to load proxy mode settings, {error}", + "更改代理模式配置,ID不能为空": "Change proxy mode configuration, ID cannot be empty", + "代理模式的账号ID,服务ID,频道ID,用户Token不能为空": "Proxy mode account ID, service ID, channel ID, user token cannot be empty", + "开始同步远程账号信息...": "Starting to sync remote account information...", + "此操作将从远程代理服务器同步账号信息,会覆盖本地现有的账号配置。确定要继续吗?": "This operation will sync account information from remote proxy server and will overwrite existing local account configuration. Are you sure you want to continue?", + "删除账号失败,{error}": "Failed to delete account, {error}", + "确定要删除服务器ID为 “{guildId}” 的账号吗?此操作不可撤销。": 'Are you sure to delete the account with server ID "{guildId}"? This operation cannot be undone.', + "3. 开启 “国内转发” 选项可解决部分地区(如河南、福建等)的网络访问问题": '3. Enable "Domestic Forwarding" option to solve network access issues in some regions (such as Henan, Fujian, etc.)', + "2. 通过 “新增账号” 可添加多个MJ账号,实现并行处理,提高效率": '2. Add multiple MJ accounts through "Add Account" to achieve parallel processing and improve efficiency', + "1. 日常使用无需开启代理,仅添加账号时需要网络代理": "1. Daily use does not require proxy, only need network proxy when adding accounts", + "保存生图包设置失败,{error}": "Failed to save image generation package settings, {error}", + "加载生图包设置失败,{error}": "Failed to load image generation package settings, {error}", + "该生图包不支持查询,请联系管理员": "This image generation package does not support queries, please contact administrator", + "3. 出图稳定,采用官方接口,不会封号,保障长期稳定使用": "3. Stable generation, uses official API, no risk of ban, ensures long-term stable use", + "2. 支持 定制套餐,灵活的套餐选择,可根据使用频率和需求定制专属套餐方案": "2. Supports custom packages, flexible package options, can customize exclusive packages based on usage frequency and needs", + "1. 使用 无需科学上网,全球加速访问!延迟 30ms 以内,国内外用户均可稳定使用": "1. No VPN required, global accelerated access! Latency within 30ms, stable for both domestic and international users", + "保存本地代理模式设置失败,{error}": "Failed to save local proxy mode settings, {error}", + "加载本地代理模式设置失败,{error}": "Failed to load local proxy mode settings, {error}", + "未找到对应的账号,无法更新": "Corresponding account not found, cannot update", + "本地代理模式的账号ID,服务ID,频道ID,用户Token不能为空": "Local proxy mode account ID, service ID, channel ID, user token cannot be empty", + "更改本地代理模式配置,ID不能为空": "Change local proxy mode configuration, ID cannot be empty", + "添加失败,{error}": "Add failed, {error}", + "本地代理模式的频道ID,服务器ID,用户Token必填": "Local proxy mode channel ID, server ID, user token are required", + "代理模式的频道ID,服务器ID,用户Token必填": "Proxy mode channel ID, server ID, user token are required", + "确定要删除服务器ID为 “{guildId}” 的本地账号吗?此操作不会影响服务器账号,只会删除本地记录。": 'Are you sure to delete the local account with server ID "{guildId}"? This operation will not affect the server account, only delete the local record.', + "无法删除,账号信息无效": "Cannot delete, account information invalid", + "删除本地账号失败,{error}": "Failed to delete local account, {error}", + "未找到对应的账号,无法删除": "Corresponding account not found, cannot delete", + "打开编辑账号失败,{error}": "Failed to open edit account, {error}", + "同步账号失败,{error}": "Account sync failed, {error}", + "同步完成!已同步 {count} 个账号": "Sync completed! Synced {count} accounts", + "没有配置本地代理模式的基本信息,请检查请求地址和访问令牌": "Local proxy mode basic information not configured, please check request URL and access token", + "确定要从服务器同步账号信息到本地吗?此操作会覆盖本地的账号列表,是否继续?": "Are you sure you want to sync account information from server to local? This will overwrite local account list, continue?", + "打开新增账号失败,{error}": "Failed to open add account, {error}", + "删除服务账号信息,会同步删除本地账号信息": "Deleting service account info will also delete local account info", + "删除本地账号信息,对服务器信息不会有影响": "Deleting local account info will not affect server information", + "3. 通过账号管理功能可添加多个MJ账号,实现并行处理,提高效率": "3. Through account management, multiple MJ accounts can be added for parallel processing and improved efficiency", + "2. 访问令牌默认为 admin,如有修改请相应更新": "2. Access token defaults to admin, please update accordingly if modified", + "1. 本地代理模式支持本地部署和服务器部署,需要自己搭建部署,": "1. Local proxy mode supports local and server deployment, requires self-setup and deployment,", + "添加一个新的账号": "Add a new account", + "本地代理模式设置-自行部署服务": "Local Proxy Mode Settings - Self-Deployed Service", + "加载API设置失败,{error}": "Failed to load API settings, {error}", + "Just My Socks网络加速服务": "Just My Socks Network Acceleration Service", + "确保网络环境稳定,以保证服务正常运行,推荐稳定🪜:": "Ensure stable network environment for proper service operation, recommended stable 🪜:", + "4. 开启 “国内转发” 选项可解决部分地区(如河南、福建等)的网络访问问题": '4. Enable "Domestic Forwarding" option to solve network access issues in some regions (such as Henan, Fujian, etc.)', + "3. 支持 20并发请求,可同时处理多张图片生成任务,大大提高工作效率": "3. Supports 20 concurrent requests, can simultaneously process multiple image generation tasks, greatly improving work efficiency", + "2. 提供 快速慢速 两种出图方式,可根据需求选择": '2. Provides Fast and Slow image generation modes, choose according to your needs', + "1. 使用 无需科学上网,支持香港和美国节点,香港节点对大陆做了优化,延迟 100ms 以内": "1. No VPN required, supports Hong Kong and US nodes, Hong Kong node optimized for mainland China with latency under 100ms", + "必填字段核心线程数,队列大小,超时时间不能为空": "Required fields core thread count, queue size, and timeout cannot be empty", + "必填字段服务器ID,频道ID,用户token不能为空": "Required fields Server ID, Channel ID, and User Token cannot be empty", + "获取自定义预设失败,{error}": "Failed to get custom preset, {error}", + "预设名称已存在,请更换一个名称": "Preset name already exists, please choose a different name", + "未添加角色占位符,不能勾选 “必须包含角色信息”": 'Role placeholder not added, cannot check "Must contain role information"', + "打开预览失败,{error}": "Failed to open preview, {error}", + "AI 回复结果": "AI Response Result", + "没有设置API Token": "API Token not set", + "提示词预设测试失败,{error}": "Prompt preset test failed, {error}", + "测试完成,请在预览下面查看提示词": "Test completed, please check prompts in preview below", + "提示词预览已生成,开始测试连接": "Prompt preview generated, starting connection test", + "占位符 {{placeholder}} 的值无效": "Value for placeholder {{placeholder}} is invalid", + "当前文本内容(分镜文案)": "Current text content (storyboard script)", + "提示词测试完成,点击右上角 “预览” 查看详细内容": 'Prompt test completed, click "Preview" in the top right to see details', + "在提示词中使用占位符,这里会自动检测并显示": "Use placeholders in prompts, they will be automatically detected and displayed here", + "请输入用户提示词,支持占位符(如 {textContent})": "Please enter user prompt, supports placeholders (such as {textContent})", + "请输入系统提示词,支持占位符(如 {textContent})": "Please enter system prompt, supports placeholders (such as {textContent})", + "必须包含角色信息(检测角色分析)": "Must contain role information (detects role analysis)", + "可用占位符-软件会自动替换对应的占位符为实际文本": "Available placeholders - software will automatically replace corresponding placeholders with actual text", + "确定要删除预设 “{name}” 吗?此操作不可撤销。": 'Are you sure to delete preset "{name}"? This operation cannot be undone.', + "连接成功!{serverName} 运行正常": "Connection successful! {serverName} is running normally", + "测试链接失败,{error}": "Connection test failed, {error}", + "正在测试链接...": "Testing connection...", + "API服务商未选择或服务地址无效,请检查设置": "API provider not selected or service address invalid, please check settings", + "初始化数据失败,{error}": "Failed to initialize data, {error}", + "【自定义】": "[Custom]", + "获取自定义推理预设失败,{error}": "Failed to get custom inference preset, {error}", + "获取设置失败,{error}": "Failed to get settings, {error}", + "正在加载设置...": "Loading settings...", + "删除预设失败,{error}": "Failed to delete preset, {error}", + "自定义推理预设删除成功!": "Custom inference preset deleted successfully!", + "确定要删除自定义推理预设 {name} 吗?此操作不可撤销。": 'Are you sure to delete custom inference preset {name}? This operation cannot be undone.', + "打开编辑预设失败,{error}": "Failed to open edit preset, {error}", + "打开自定义推理预设失败,{error}": "Failed to open custom inference preset, {error}", + "自定义推理预设保存成功!": "Custom inference preset saved successfully!", + "购买链接不存在,请联系管理员": "Purchase link does not exist, please contact administrator", + "测试 翻译 链接": "Test Translation Link", + "测试 AI 链接": "Test AI Link", + "AI 设置": "AI Settings", + '设置 -> 推理设置': 'Settings -> Inference Settings', + '设置 -> 通用设置': 'Settings -> General Settings', + "设置 -> MJ设置": "Settings -> MJ Settings", + "设置 -> MJ设置 -> 生图包模式": "Settings -> MJ Settings -> Image Package Mode", + "设置 -> MJ设置 -> 本地代理模式": "Settings -> MJ Settings -> Local Proxy Mode", + "设置 -> MJ设置 -> API模式": "Settings -> MJ Settings -> API Mode", + "设置 -> MJ设置 -> 代理模式": "Settings -> MJ Settings -> Proxy Mode", + '设置 -> 分镜AI模型': 'Settings -> Storyboard AI Model', + '设置 -> SD设置': 'Settings -> SD Settings', + "设置 -> ComfyUI 设置": "Settings -> ComfyUI Settings", + "设置-> 通用设置 -> 项目地址": "Settings -> General Settings -> Project Path", + "文案处理->设置": "Content Processing -> Settings", + '未找到设置,请检查 {setting} 设置!': 'Settings not found, please check {setting} settings!', + "请检查 ‘{path}’ 的API提供商、API令牌、推理模型、推理模式等是不是存在!": 'Please check if API provider, API token, inference model, inference mode, etc. in "{path}" exist!', + "对应Midjourney模式的Token不能为空,请前往 {settingPath} 中配置": "Midjourney mode Token cannot be empty, please configure in {settingPath}", + "请检查推理模型是否存在!": "Please check if inference model exists!", + "当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!": "Current mode requires pre-analysis or setting of character/scene data, please analyze character/scene data first!", + "未找到对应的分镜预设的请求数据,请检查": "Could not find corresponding storyboard preset request data, please check", + "背景音乐文件夹或文件不存在,请检查": "Background music folder or file does not exist, please check", + "背景音乐文件夹下面未存在有效的音频文件": "No valid audio files exist in the background music folder", + "字幕时间信息不完整": "Subtitle timing information is incomplete", + "字幕内容信息不完整": "Subtitle content information is incomplete", + "初始视频消息成功!": "Initial video message successful!", + "获取成功 OptionKey: {key}": "Get successful OptionKey: {key}", + "获取失败 OptionKey: {key},失败原因:{error}": "Get failed OptionKey: {key}, reason: {error}", + "修改成功 OptionKey: {key}": "Modify successful OptionKey: {key}", + "修改失败 OptionKey: {key},失败原因:{error}": "Modify failed OptionKey: {key}, reason: {error}", + "请到 “{checkString}” 检查设置!": 'Please check settings in "{checkString}"!', + "当前值为空!": "Current value is empty!", + "未找到选项对象,请检查所有的选项设置是否存在!": "Option object not found, please check if all option settings exist!", + "设置数据不完整,请检查提示词类型,提示词预设数据是否完整": "Settings data is incomplete, please check if prompt type and prompt preset data are complete", + "没有找到需要处理的文案ID对应的数据,请检查数据是否正确": "Could not find data corresponding to the content ID that needs to be processed, please check if the data is correct", + "文案处理API设置不完整,请检查API地址,密钥和模型是否设置正确": 'Content processing API settings incomplete, please check if API address, key and model are configured correctly', + "文案生成成功": "Content generation successful", + "没有需要处理的文案ID": "No content ID needs to be processed", + "AI处理文案成功": "AI content processing successful", + "AI处理文案失败,{error}": "AI content processing failed, {error}", + "检测系统当前的语言已修改,继续执行会刷新当前页面并应用当前语言,是否继续?": 'System language has been modified, continuing will refresh the current page and apply the current language, continue?', + //#endregion + + //#region 预设 + "风格预设": "Style Presets", + '人物预设': "Character Presets", + '场景预设': "Scene Presets", + '新建预设': "New Preset", + '正在初始化预设库模块': "Initializing preset library module", + '释放添加图片': 'Release to add image', + '点击或拖拽添加图片': 'Click or drag to add image', + '预设名称': 'Preset Name', + '请输入预设名称': 'Please enter preset name', + '预设分类': 'Preset Category', + '请选择预设分类': 'Please select preset category', + '请先选择预设分类': 'Please select preset category first', + '请在上方选择预设分类以显示对应配置选项': 'Please select preset category above to display corresponding configuration options', + '没有选择任何文件': 'No file selected', + '输入别名或标签后按Enter添加': 'Enter alias or tag then press Enter to add', + '显示': 'Show', + '不显示': 'Hide', + '是否显示': 'Display Status', + '风格提示词': 'Style Prompt', + '人物提示词': 'Character Prompt', + '请输入人物垫图链接': 'Please enter character reference image link', + '请输入人物权重值': 'Please enter character weight value', + '请选择Lora模型': 'Please select Lora model', + 'Lora权重': 'Lora Weight', + '修改预设显示状态成功': 'Successfully modified preset display status', + '删除预设成功': 'Preset deleted successfully', + '选择分类': 'Select Category', + '选择标签': 'Select Tag', + '搜索预设数据成功': 'Preset data search successful', + '选择合适的风格可以影响最终生成图像的整体效果': 'Selecting appropriate style can affect the overall effect of the final generated image', + '已选风格': 'Selected Style', + '请输入风格垫图链接': 'Please enter style reference image link', + '请输入风格垫图权重': "Please enter style reference image weight", + '未找到指定的预设数据': "Specified preset data not found", + "不支持的合并类型": "Unsupported merge type", + "删除预设成功!": "Preset deleted successfully!", + "修改预设成功!": "Preset modified successfully!", + "修改预设失败,{error}": "Failed to modify preset, {error}", + "添加预设成功!": "Preset added successfully!", + "添加预设失败,{error}": "Failed to add preset, {error}", + "获取预设成功!": "Get preset successful!", + "获取预设失败,{error}": "Failed to get preset, {error}", + "图片格式不合法!只支持 png、jpg、webp 格式的图片!": "Invalid image format! Only png, jpg, webp formats are supported!", + "获取预设列表成功!": "Get preset list successful!", + "获取预设列表失败,{error}": "Failed to get preset list, {error}", + "加载预设列表...": "Loading preset list...", + "sw 值(0-1000)": "sw value (0-1000)", + 'MJ:风格垫图链接(sref)': 'MJ: Style Reference Link (sref)', + '批量设置选中的风格会覆盖原有的风格,覆盖结果不可恢复': 'Batch setting selected styles will overwrite existing styles, overwrite results cannot be recovered', + '3. 若风格仅应用于部分分镜,视为个性化设置,此处不会默认选中': '3. If style is only applied to some shots, it is considered personalized setting and will not be selected by default here', + '2. 初始选中项为所有分镜共同使用的风格': '2. Initially selected items are styles used by all shots', + '1. 此处仅显示预设库中已勾选“显示”的风格预设': '1. 此处仅显示预设库中已勾选“显示”的风格预设', + '搜索预设数据失败,{error}': 'Failed to search preset data, {error}', + '正在搜索预设数据...': 'Searching preset data...', + '搜索预设名称...': 'Search preset name...', + '确定要删除此预设吗?删除之后将无法恢复。请谨慎操作。': + 'Are you sure you want to delete this preset? It cannot be recovered after deletion. Please operate with caution.', + '修改预设显示状态失败,{error} ': 'Failed to modify preset display status, {error} ', + '获取Lora模型列表失败,请到 “{path}” 中加载远程Lora模型列表!': 'Failed to get Lora model list, please load remote Lora model list in "{path}"!', + '没有可用的Lora模型,请到 “{path}” 中加载远程Lora模型列表!': 'No available Lora models, please load remote Lora model list in "{path}"!', + 'SD:Lora选择': 'SD: Lora Selection', + 'cw 值(0-100)': 'cw value (0-100)', + 'MJ:人物图片链接(cref)': 'MJ: Character Image Link (cref)', + '别名/标签': 'Alias/Tag', + "新增预设成功,预设名称 '{name}'": "Added preset successfully, preset name '{name}'", + "修改预设成功,预设名称 '{name}'": "Modified preset successfully, preset name '{name}'", + '预设分类不能为空!': 'Preset category cannot be empty!', + '预设名称不能为空!': 'Preset name cannot be empty!', + "预设ID不能为空!": "Preset ID cannot be empty!", + '是否取消当前操作?继续执行会丢失当前操作的所有数据,请谨慎操作!': 'Cancel current operation? Continuing will lose all data from current operation, please operate with caution!', + '选择文件失败,{error}': 'Failed to select file, {error}', + "选择文件夹失败,{error}": "Failed to select folder, {error}", + "在相同类型下已存在当前的预设名字,请修改后再试!": "Preset name already exists under the same type, please modify and try again!", + //#endregion + + //#region 原创 + '场景推理': 'Scene Inference', + '手动添加': 'Manual Add', + '导入到自定义场景预设': 'Import to Custom Scene Preset', + '删除选中': 'Delete Selected', + '编辑数据': 'Edit Data', + '角色名称': 'Character Name', + '请输入角色名称': 'Please enter character name', + '角色提示词': 'Character Prompt', + '请输入角色提示词': 'Please enter character prompt', + '场景名称': 'Scene Name', + '请输入场景名称': 'Please enter scene name', + '场景提示词': 'Scene Prompt', + '请输入场景提示词': 'Please enter scene prompt', + '场景推理成功': 'Scene inference successful', + '角色推理成功': 'Character inference successful', + '修改成功': 'Modification successful', + '请先选择要删除的行': 'Please select the row to delete first', + '请先选择要导出的行': 'Please select the row to export first', + '导入所有选中成功的场景设置到预设中完成': 'Successfully imported all selected scene settings to presets', + '导入所有选中成功的人物角色到预设中完成': 'Successfully imported all selected character roles to presets', + '角色推理': 'Character Inference', + '导入到自定义角色预设': 'Import to Custom Character Preset', + '定位到对应行': 'Locate to Corresponding Row', + '已定位到对应分镜': 'Located to corresponding storyboard', + '提示词命令': 'Prompt Command', + '合成视频': 'Compose Video', + '无效的ID': 'Invalid ID', + '未找到指定ID的分镜数据': 'Storyboard data with specified ID not found', + '主图操作': 'Main Image Operations', + '上传图片': 'Upload Image', + '下载图片': 'Download Image', + '高清': 'HD', + '打开文件夹': 'Open Folder', + '打开文件夹成功': 'Folder opened successfully', + "打开文件/文件夹成功": 'File/folder opened successfully', + "打卡开文件/文件夹失败,{error}": 'Failed to open file/folder, {error}', + "没有选择的文件或文件夹": 'No file or folder selected', + "选择文件/文件夹成功": 'File/folder selected successfully', + "选择文件/文件夹失败,{error}": 'Failed to select file/folder, {error}', + "选择文件夹": "Select Folder", + "选择类型": "Select Type", + "请选择要选择的类型:": "Please select the type to choose:", + '子图操作': 'Sub-image Operations', + '上传图片成功': 'Image uploaded successfully', + '下载图片失败': 'Image download failed', + '合并生图提示词的排序顺序': 'Merge image generation prompt sorting order', + '修改排序顺序的按钮': 'Button to modify sorting order', + '提示词排序顺序格式不正确': 'Prompt sorting order format is incorrect', + '修改提示词排序顺序': 'Modify prompt sorting order', + '选择当前的prompt排序方式': 'Select current prompt sorting method', + '图片描述': 'Image Description', + '选择完成': 'Selection Complete', + '加载数据中': 'Loading Data', + '图片只能拖拽到主图片上': 'Images can only be dragged to main image', + '单句生图': 'Single Generation', + '生成当前分镜图片': 'Generate Current', + '下生图': 'Next Generation', + '锁定图片': 'Lock Image', + '清空图片': 'Clear Image', + '导入图片': 'Import Image', + '单句推理': 'Single Inference', + '下推理': 'Next Inference', + '可以导入网络图片或者是本地图片地址': 'You can import network images or local image addresses', + '本地图片地址无效或图片不存在': 'Local image address is invalid or image does not exist', + '导入图片成功': 'Image imported successfully', + '请生成或推理提示词': 'Please generate or infer prompts', + '推理当前分镜的提示词': 'Infer prompts for current storyboard', + '推理当前及后续所有分镜的提示词': 'Infer prompts for current and all subsequent storyboards', + '翻译提示词语言': 'Translate Prompt Language', + '命令预览': 'Command Preview', + '显示完整的提示词命令预览': 'Show complete prompt command preview', + '未知的翻译类型': 'Unknown translation type', + '没有需要翻译的数据': 'No data to translate', + '翻译完成': 'Translation completed', + '没有返回的分镜ID和数据信息': 'No returned storyboard ID and data information', + '没有找到对应的分镜ID的数据': 'No data found for corresponding storyboard ID', + '完整命令预览': 'Complete Command Preview', + '推理完成': 'Inference completed', + '批量风格': 'Batch Style', + '出图': 'Generate Image', + '暂不支持的出图方式': 'Unsupported image generation method', + '一键锁定': 'One-click Lock', + '一键锁定图片提示': 'One-click image lock prompt', + '一键解锁图片提示': 'One-click image unlock prompt', + '一键解锁': 'One-click Unlock', + '一键锁定图片成功': 'One-click image lock successful', + '一键解锁图片成功': 'One-click image unlock successful', + 'MJ采集全部图片': 'MJ Collect All Images', + '一拆四': 'Split One to Four', + '应用当前': 'Apply Current', + '请选择需要添加的标签': 'Please select tags to add', + '传入的数据中没有找到对应的数据': 'No corresponding data found in the input data', + 'tag传入的数据格式不正确': 'Tag input data format is incorrect', + '界面显示修改完成': 'Interface display modification completed', + '选择文件': 'Select File', + '背景音乐': 'Background Music', + '剪映草稿模板': 'JianYing Draft Template', + '选择剪映草稿模板': 'Select JianYing Draft Template', + '保存配置': 'Save Configuration', + '生成剪映草稿': 'Generate JianYing Draft', + '草稿数据保存成功': 'Draft data saved successfully', + '请选择音频文件': 'Please select audio file', + '音频文件': 'Audio File', + 'SRT文件': 'SRT File', + '出图文件夹': 'Image Output Folder', + '反推提示词': 'Reverse Prompt', + '未设置': 'Not Set', + '文件路径未设置': 'File path not set', + '未知的定位类型': 'Unknown location type', + '字幕分组': 'Subtitle Grouping', + '角色场景': 'Character Scene', + '一键推理': 'One-click Inference', + '一键出图': 'One-click Image Generation', + '图片预览': 'Image Preview', + '一键高清': 'One-click HD', + '导出剪映': 'Export to JianYing', + '初始化分镜': 'Initialize Storyboard', + '手动分组': 'Manual Grouping', + '文案分组': 'Content Grouping', + '角色分析': 'Character Analysis', + '场景分析': 'Scene Analysis', + '推理所有提示词': 'Infer All Prompts', + '推理空白分镜提示词': 'Infer Blank Storyboard Prompts', + '生成所有图片': 'Generate All Images', + '生成未生成图片分镜': 'Generate Un-generated Image Storyboards', + '生成失败图片分镜': 'Generate Failed Image Storyboards', + '停止当前批次生图任务': 'Stop Current Batch Image Generation Task', + '停止所有批次生图任务': 'Stop All Batch Image Generation Tasks', + '小说分镜初始化成功': 'Novel storyboard initialization successful', + 'AI智能分组': 'AI Smart Grouping', + '未知字幕分组操作': 'Unknown subtitle grouping operation', + '未知角色场景操作': 'Unknown character scene operation', + '未知一键推理操作': 'Unknown one-click inference operation', + '未知一键出图操作': 'Unknown one-click image generation operation', + '全部图片高清处理完成': 'All images HD processing completed', + '空行清理完成': 'Empty lines cleaned up', + '未找到上一行数据': 'Previous row data not found', + "未找到指定的字幕项": 'Specified subtitle item not found', + "未找到当前行数据": 'Current row data not found', + "已自动创建新行": 'New row automatically created', + "添加分镜字幕": 'Add Storyboard Subtitle', + "文案信息已成功保存": 'Content information saved successfully', + "没有数据可以处理": 'No data to process', + "智能批量合并": 'Smart Batch Merge', + "智能合并部分完成": 'Smart merge partially completed', + "处理分镜文案": 'Process Storyboard Content', + "保存文案信息": 'Save Content Information', + "下合并": 'Merge Down', + "初始化分镜数据": 'Initialize Storyboard Data', + "短文本": 'Short Text', + "长文本": 'Long Text', + "分镜文案": 'Storyboard Content', + "AI文案分镜": 'AI Content Storyboard', + "模型名称": 'Model Name', + "字幕": 'Subtitle', + "批量合并": 'Batch Merge', + "向上": 'Up', + "向下": 'Down', + "时间范围": 'Time Range', + "时间轴检查": 'Timeline Check', + "请输入文案内容": 'Please enter content text', + "请输入有效的文案内容": 'Please enter valid content text', + "字幕列表": 'Subtitle List', + "显示区": 'Display Area', + "时间": 'Time', + "向上合并": 'Merge Up', + "向上拆分": 'Split Up', + "向下合并": 'Merge Down', + "向下拆分": 'Split Down', + "导入分镜文案": 'Import Storyboard Content', + "加载数据成功": 'Data loaded successfully', + "请选择图片": 'Please select image', + "编辑小说": 'Edit Novel', + "添加新小说": 'Add New Novel', + "小说名称": 'Novel Name', + "小说类型": 'Novel Type', + "小说批次任务名称": "Novel Batch Task Name", + "小说分镜名称": "Novel Storyboard Name", + "任务类型": "Task Type", + "配音地址": 'Voice Address', + "项目文件夹将自动生成": 'Project folder will be automatically generated', + "图片输出文件夹": 'Image Output Folder', + "图片输出文件夹将自动生成": 'Image output folder will be automatically generated', + "SD反推": 'SD Reverse', + "MJ反推": 'MJ Reverse', + "选择配音文件": 'Select Voice File', + "新增批次数": 'New Batch Count', + "选择风格": 'Select Style', + "风格目前需要到每个批次中单独设置": 'Style currently needs to be set individually in each batch', + "选择旧批次": 'Select Old Batch', + "选择要复制数据的已存在的批次": 'Select existing batch to copy data from', + "使用旧批次数据": 'Use Old Batch Data', + "复制的数据": 'Copied Data', + "文案": 'Content', + "生图风格": 'Image Generation Style', + "选择角色": 'Select Character', + "生图提示词": 'Image Generation Prompt', + "添加成功": 'Added successfully', + "进入小说批次信息": 'Enter Novel Batch Information', + "删除子项目": 'Delete Sub-project', + "批次出图方式": 'Batch Image Generation Method', + "批次号": 'Batch Number', + "未命名": 'Unnamed', + "删除小说任务并刷新数据成功": 'Novel task deleted and data refreshed successfully', + "还未选择项目": 'No project selected yet', + "选择项目": 'Select Project', + "请选择项目": 'Please select project', + "创建新项目": 'Create New Project', + "修改小说任务": 'Modify Novel Task', + "任务编号": 'Task Number', + "重置名称": 'Reset Name', + "开启视频生成": 'Enable Video Generation', + "保存修改": 'Save Changes', + "创建账号": "Create Account", + "打开小说文件夹": 'Open Novel Folder', + "打开图片文件夹": 'Open Image Folder', + "查看详情": 'View Details', + "查看项目的详细信息和配置": 'View project details and configuration', + "重置小说": 'Reset Novel', + "删除小说": 'Delete Novel', + "重置小说数据成功": 'Novel data reset successfully', + "删除小说数据成功": 'Novel data deleted successfully', + "小说管理": 'Novel Management', + "新建小说": 'Create New Novel', + "搜索指定的小说名称": 'Search specific novel name', + "重置筛选条件": 'Reset Filter Conditions', + "加载设置失败": 'Failed to load settings', + "修改任务信息成功": 'Task information modified successfully', + "重置任务数据成功": 'Task data reset successfully', + "确认删除": 'Confirm Delete', + "任务已删除": 'Task deleted', + "新增小说批次": 'Add Novel Batch', + "新增小说批次成功": 'Novel batch added successfully', + "点击添加一个新的小说批次任务": 'Click to add a new novel batch task', + "暂无小说批次任务": 'No novel batch tasks available', + "创建第一个小说批次任务": 'Create first novel batch task', + "查看小说数据": 'View Novel Data', + "查看小说批次任务信息": 'View novel batch task information', + "生成视频文件夹": 'Generate Video Folder', + "依赖草稿": 'Dependent Draft', + "前缀提示词": 'Prefix Prompt', + "后缀提示词": 'Suffix Prompt', + "自动解析角色": 'Auto Parse Character', + "自动解析的角色信息": 'Auto-parsed character information', + "删除本地账号成功": "Local account deleted successfully", + "删除服务器账号请求失败": "Failed to delete server account request", + "服务器删除账号失败": "Server account deletion failed", + "删除服务器账号成功": "Server account deleted successfully", + "深色模式": "Dark Mode", + "打开任务": "Open Task", + "查看任务信息": "View Task Information", + "编辑任务": "Edit Task", + "导出剪映草稿": "Export JianYing Draft", + "重置任务": "Reset Task", + "删除任务": "Delete Task", + "查看当前批次任务的所有的数据信息": "View all data information of current batch task", + "正在初始化原创模块": "Initializing original content module", + "加载小说信息中...": "Loading novel information...", + "加载小说信息失败,{error}": "Failed to load novel information, {error}", + "预设-{name}": "Preset-{name}", + '加载中...': 'Loading...', + "打开当前任务,进入图转视频的信息操作界面": "Open current task and enter image-to-video information operation interface", + "将当前批次任务数据和分镜数据全部删除,删除后不可恢复,请谨慎操作": "Delete all current batch task data and storyboard data, this action cannot be undone, please operate with caution", + '将项目数据恢复至新建状态,所有的分镜数据都会被删除,包含提示词、图片等信息': 'Restore project data to newly created state, all storyboard data will be deleted, including prompts, images and other information', + "将当前批次任务导出到剪映草稿,包含背景音乐、字幕、图片、关键帧等信息": "Export current batch task to JianYing draft, including background music, subtitles, images, keyframes and other information", + "编辑当前批次任务,可以修改批次名称、SRT地址、配音地址等信息": "Edit current batch task, can modify batch name, SRT address, voice address and other information", + "打开当前任务,进入分镜操作的详细界面": "Open current task and enter detailed storyboard operation interface", + "删除服务器账号失败,{error}": "Failed to delete server account, {error}", + "确定要删除服务器ID为 “{guildId}” 的服务器账号吗?此操作会从服务器删除账号,并同时删除本地记录,操作不可撤销!": 'Are you sure you want to delete the server account with server ID "{guildId}"? This operation will delete the account from the server and simultaneously delete local records, and cannot be undone!', + "无法删除服务器账号,账号信息无效或缺少服务器账号ID": 'Cannot delete server account, account information is invalid or missing server account ID', + '打开 文件/文件夹 失败,{error}': 'Failed to open file/folder, {error}', + '打开 文件/文件夹 成功': 'File/folder opened successfully', + '文件/文件夹 路径为空': 'File/folder path is empty', + '子项目列表 - {bookTaskName}': 'Sub-project List - {bookTaskName}', + '删除任务失败,{error}': 'Failed to delete task, {error}', + '正在删除任务...': 'Deleting task...', + '确定要删除任务 {bookTaskName} 吗?删除会将当前批次任务数据和分镜数据全部删除,删除后不可恢复,请确认是否继续?': 'Are you sure you want to delete task {bookTaskName}? Deletion will completely remove all current batch task data and storyboard data, and cannot be recovered. Please confirm whether to continue?', + '重置任务数据失败,{error}': 'Failed to reset task data, {error}', + '正在重置任务数据...': 'Resetting task data...', + '是否重置任务 {bookTaskMame} 的数据?重置后数据将恢复至新建状态,所有的分镜数据(包括提示词、图片、分镜分组等)都会被删除,请确认是否继续!': 'Do you want to reset the data of task {bookTaskMame}? After reset, the data will return to the newly created state, and all storyboard data (including prompts, images, storyboard grouping, etc.) will be deleted. Please confirm whether to continue!', + '未选择任务,请返回重新选择': 'No task selected, please go back and select again', + '正在初始化图文转视频模块,{bookName}_{bookTaskName}': 'Initializing image-to-video module, {bookName}_{bookTaskName}', + '打开任务失败,{error}': 'Failed to open task, {error}', + '初始化小说分镜模块失败,{error}': 'Failed to initialize novel storyboard module, {error}', + '正在初始化小说分镜模块,{bookName}_{bookTaskName}': 'Initializing novel storyboard module, {bookName}_{bookTaskName}', + '分镜出图的进度:{progress}% ': 'Storyboard generation progress: {progress}% ', + '正在删除小说数据...': 'Deleting novel data...', + '是否删除该小说,删除后数据将无法恢复,请确认是否继续!': 'Are you sure you want to delete this novel? Data cannot be recovered after deletion, please confirm whether to continue!', + '正在重置小说数据。。。': 'Resetting novel data...', + '是否重置小说数据,重置后数据将恢复至新建状态,所有的批次数据和文件数据都会被删除,请确认是否继续!': 'Do you want to reset novel data? After reset, data will return to newly created state, all batch data and file data will be deleted. Please confirm whether to continue!', + "查看小说,名称 '{name}'": "View novel, name '{name}'", + '永久删除后项目数据,所有的批次数据和文件数据都会被删除': 'Permanently delete project data, all batch data and file data will be deleted', + '将项目数据恢复至新建状态,所有的批次数据和文件数据都会被删除': 'Restore project data to newly created state, all batch data and file data will be deleted', + '修改小说名称、配音地址和SRT地址等基本信息': 'Modify basic information such as novel name, voiceover address, and SRT address', + ' {count} 个子项目': ' {count} sub-projects', + '修改失败,{error}': 'Modification failed, {error}', + '修改任务信息成功,三秒后自动关闭当前界面!': 'Task information modified successfully, current interface will close automatically in 3 seconds!', + '请选择背景音乐文件/文件夹': 'Please select background music file/folder', + '选择一个项目来查看子项目,或者创建一个新项目': 'Select a project to view sub-projects, or create a new project', + '确定要删除小说任务吗?该操作不可逆,会将对应的子任务数据和分镜数据全部删除,请谨慎操作!': 'Are you sure you want to delete the novel task? This operation is irreversible and will delete all corresponding sub-task data and storyboard data. Please operate with caution!', + '正在加载小说批次信息...': 'Loading novel batch information...', + '创建时间 {time}': 'Created at {time}', + '反推/GPT提示词': 'Reverse/GPT Prompt', + '添加小说失败,{error}': 'Failed to add novel, {error}', + '添加/修改小说信息成功,三秒后自动关闭当前界面!': 'Novel information added/modified successfully, interface will close automatically in 3 seconds!', + '未找到对应的图片数据,请检查': 'Corresponding image data not found, please check', + '未找到对应的批次数据,请检查': 'Corresponding batch data not found, please check', + '未找到对应的图片ID,请检查': 'Corresponding image ID not found, please check', + '未找到对应的批次数据名称,请检查': 'Corresponding batch data name not found, please check', + '未找到对应的操作,请检查': 'Corresponding operation not found, please check', + '未找到对应的被删除的图片数据,请检查!!': 'Corresponding deleted image data not found, please check!!', + '点击会自动将当前行的 “分镜文案” 与 “字幕” 进行智能合并,合并之后会自动对齐时间轴,可能会失败,失败需要自行定位到自定的行,把失败行对齐之后,再下一行点击 “下合并”,后续就会自动合并': 'Clicking will automatically intelligently merge the "Storyboard Text" and "Subtitle" of the current row. After merging, the timeline will be automatically aligned. This may fail. If it fails, you need to manually locate the specific row, align the failed row, then click "Merge Down" on the next row, and subsequent rows will be automatically merged', + '点击保存后,会再之前的分镜数据上进行修改,多余的分镜会自动创建,超出的分镜会自动删除,可能会导致数据丢失或者是提示词和出图数据对不上,是否继续?': 'After clicking save, modifications will be made to the previous storyboard data. Extra storyboards will be automatically created, and excess storyboards will be automatically deleted. This may cause data loss or misalignment between prompts and image generation data. Continue?', + '点击取消后,当前操作将被放弃,所有的修改数据不会记录,是否继续?': 'fter clicking cancel, the current operation will be abandoned and all modified data will not be recorded. Continue?', + '当前行是最后一行,无法向下合并': 'Current row is the last row, cannot merge down', + '向下拆分失败,{error}': 'Failed to split down, {error}', + '当前行只存在一行字幕,无法向下拆分': 'Current row only has one subtitle, cannot split down', + '向上拆分失败,{error}': 'Failed to split up, {error}', + '当前行只存在一行字幕,无法向上拆分': 'Current row only has one subtitle, cannot split up', + '该字幕已存在于目标行中,无法重复合并': 'This subtitle already exists in the target row, cannot merge repeatedly', + '当前行是第一行,无法向上合并': 'Current row is the first row, cannot merge up', + '请在此输入内容...': 'Please enter content here...', + '共 {wordCount} 行分镜,共 {count} 个字,自动删除空行': '{wordCount} storyboard lines, {count} characters in total, empty lines automatically deleted', + '请输入分镜文案,每行一个分镜...': 'Please enter storyboard content, one storyboard per line...', + '请输入分镜好的文案,软件只会检查输入文案的空行,不会对文案进行修改,请修改处理后写入!': 'Please enter well-storied content. The software will only check for empty lines in the input content and will not modify the content. Please make modifications before writing!', + '设置AI模型名称失败,{error}': 'Failed to set AI model name, {error}', + '正在设置AI模型名称为:{modelName}': 'Setting AI model name to: {modelName}', + '从当前行开始,合并下面所有的分镜与字幕': 'Starting from current row, merge all storyboards and subtitles below', + '点击会自动将当前行的分镜文案与字幕进行智能合并,合并之后会自动对齐时间轴,可能会失败,失败需要自行定位到指定的行,把失败行对齐之后,再下一行点击下合并,后续就会自动合并': 'Clicking will automatically intelligently merge the storyboard text and subtitle of the current row. After merging, the timeline will be automatically aligned. This may fail. If it fails, you need to manually locate to the specified row, align the failed row, then click Merge Down on the next row, and subsequent rows will be automatically merged', + '请输入分镜文案...': 'Please enter storyboard content...', + '请输入调用分组的模型名称,不填写调用的是 “{path}” 中设置的模型': 'Please enter the model name for grouping. If not filled, the model set in "{path}" will be used', + 'AI文案分镜-{typeText} 处理完成': 'AI Content Storyboard-{typeText} processing completed', + 'AI文案分镜-{typeText} 处理失败,失败原因: {error}': 'AI Content Storyboard-{typeText} processing failed, reason: {error}', + '正在调用AI文案分镜-{typeText}处理...': 'Calling AI Content Storyboard-{typeText} processing...', + '正在调用AI进行分镜合并,请稍后...': 'Calling AI for storyboard merging, please wait...', + '根据字幕列,将数据拆分为单句,重置分镜分组数据': 'Split data into single sentences based on subtitle list, reset storyboard grouping data', + 'AI文案分镜-长文本': 'AI Content Storyboard-Long Text', + '将调用AI对文案进行分析,合并成35字以上的长文本分镜。合并的长度由AI决定,由AI处理,可能会与原文案存在差异,导致字幕和分镜不能完全对上,需要手动调整对齐': 'Will call AI to analyze the content and merge it into long text storyboards with 35+ characters. The merge length is determined by AI processing, which may differ from the original content, causing subtitles and storyboards to not align perfectly, requiring manual adjustment', + 'AI文案分镜-短文本': 'AI Content Storyboard-Short Text', + '将调用AI对文案进行分析,合并成15-35字左右的短文本分镜,由AI处理,可能会与原文案存在差异,导致字幕和分镜不能完全对上,需要手动调整对齐': 'Will call AI to analyze the content and merge it into short text storyboards of approximately 15-35 characters. Processed by AI, may differ from the original content, causing subtitles and storyboards to not align perfectly, requiring manual adjustment', + '将分镜数据上进行修改,多余的分镜会自动创建,超出的分镜会自动删除': 'Modifications will be made to storyboard data. Extra storyboards will be automatically created, excess storyboards will be automatically deleted', + '点击该按钮,会弹出导入分镜文案的对话框,可以输入已经分好镜的文案,方便我们的字幕根据分镜文案进行分组对齐,导入之后点击 “批量合并” 按钮,进行自动对齐': 'Clicking this button will open a dialog for importing storyboard content. You can input pre-segmented content to help align subtitles according to storyboard content. After importing, click the "Batch Merge" button for automatic alignment', + '时间轴检查和改写完成,请手动保存!!!': 'Timeline check and rewriting completed, please save manually!!!', + '第 {index} 行字幕结束时间为空,请检查': 'Subtitle end time is empty for line {index}, please check', + '第 {indx}行字幕为空,请检查': 'Subtitle is empty for line {indx}, please check', + '确定要检查时间轴吗?该操作会更具对应的字幕进行时间的强制对齐,是不是继续操作?': 'Are you sure you want to check the timeline? This operation will force time alignment based on corresponding subtitles. Continue?', + '智能合并失败,{error}': 'Smart merge failed, {error}', + '智能批量合并完成,所有字幕都已正确匹配': 'Smart batch merge completed, all subtitles matched correctly', + '智能合并完成,{count} 个剩余字幕已写入后续行': 'Smart merge completed, {count} remaining subtitles written to subsequent lines', + '处理过程中遇到错误,已停止智能匹配:\n\n{message}\n\n已处理字幕:${totalProcessed} 个\n剩余字幕:${totalRemaining} 个\n\n剩余字幕已按顺序写入后续行中,请手动调整文案匹配。': 'Error encountered during processing, smart matching stopped:\n\n{message}\n\nProcessed subtitles: ${totalProcessed}\nRemaining subtitles: ${totalRemaining}\n\nRemaining subtitles have been written to subsequent lines in order, please manually adjust content matching.', + '第 {rowIndex} 行没有找到任何匹配的字幕': 'No matching subtitles found for line {rowIndex}', + '第 {rowIndex} 行在处理字幕 “{subtitleText}” 时无法匹配,已累积文本 “{accumulatedText}” 与目标文案 “{text}”不匹配': 'Unable to match subtitle "{subtitleText}" on line {rowIndex}, accumulated text "{accumulatedText}" does not match target content "{text}"', + '第 {rowIndex} 行的第一个字幕 “{subtitleText}” 与文案 “{text}” 不匹配': 'The first subtitle "{subtitleText}" on line {rowIndex} does not match the content "{text}"', + '第 {index} 行字幕为空,无法继续处理': 'Subtitle on line {index} is empty, cannot continue processing', + '确定要进行智能批量合并吗?将从第 {startIndex} 行开始处理。此操作会根据分镜文案内容自动将字幕合并到对应的行中。': 'Are you sure you want to perform smart batch merge? Processing will start from line {startIndex}. This operation will automatically merge subtitles into corresponding lines based on storyboard content.', + '起始索引 {startIndex} 无效,有效范围是 0 到 {endIndex}': 'Start index {startIndex} is invalid, valid range is 0 to {endIndex}', + '正在保存文案信息...': 'Saving content information...', + '点击保存后,会在之前的分镜数据上进行修改,多余的分镜会自动创建,超出的分镜会自动删除,可能会导致数据丢失或者是提示词和出图数据对不上,是否继续?': 'After clicking save, modifications will be made to the previous storyboard data. Extra storyboards will be automatically created, excess storyboards will be automatically deleted. This may cause data loss or misalignment between prompts and image generation data. Continue?', + "保存数据失败,数据不完整,请检查'文案'、'时间'、'字幕' 等,数据是否完整": "Save data failed, data incomplete. Please check if 'Content', 'Time', 'Subtitle' and other data are complete", + '向下合并失败,{error}': 'Failed to merge down, {error}', + '字幕向下合并成功,移动了 {count} 个字幕项': 'Subtitle merge down successful, moved {count} subtitle items', + '向上合并失败,{error}': 'Failed to merge up, {error}', + '字幕向上合并成功,移动了 {itemIndex} 个字幕项': 'Subtitle merge up successful, moved {itemIndex} subtitle items', + '当前是第一行,不能向上合并': 'Current is the first line, cannot merge up', + '当前行索引:{rowIndex}, 当前字幕项索引:{itemIndex}': 'Current row index: {rowIndex}, current subtitle item index: {itemIndex}', + '操作进行中,请稍后...': 'Operation in progress, please wait...', + '导出剪映草稿失败:{error}': 'Failed to export JianYing draft: {error}', + '高清失败:{error}': 'HD processing failed: {error}', + '正在高清图片... {current} / {total}': 'Processing HD images... {current} / {total}', + '所有分镜的图片会被替换为高清图片,是否继续?\n当前高清倍数:{hdScale} 倍': 'All storyboard images will be replaced with HD images, continue?\nCurrent HD scale: {hdScale}x', + '分镜的图片全部存在,开始进行高清处理': 'All storyboard images exist, starting HD processing', + '正在检查当前分镜的图片是否存在...': 'Checking if current storyboard images exist...', + '执行操作会将所有批次正在等待中的出图任务停止,是否继续?': 'This operation will stop all pending image generation tasks in all batches, continue?', + '执行操作会将当前批次正在等待中的出图任务停止,是否继续?': 'This operation will stop all pending image generation tasks in current batch, continue?', + '执行操作会将生图失败的分镜的出图任务添加到队列中,MJ生图错误任务不可重新提交,会直接出现错误提示,是否继续?': 'This operation will add image generation tasks for storyboards that failed image generation to the queue. MJ image generation error tasks cannot be resubmitted and will directly show error prompts. Continue?', + '执行操作会将未出图的分镜的出图任务添加到队列中,除了生图失败的分镜外,是否继续?': 'This operation will add image generation tasks for storyboards that have not generated images to the queue, except for those that failed image generation. Continue?', + '执行操作会添加所有的分镜任务到后台任务队列中,除了被锁定的分镜外,其余的分镜的出图任务都会被添加到后台任务队列中,可能会导致部分图片数据重置,是否继续?': 'This operation will add all storyboard tasks to the background task queue, except for locked storyboards. Image generation tasks for other storyboards will be added to the background task queue, which may cause some image data to be reset. Continue?', + 'MJ生图,错误的任务不可重新提交,请单句提交!': 'MJ image generation: error tasks cannot be resubmitted, please submit individually!', + '会将当前批次的不存在提示词的分镜进行推理,不会影响已有提示词的分镜,是否继续?': 'Will perform inference on storyboards without prompts in current batch, will not affect storyboards with existing prompts, continue?', + '会将当前批次的所有的提示词进行推理,并且当前已有的提示词会被覆盖,是否继续?': 'Will perform inference on all prompts in current batch, existing prompts will be overwritten, continue?', + '打开AI智能分组失败: {error}': 'Failed to open AI smart grouping: {error}', + '正在准备AI智能分组组件,请稍等...': 'Preparing AI smart grouping component, please wait...', + '打开文案分组失败: {error}': 'Failed to open content grouping: {error}', + '正在准备文案分组组件,请稍等...': 'Preparing content grouping component, please wait...', + '打开手动分组失败: {error}': 'Failed to open manual grouping: {error}', + '正在准备手动分组组件,请稍等...': 'Preparing manual grouping component, please wait...', + '初始化小说分镜失败,{error}': 'Failed to initialize novel storyboard, {error}', + '正在开始初始化小说分镜,请稍等。。。': 'Starting to initialize novel storyboard, please wait...', + '该操作会将当前的分镜数据全部删除,然后根据当前的批次任务的 SRT文件,重新初始化分镜(生成的分镜是未分组的,需要重新进行分镜的分组)': 'This operation will delete all current storyboard data and reinitialize the storyboard based on the SRT files of the current batch tasks (the generated storyboards are ungrouped and need to be regrouped)', + '使用 AI 大模型对分镜进行分组,由AI对文案进行分析,自动生成分组建议': 'Use AI large models to group storyboards, with AI analyzing the content and automatically generating grouping suggestions', + '通过以分镜好的文案进行分镜的分组,导入的SRT文件中的字幕会和分镜的文案进行匹配,可能会出现文案和SRT不能完全对齐,需要手动进行处理': 'Group storyboards based on storyboarded content. The subtitles in the imported SRT files will be matched with the storyboard content, which may result in misalignment requiring manual processing', + '手动将所有当前手动的字幕进行分组,可以向上/向下合并,向上/向下拆分': 'Manually group all current subtitles with options to merge up/down or split up/down', + '将当前的分镜的数据全部删除,然后根据当前的批次任务的 SRT文件,重新生成分镜(生成的分镜是单个,需要重新进行分镜的分组)': 'Delete all current storyboard data and regenerate storyboards based on the SRT files of the current batch tasks (generated storyboards are individual and need to be regrouped)', + '将当前批次的所有的提示词进行推理,并且当前已有的提示词会被覆盖,是否继续?': 'This will process all prompts in the current batch and overwrite existing prompts. Do you want to continue?', + '全部已完成,不必跳转': 'All completed, no need to navigate', + '打开文件夹失败,{error}': 'Failed to open folder, {error}', + '剪映草稿 {draftName} 生成成功': 'JianYing draft {draftName} generated successfully', + '正在生成剪映草稿。。。': 'Generating JianYing draft...', + '表单验证失败,{error}': 'Form validation failed, {error}', + '未传入任务或小说信息,无法加载数据': 'No task or novel information provided, unable to load data', + '在生成草稿前要先保存数据,选择的音频文件要和当前的字幕文件保持一致': 'Data must be saved before generating the draft, and the selected audio file must match the current subtitle file', + '注意:选择的草稿主轨道的图片数量要和当前的相同,会生成新的草稿并且只会替换图片,保留其余的数据,依赖的草稿必须是剪映 5.9 级以下版本,不支持 6.0 及以上版本': 'Note: The number of images in the main track of the selected draft must match the current one. A new draft will be generated and only images will be replaced while preserving other data. The dependent draft must be JianYing version 5.9 or below, versions 6.0 and above are not supported', + '选择文件/文件夹': 'Select File/Folder', + '请选择背景音乐文件夹或音频文件...': 'Please select background music folder or audio file...', + '请选择配音文件...': 'Please select voiceover file...', + '配音文件 (MP3/WAV)': 'Voiceover File (MP3/WAV)', + '获取数据失败,失败原因:没有找到对应的数据': 'Failed to get data, reason: No corresponding data found', + '是否保存当前的提示词的排序顺序?只会修改当前分镜的提示词的顺序!': 'Save the current prompt sorting order? This will only modify the prompt order for the current storyboard!', + '获取数据失败,{error}': 'Failed to get data, {error}', + '修改提示词排序顺序失败,{error}': 'Failed to modify prompt sorting order, {error}', + '正在保存提示词的排序顺序,请稍等...': 'Saving prompt sorting order, please wait...', + '是否保存当前的提示词的排序顺序?会修改当前任务下的所有提示词的顺序!': 'Save the current prompt sorting order? This will modify the order of all prompts under the current task!', + '当前的标签数量不够,需要添加 {count} 个标签': 'Current tag count is insufficient, need to add {count} tags', + '下载结果 (成功: {successCount}/{totalCount})': 'Download Result (Success: {successCount}/{totalCount})', + '是否一拆四(一拆四是个泛指,根据出的子图数量最少的),会自动生成多个批次任务并设置对应的图片!': 'Split into four (general term, based on minimum sub-image count)? This will automatically generate multiple batch tasks and set corresponding images!', + '正在采集图片,请稍后...': 'Collecting images, please wait...', + '即将开始采集所有的MJ生图图片,满足一下条件的分镜才会被采集:有生图信息,有消息ID,没有生成图片,图片的有效期为24小时,是否继续?': 'About to start collecting all MJ generated images. Only storyboards meeting the following conditions will be collected: have image generation info, have message ID, no generated images. Images are valid for 24 hours. Continue?', + '没有需要锁定/解锁的分镜,请先出图!': 'No storyboards need locking/unlocking, please generate images first!', + '即将开始锁定当前批次的所有图片,只有有输出主图的分镜才会被锁定,是否继续?': 'About to start locking all images in the current batch. Only storyboards with output main images will be locked. Continue?', + '即将开始解锁当前批次的所有图片,只有被锁定的分镜才会被解锁,是否继续?': 'About to start unlocking all images in the current batch. Only locked storyboards will be unlocked. Continue?', + '正在批量设置风格...': 'Setting styles in batch...', + '批量设置风格失败,{error}': 'Failed to set styles in batch, {error}', + '选择对应的风格,应用所有的分镜': 'Select corresponding style to apply to all storyboards', + '人物/场景/风格': 'Character/Scene/Style', + '即将开始执行推理提示词任务,请稍等。。。': 'About to start executing prompt inference task, please wait...', + '命令预览生成错误:{error}': 'Command preview generation error: {error}', + '正在生成命令预览...': 'Generating command preview...', + '中文 2 英文': 'Chinese to English', + '英文 2 中文': 'English to Chinese', + '下翻译 英文 2 中文': 'Next Translate English to Chinese', + '下翻译 中文 2 英文': 'Next Translate Chinese to English', + '清空 {name} 图片成功': 'Successfully cleared {name} images', + '是否清空当前分镜的图片?清空之后数据将无法恢复,请谨慎操作!': 'Clear images for current storyboard? Data cannot be recovered after clearing, please proceed with caution!', + '当前分镜图片已被锁定,无法清空图片': 'Current storyboard images are locked, cannot clear images', + '未找到可生成图片的分镜,或者分镜都被锁定!': 'No storyboards found for image generation, or all storyboards are locked!', + '添加出图任务到任务队列中成功!': 'Successfully added image generation task to queue!', + '当前分镜图片已被锁定,无法生成图片': 'Current storyboard images are locked, cannot generate images', + '清除当前分镜的图片,包括子图和主图': 'Clear current storyboard images, including sub-images and main image', + '锁定/解锁当前图片,防止被修改': 'Lock/unlock current image to prevent modification', + '为当前分镜及后续分镜生成图片,会跳过已锁定的分镜,但是不会跳过已生成图片的分镜': 'Generate images for current and subsequent storyboards, will skip locked storyboards but not skip storyboards with existing images', + '图片锁定/解锁成功': 'Image lock/unlock successful', + '没有生成图片,不能锁定': 'No generated images, cannot lock', + '图片文件不存在或已被删除,无法锁定': 'Image file does not exist or has been deleted, cannot lock', + '检查图片文件出错: {error}': 'Error checking image file: {error}', + '选择缓存区文件__主图__{bookTaksName}__{bookTaskDetailName}': 'Select cache file__Main Image__{bookTaksName}__{bookTaskDetailName}', + 'MJ任务ID:{id}': 'MJ Task ID: {id}', + '修改风格标签失败,{error}': 'Failed to modify style tag, {error}', + '修改风格标签成功!': 'Style tag modified successfully!', + '修改场景标签失败,{error}': 'Failed to modify scene tag, {error}', + '修改场景标签成功!': 'Scene tag modified successfully!', + '修改角色标签成功!': 'Character tag modified successfully!', + '修改角色标签失败,{error}': 'Failed to modify character tag, {error}', + '获取标签数据失败,{error}': 'Failed to get tag data, {error}', + '下载图片失败,{error}': 'Failed to download image, {error}', + '正在下载图片,请稍后...': 'Downloading image, please wait...', + '没有MJ生图信息,不能下载': 'No MJ image generation info, cannot download', + '没有消息ID,不能采集图片': 'No message ID, cannot collect image', + '当前图片不是MJ图片,不能下载': 'Current image is not an MJ image, cannot download', + '单个图片的下载,只支持 MJ API 生图,当前有图片的话会被覆盖掉,是否继续?': 'Single image download only supports MJ API generation. Existing images will be overwritten. Continue?', + '正在生成高清图片...': 'Generating HD image...', + '将当前分镜的主图高清,高清倍数:{hdScale} 倍,是否继续?': 'Enhance current storyboard main image to HD, enhancement scale: {hdScale}x. Continue?', + '注意:上传的图片(png)到主图,并且会覆盖原有的图片,不可恢复,上传到图片只能是裁剪过的,该操作不会自动裁剪上传,是否继续?': 'Note: Upload image (PNG) to main image, this will overwrite existing images irreversibly. Uploaded images must be pre-cropped as this operation will not auto-crop. Continue?', + '翻译中 ... {current} / {total}': 'Translating ... {current} / {total}', + '正在推理提示词,推理进度 {current} / {total}': 'Inferring prompts, progress {current} / {total}', + '字幕/文案': 'Subtitle/Content', + '无法定位:找不到对应的ID': 'Cannot locate: ID not found', + '分镜_{name}': 'Storyboard_{name}', + '图片 {index}': 'Image {index}', + '删除失败,未找到对应的行': 'Delete failed, corresponding row not found', + '场景推理失败:{error}': 'Scene inference failed: {error}', + '角色推理失败:{error}': 'Character inference failed: {error}', + '删除 {name} 成功': 'Successfully deleted {name}', + '导入 {name} 到场景预设成功': 'Successfully imported {name} to scene presets', + '导入 {name} 到角色预设成功': 'Successfully imported {name} to character presets', + '1. {type}的数据是由当前的小说文案进行分析的结果': '1. {type} data is the result of analyzing the current novel content', + '2. {type}的数据只会在部分推理模式中生效,请到 “设置 -> 推理设置” 中查看哪些推理模式支持': '2. {type} data will only take effect in certain inference modes. Please check "Settings -> Inference Settings" to see which inference modes are supported', + '3. {type}的数据会在每次推理时进行更新,推理完成后会自动保存到当前小说的{type}数据中': '3. {type} data will be updated during each inference and automatically saved to the current novel\'s {type} data after completion', + '4. 也可自定义或者修改指定的{type}数据,点击 “编辑” 按钮进行修改或 “添加数据” 按钮进行添加': '4. You can also customize or modify specific {type} data by clicking the "Edit" button to modify or the "Add Data" button to add', + '5. 数据修改之后需要手动保存,点击 “保存” 按钮进行保存': '5. After modifying data, you need to save manually by clicking the "Save" button', + '6. 删除数据后,推理时会自动删除当前小说的{type}中的对应数据': '6. After deleting data, the corresponding data in the current novel\'s {type} will be automatically removed during inference', + '7. 需要在外部手动选择需要的{type}数据时,请点击“{button}” 按钮进行导入到标签集中': '7. When you need to manually select the required {type} data externally, please click the "{button}" button to import it into the tag set', + '即将开始自动推理,该操作会将之前的 {type} 数据覆盖,是否继续?': 'About to start automatic inference. This operation will overwrite previous {type} data. Continue?', + '正在推理,请稍等...': 'Inferring, please wait...', + //#endregion + + //#region 转视频 + "视频配置": 'Video Configuration', + "批量设置": 'Batch Settings', + "选中视频": 'Selected Videos', + "暂无选择视频": 'No videos selected', + "暂无可选视频": 'No videos available', + "视频选择": 'Video Selection', + "返回图转视频列表": 'Return to image-to-video list', + "视频配置信息": 'Video Configuration Information', + "右侧面板已显示": 'Right panel is displayed', + "请选择左侧表格中的任务查看详细信息": 'Please select a task from the left table to view details', + "已取消批量设置视频类型": 'Batch video type setting cancelled', + "选择转视频方式": 'Select video conversion method', + "提示词": 'Prompt', + "保存选中视频": 'Save selected videos', + "视频生成类型": 'Video Generation Type', + "视频总数量": 'Total Video Count', + "是否删除小说数据,删除后数据将被永久删除,所有的子项目数据和文件数据都会被删除,请确认是否继续?": "Are you sure you want to delete the novel data? After deletion, the data will be permanently deleted, and all sub-project data and file data will be deleted. Please confirm whether to continue?", + "正在打开任务...": "Opening task...", + "正在初始化图/文转视频模块": "Initializing image/text-to-video module", + '已开启分页,表格高度已调整': 'Pagination enabled, table height adjusted', + '已关闭分页,显示全部数据,表格高度已调整': 'Pagination disabled, showing all data, table height adjusted', + '右侧面板已隐藏,点击查看按钮可在抽屉中查看详情': 'Right panel is hidden, click the view button to see details in drawer', + 'Runway 视频配置': 'Runway Video Configuration', + 'Luma 视频配置': 'Luma Video Configuration', + 'Kling 视频配置': 'Kling Video Configuration', + 'Midjourney 视频配置': 'Midjourney Video Configuration', + 'PIKA 视频配置': 'PIKA Video Configuration', + '确定要将所有 {taskLength} 个任务的视频类型设置为 {typeInfo} 吗?此操作不可撤销。': 'Are you sure you want to set all {taskLength} tasks video type to {typeInfo}? This operation cannot be undone.', + '已批量设置 {tasksLength} 个任务的视频类型为: {optionLabel}': 'Successfully set video type for {tasksLength} tasks to: {optionLabel}', + '批量设置视频类型失败: {error}': 'Failed to batch set video type: {error}', + '确定要重新加载任务 “{name}” 吗?\n\n重新加载会将当前的任务重新获取,只能再当前任务视频下载失败时才能使用。否则会导致重复的视频显示,请确认是否继续!': ' Are you sure you want to reload task "{name}"?\n\nReloading will re-fetch the current task and can only be used when the current task video download fails. Otherwise, it may cause duplicate videos to appear. Please confirm whether to continue!', + '正在重新加载视频任务...': 'Reloading video task...', + '未找到当前分镜的视频配置,不可重新加载!!': 'Video configuration for current storyboard not found, cannot reload!!', + '重新加载任务失败,{error}': 'Failed to reload task, {error}', + '暂无视频,请先生成视频': 'No videos available, please generate videos first', + '当前分镜没有转视频相关的信息,请先进行转视频操作!': 'Current storyboard has no video conversion information, please perform video conversion first!', + '当前未选择任何视频,请先选择 {cout} 个视频!': 'No videos currently selected, please select {cout} videos first!', + '任务ID不能为空,请检查!': 'Task ID cannot be empty, please check!', + '视频信息详情:': 'Video Information Details:', + '修改选择的视频之后,请点击 “保存视频选择” 按钮将操作进行保存生效!!!': 'After modifying the selected videos, please click the "Save Video Selection" button to save and apply the changes!!!', + '正在加载任务数据...': 'Loading task data...', + '加载数据失败,{error}': 'Failed to load data, {error}', + '处理数据时发生错误,{error}': 'Error occurred while processing data, {error}', + "视频生成成功": "Video generation successful", + "图转视频失败,失败信息如下:{error}": "Image-to-video conversion failed, error details: {error}", + '不支持的图片链接,仅支持网络图片链接!': 'Unsupported image link, only network image links are supported!', + + //#region MJ + '基本信息': 'Basic Information', + "任务名称": 'Task Name', + "消息名称": "Message Name", + "任务状态": 'Task Status', + "任务描述": 'Task Description', + "暂无视频配置信息": 'No video configuration information', + '视频类型已更改为:{type}': 'Video type changed to: {type}', + '更新视频类型失败:{error}': 'Failed to update video type: {error}', + "数据加载中...": "Loading data...", + "选择大分类": "Select Main Category", + "文案处理设置": "Text Processing Settings", + "设置文案处理相关的API和设置": "Configure APIs and settings for text processing", + "暂无视频,请先生成视频!": "No videos available, please generate videos first!", + '图片链接(首帧)': 'Image Link (First Frame)', + '请输入图片链接': 'Please enter image link', + '尾帧图片链接': 'End Frame Image Link', + '提示词(可选)': 'Prompt (Optional)', + "图生视频": 'Image to Video', + "视频拓展": 'Video Extension', + "任务ID": 'Task ID', + "视频索引": 'Video Index', + "视频类型": 'Video Type', + "更新到": 'Update to', + '未知的修改键: {key}': 'Unknown modification key: {key}', + "视频预览": 'Video Preview', + "请选择一个视频": 'Please select a video', + "任务详情": 'Task Details', + "保存任务选择": 'Save Task Selection', + "分镜名称": 'Shot Name', + "本地路径": 'Local Path', + "远端地址": 'Remote Address', + "视频总数": 'Total Videos', + '视频 {index}': 'Video {index}', + "没有找到对应的API的配置,请检查 ‘{data}’ 配置!": "API configuration not found, please check '{data}' configuration!", + "当前API不支持MJ出图,请检查 ‘{data}’ 配置!": "Current API does not support MJ image generation, please check '{data}' configuration!", + "没有找到对应的生图包的配置或配置有误,请检查 ‘{data}’ 配置!": "Image generation package configuration not found or incorrect, please check '{data}' configuration!", + "当前选择的包不支持,请检查 ‘{data}’ 配置!": "Currently selected package not supported, please check '{data}' configuration!", + "本地代理模式的设置不完善或配置错误,请检查 ‘{data}’ 配置!": "Local proxy mode settings incomplete or configured incorrectly, please check '{data}' configuration!", + "当前的MJ出图模式不支持,请检查 ‘{data}’ 配置!": "Current MJ image generation mode not supported, please check '{data}' configuration!", + "MJ反推的类型不支持,反推只支持API和代理模式": "MJ reverse engineering type not supported, reverse engineering only supports API and proxy modes", + "MJ出图的类型不支持": "MJ image generation type not supported", + "提示词里面出现了 feishu.cn 飞书的链接,请检查并复制正确的链接": "Feishu.cn link detected in prompt, please check and copy the correct link", + "返回的数据为空": "Returned data is empty", + "{emptyName} 的提示词为空,请先推理提示词": "{emptyName} prompt is empty, please generate prompt first", + "MJ模式合并数据成功!": "MJ image generation failed, {error}", + "MJ模式合并数据失败,{error}": "MJ mode data merge failed, {error}", + "MJ生图失败,{error}": "MJ image generation failed, {error}", + '当前选中的视频的 taskId 或 videoIndex 为空,请检查视频信息': "TaskId or videoIndex of currently selected video is empty, please check video information", + '修改选择的视频任务之后,会将对应的选择的视频的': "After modifying the selected video task, the corresponding selected video's", + '父任务ID': 'Parent Task ID', + '选择父任务': 'Select Parent Task', + '请输入需要操作的视频任务ID': 'Please enter the video task ID to operate', + '父任务ID说明:': 'Parent Task ID Description:', + '点击按钮可以选择当前分镜的父任务ID,转视频类别为 MJ_VIDEO 的任务。': 'Click the button to select the parent task ID for the current shot, for tasks with video category MJ_VIDEO.', + '也可手动输入 MJ_VIDEO 的任务ID,手动输入时,视频索引也需手动输入!!': 'You can also manually enter the MJ_VIDEO task ID. When entering manually, the video index must also be entered manually!!', + '注意:如果当前任务不是 MJ_VIDEO 类型,则无法选择父任务。': 'Note: If the current task is not of MJ_VIDEO type, you cannot select a parent task.', + '当前任务不是 MJ_VIDEO 类型,无法选择父任务!': 'Current task is not of MJ_VIDEO type, cannot select parent task!', + '视频索引 (Index)': 'Video Index (Index)', + '视频索引说明:': 'Video Index Description:', + '选择要进行视频拓展的具体视频索引,范围为 0-3。': 'Select the specific video index for video extension, range 0-3.', + '索引 0:': 'Index 0:', + '第一个生成的视频': 'First generated video', + '索引 1:': 'Index 1:', + '第二个生成的视频': 'Second generated video', + '索引 2:': 'Index 2:', + '第三个生成的视频': 'Third generated video', + '索引 3:': 'Index 3:', + '第四个生成的视频': 'Fourth generated video', + '选择对应的索引后,会对该索引的视频进行拓展操作。': 'After selecting the corresponding index, the video at that index will be extended.', + '视频类型 (SD/HD)': 'Video Type (SD/HD)', + '视频质量说明:': 'Video Quality Description:', + '选择视频类型': 'Select Video Type', + 'SD(标清):': 'SD (Standard Definition):', + '480p 质量,适合一般用途和快速生成。': '480p quality, suitable for general use and fast generation.', + 'HD(高清):': 'HD (High Definition):', + '720p 质量,更高的清晰度,需要相应的订阅套餐。': '720p quality, higher clarity, requires corresponding subscription plan.', + '不同宽高比的视频尺寸:': 'Video dimensions for different aspect ratios:', + '* 视频的具体形状和大小取决于起始图像的宽高比': '* The specific shape and size of the video depends on the aspect ratio of the starting image', + '运动变化 (Motion)': 'Motion Variation (Motion)', + '运动变化程度说明:': 'Motion Variation Level Description:', + '生成视频时,您有两个运动设置选项可供选择:“低运动”和“高运动”。请使用网站上的相应按钮,或在视频提示末尾添加': 'When generating videos, you have two motion setting options to choose from: "Low Motion" and "High Motion". Please use the corresponding buttons on the website, or add at the end of the video prompt', + '或参数': 'or parameter', + '低运动(默认):': 'Low Motion (Default):', + '更有可能产生静止场景、低摄像机运动、慢动作或细微的角色动作。': 'More likely to produce static scenes, low camera movement, slow motion, or subtle character actions.', + '高运动:': 'High Motion:', + '更有可能导致大的摄像机运动和更大的角色运动,但也可能产生不切实际或有故障的动作。': 'More likely to result in large camera movements and greater character motion, but may also produce unrealistic or faulty actions.', + '选择运动变化程度': 'Select Motion Variation Level', + '批次数量 (Batch)': 'Batch Count (Batch)', + '批次数量说明:': 'Batch Count Description:', + '选择一次生成的视频数量。生成多个视频可以提供更多选择,但会消耗更多的配额和时间。': 'Select the number of videos to generate at once. Generating multiple videos provides more choices but consumes more quota and time.', + '1个视频:': '1 Video:', + '生成单个视频,速度快,配额消耗少。': 'Generate a single video, fast speed, low quota consumption.', + '2个视频:': '2 Videos:', + '生成2个不同的视频变体,提供更多选择。': 'Generate 2 different video variants, providing more choices.', + '4个视频:': '4 Videos:', + '生成4个不同的视频变体,最多选择,但配额消耗最多。': 'Generate 4 different video variants, maximum choices but highest quota consumption.', + '注意:批次数量越大,消耗的配额越多,生成时间也越长。': 'Note: The larger the batch count, the more quota consumed and the longer the generation time.', + '选择批次数量': 'Select Batch Count', + '视频原始 (Raw)': 'Video Raw (Raw)', + '视频原始开关说明:': 'Video Raw Switch Description:', + '为了更精确地控制视频创作的运动,使用': 'For more precise control of video creation motion, using', + '参数会很有帮助。此功能的作用类似于图像的 Raw 模式。': 'parameter will be very helpful. This function works similarly to Raw mode for images.', '当您添加': 'When you add', + ',它会减少 Midjourney 添加的额外创意天赋,从而使您的提示文本对结果产生更大的影响。': ', it reduces the extra creative flair that Midjourney adds, making your prompt text have a greater impact on the results.', '注意:视频原始只有在有提示词的时候才会生效': 'Note: Video Raw only takes effect when there is a prompt', + '首尾循环(首尾帧相同)': 'Loop (Same First and Last Frame)', + '首尾循环说明:': 'Loop Description:', + '开启此选项将创建一个无缝循环的视频,视频的最后一帧将平滑过渡到第一帧。': 'Enabling this option will create a seamlessly looping video, where the last frame of the video will smoothly transition to the first frame.', + '首尾循环和尾帧图片链接不能同时使用,如果同时启用,尾帧图片链接将被忽略。': 'Loop and end frame image link cannot be used simultaneously. If both are enabled, the end frame image link will be ignored.', + '注意:循环视频适合制作动画效果或背景视频。': 'Note: Loop videos are suitable for creating animation effects or background videos.', + '执行视频拓展': 'Execute Video Extension', + '请先选择父任务ID和视频索引!': 'Please select parent task ID and video index first!', + '添加视频拓展任务成功!': 'Video extension task added successfully!', + '请选择运动变化程度': 'Please select motion variation level', + '生成视频': 'Generate Video', + '图片加载失败,请检查图片链接是否有效!': 'Image loading failed, please check if the image link is valid!', + '视频生成时长为5秒,但并非仅限于此。视频制作完成后,您可以在当前界面为选定的适配进行延长!': 'Video generation duration is 5 seconds, but not limited to this. After video production is completed, you can extend the selected adaptation in the current interface!', + '您可以随意将视频延长最多 4 次,每次延长 4 秒,直至达到 21 秒(即可用的最大长度)。': 'You can freely extend the video up to 4 times, each extension adding 4 seconds, until reaching 21 seconds (the maximum available length).', + + //#endregion + //#endregion + + //#region 文案处理 + "【LaiTool】场景提示大师(上下文-提示词不包含人物)": "【LaiTool】Scene Prompt Master (Context - Prompt without characters)", + "【LaiTool】分镜大师-特效增强版(上下文-人物场景固定)": "【LaiTool】Storyboard Master - Effects Enhanced (Context - Fixed characters and scenes)", + '【LaiTool】分镜大师-通用版(上下文-人物场景固定-类型推理)': '【LaiTool】Storyboard Master - Universal (Context - Fixed characters and scenes - Type inference)', + '【LaiTool】分镜大师-全面版-AI增强(上下文-人物场景固定-单帧)': '【LaiTool】Storyboard Master - Comprehensive AI Enhanced (Context - Fixed characters and scenes - Single frame)', + '【LaiTool】分镜大师-全能优化版(上下文-人物固定)': '【LaiTool】Storyboard Master - All-in-One Optimized (Context - Fixed characters)', + '【LaiTool】分镜大师-MJ古风版(上下文-人物场景固定-MJ古风提示词)': '【LaiTool】Storyboard Master - MJ Ancient Style (Context - Fixed characters and scenes - MJ ancient style prompts)', + '【LaiTool】分镜大师-SD英文版(上下文-人物场景固定-SD-英文提示词)': '【LaiTool】Storyboard Master - SD English (Context - Fixed characters and scenes - SD English prompts)', + '【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物自动推理)': '【LaiTool】Storyboard Master - Single Frame Prompts (Context - Single frame - Auto character inference)', + "没有找到对应的AI选项,请先检查配置": "No corresponding AI option found, please check configuration first", + '添加分割标识符': 'Add Separator Identifier', + '获取特殊字符失败:{error}': 'Failed to get special characters: {error}', + '格式化成功': 'Formatting successful', + '格式化失败:{error}': 'Formatting failed: {error}', + "添加分割符": 'Add Separator', + "请输入分割符": 'Please enter separator', + "正在初始化文案处理模块": "Initializing text processing module", + "选择分类失败": "Failed to select category", + "设置已更新": "Settings updated", + "设置更新失败,{error}": "Settings update failed, {error}", + "数据加载失败": "Data loading failed", + "分割合并文案成功": "Text splitting and merging successful", + "分割合并文案失败, {error}": "Text splitting and merging failed, {error}", + "分割合并文案失败:没有数据({data})": "Text splitting and merging failed: no data ({data})", + "初始化文案处理设置失败,{error}": "Failed to initialize text processing settings, {error}", + "同步/初始化/加载 文案处理界面数据成功": "Sync/Initialize/Load text processing interface data successful", + "初始化AI设置失败,{error}": "Failed to initialize AI settings, {error}", + "同步/初始化/加载 文案处理AI设置成功": "Sync/Initialize/Load text processing AI settings successful", + "数据加载失败,{error}": "Data loading failed, {error}", + "没有选择任何分类,请重新选择": "No category selected, please select again", + "没有选择有效的分类,请重新选择": "No valid category selected, please select again", + 'API 设置': 'API Settings', + "AI生成设置": 'AI Generation Settings', + "是否流式发送": 'Stream Sending', + "是否拆分发送": 'Split Sending', + "单次最大字符数": 'Maximum Characters per Request', + '注意:爆款开头不要拆分发送': 'Note: Do not split sending for viral openings', + '同步通用设置Key': 'Sync Universal Settings Key', + '测试连接': 'Test Connection', + '请先填写完整的API设置后再进行测试!': 'Please complete the API settings before testing!', + '测试连接失败': 'Test connection failed', + '测试连接失败: {error}': 'Test connection failed: {error}', + '测试连接失败,返回结果异常,请检查设置!': 'Test connection failed, abnormal return result, please check settings!', + '测试连接成功': 'Test connection successful', + '测试连接成功!请保存数据后使用!': 'Test connection successful! Please save data before use!', + '确认要同步 “设置 -> 推理设置” 中的 “API Key” 和 “推理模型” 吗?这将覆盖当前设置。': 'Confirm to sync "API Key" and "Inference Model" from "Settings -> Inference Settings"? This will overwrite current settings.', + '取消同步': 'Cancel Sync', + '已同步通用设置,请测试成功后保存后使用!': 'Universal settings synced, please save after successful testing!', + '同步失败': 'Sync failed', + '同步通用设置失败,错误信息:{error}': 'Failed to sync universal settings, error: {error}', + '确认保存设置?该操作不会检测数据的可用性,请确保数据填写正确!!!': 'Confirm saving settings? This operation will not detect data availability, please ensure data is filled correctly!!!', + '请填写完整选择的AI相关的设置!': 'Please complete the selected AI-related settings!', + '处理前文本': 'Pre-processing Text', + '导入需要AI处理的文本,新导入的数据会覆盖当前的旧数据': 'Import text that needs AI processing, newly imported data will overwrite current old data', + '导入文本': 'Import Text', + '取消导入操作': 'Cancel Import Operation', + '导入失败:文本不能为空': 'Import failed: text cannot be empty', + '导入失败': 'Import failed: {error}', + '当前已经存在数据,继续操作会删除之前的数据,包括生成之后的数据,若只是简单调整数据,可在外面显示的表格中进行直接修改,是否继续?': 'Data already exists. Continuing will delete previous data, including generated data. For simple adjustments, you can directly modify in the displayed table outside. Continue?', + '将当前文本按照设定的单次最大次数进行分割,分割后会清空已生成的内容': 'Split current text according to set maximum count per session, generated content will be cleared after splitting', + '文案分割': 'Text Splitting', + '将所有分割出来的文案进行AI处理,再处理之前会删除当前已有的处理后文本,若是需要生成部分,请使用单个的生成': 'Process all split text with AI, existing processed text will be deleted before processing. For partial generation, please use individual generation', + '全部生成': 'Generate All', + '请输入文本': 'Please enter text', + '处理后文本': 'Post-processing Text', + '将所有的处理后文本合并显示,在里面可以格式化和一键复制,但是在里面对于文本的操作不会被保存,修改后请及时复制': 'Merge and display all processed text, with formatting and one-click copy available, but text operations inside will not be saved, please copy promptly after modification', + '显示生成文本': 'Show Generated Text', + '将所有处理后文本合并之后,直接复制到剪贴板': 'Merge all processed text and copy directly to clipboard', + '复制生成文本': 'Copy Generated Text', + '将所有生成后文本为空的数据进行AI处理,有数据的则跳过': 'Process all data with empty generated text using AI, skip those with existing data', + '生成失败:不存在未生成的文本!': 'Generation failed: No ungenerated text exists!', + '生成空文本': 'Generate Empty Text', + '清空所有的生成后文本,数据删除后不可恢复': 'Clear all generated text, data cannot be recovered after deletion', + 'AI 改写后的文本': 'AI Rewritten Text', + 'AI生成文本': 'AI Generated Text', + '确定要清空所有的AI生成文本吗?清空后不可恢复,是否继续?': 'Are you sure to clear all AI generated text? Cannot be recovered after clearing, continue?', + '清空成功': 'Clear successful', + '清空失败:': 'Clear failed: {error}', + '取消清空': 'Cancel Clear', + '直接复制会将所有的AI生成后的数据直接进行复制,不会进行格式之类的调整,若有需求可以再下面表格直接修改,或者是再左边的显示生成文本中修改,是否继续复制?': 'Direct copy will copy all AI generated data directly without format adjustments. If needed, you can modify directly in the table below or in the display generated text on the left. Continue copying?', + '复制失败:存在未生成的文本,请先生成文本!': 'Copy failed: Ungenerated text exists, please generate text first!', + '复制成功': 'Copy successful', + '复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}': 'Copy failed, please manually copy in the display generated text on the left, error details: {error}', + '复制失败:{error}': 'Copy failed: {error}', + "复制失败,请手动复制:{data}": "Copy failed, please copy manually: {data}", + '取消复制': 'Cancel Copy', + '批量生成确认': 'Batch Generation Confirmation', + '生成确认': 'Generation Confirmation', + '确定要重新生成选中的 {count} 行AI生成文本吗?重新生成后会清空之前的生成文本并且不可恢复,是否继续?': 'Are you sure to regenerate the selected {count} lines of AI generated text? Previous generated text will be cleared and cannot be recovered after regeneration, continue?', + '确定重新生成当前行的AI生成文本吗?重新生成后会清空之前的生成文本并且不可恢复,是否继续?': 'Are you sure to regenerate the AI generated text for the current line? Previous generated text will be cleared and cannot be recovered after regeneration, continue?', + '批量生成中 ({count} 条)......': 'Batch generating ({count} items)......', + '生成中......': 'Generating......', + '生成失败:未找到对应的数据': 'Generation failed: No corresponding data found', + '警告:{allCount} 个ID未找到对应数据,将生成 {validCount} 条记录': 'Warning: {allCount} IDs have no corresponding data, will generate {validCount} records', + '批量生成完成,共处理 {conut} 条记录': 'Batch generation completed, processed {conut} records in total', + '确定要清空当前行的AI生成文本吗?数据删除后不可恢复,是否继续?': 'Are you sure to clear the AI generated text for the current line? Data cannot be recovered after deletion, continue?', + '清空失败:未找到对应的数据': 'Clear failed: No corresponding data found', + '生成完成': 'Generation completed', + '生成失败:{error}': 'Generation failed: {error}', + '取消批量生成': 'Cancel Batch Generation', + '取消生成': 'Cancel Generation', + '确定要将当前文本按照设定的单次最大次数进行分割吗?分割后会清空已生成的内容,是否继续?': 'Are you sure to split the current text according to the set maximum count per session? Generated content will be cleared after splitting, continue?', + '分割失败:{error}': 'Split failed: {error}', + '取消分割': 'Cancel Split', + '确定要将所有的文案进行AI生成吗?在生成前会将生成后文本删除,是否继续?': 'Are you sure to generate all text content with AI? Generated text will be deleted before generation, continue?', + '批量生成失败:{error}': 'Batch generation failed: {error}', + '批量生成完成': 'Batch generation completed', + '注意:这边的格式化不一定会完全格式化,需要自己手动检查': 'Note: The formatting here may not be completely formatted, manual checking is required', + '注意:当前弹窗的修改和格式化只在该界面有效,关闭或重新打开会重新加载同步外部表格数据,之前修改的数据会丢失': 'Note: Modifications and formatting in this popup are only valid in this interface. Closing or reopening will reload and sync external table data, and previous modifications will be lost', + //#endregion + + //#region 菜单 + "首页": "Home", + "原创": "Original", + "转视频": "Video Conversion", + "预设库": "Preset Library", + "文案处理": "Content Processing", + "工具箱": "Toolbox", + "任务": "Tasks", + "文档": "Documentation", + "关于": "About", + "设置": "Settings", + "通用设置": "General Settings", + "推理设置": "Inference Settings", + "MJ设置": "MJ Settings", + "SD/FLUX设置": "SD/FLUX Settings", + "ComfyUI 设置": "ComfyUI Settings", + '剪映关键帧设置': "JianYing Keyframe Settings", + '外观': "Appearance", + //#endregion + + //#region 按钮 + + "按钮,风格预设": "Style", + "按钮,场景预设": "Scene", + "按钮,角色预设": "Character", + + "按钮,单句推理": "Single", + "按钮,下推理": "Next", + "按钮,中文to英文": "CN->EN", + "按钮,命令预览": "Preview", + + "按钮,单句生图": "Single", + "按钮,下生图": "Next", + "按钮,锁定图片": "Lock/Unlock", + "按钮,清空图片": "Clear", + "按钮,导入图片": "Import", + + //#endregion +} \ No newline at end of file diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts new file mode 100644 index 0000000..56004c9 --- /dev/null +++ b/src/i18n/locales/ja.ts @@ -0,0 +1 @@ +export default {} \ No newline at end of file diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts new file mode 100644 index 0000000..56004c9 --- /dev/null +++ b/src/i18n/locales/ko.ts @@ -0,0 +1 @@ +export default {} \ No newline at end of file diff --git a/src/i18n/locales/zh-cn.ts b/src/i18n/locales/zh-cn.ts new file mode 100644 index 0000000..f534375 --- /dev/null +++ b/src/i18n/locales/zh-cn.ts @@ -0,0 +1,1846 @@ +export default { + //#region 通用 + "是": '是', + "否": '否', + "操作": '操作', + "清空": '清空', + "生成": '生成', + "确认": '确认', + "继续": '继续', + "取消": '取消', + "提示": '提示', + "导入": '导入', + "复制": '复制', + "保存": '保存', + "分页": '分页', + "图片": '图片', + "查看": '查看', + "状态": '状态', + "名称": '名称', + "编辑": '编辑', + "删除": '删除', + "成功": '成功', + "失败": '失败', + "关闭": '关闭', + "信息": '信息', + "添加": '添加', + "返回": '返回', + "编号": '编号', + "打开": '打开', + "搜索": '搜索', + "重置": '重置', + "购买": '购买', + "隐藏": "隐藏", + "预览": "预览", + "禁用": "禁用", + "启用": "启用", + "备注": '备注', + "查询": "查询", + "更新": "更新", + "全部": "全部", + "确定": "确定", + "警告": "警告", + "序号": "序号", + "重新加载": '重新加载', + "图片链接": '图片链接', + "文件大小": "文件大小", + "右侧面板": '右侧面板', + "一键格式化": '一键格式化', + "请输入内容": '请输入内容', + "操作提示": '操作提示', + "重要提示": '重要提示', + "更多操作": '更多操作', + "操作确认": '操作确认', + "取消操作": '取消操作', + "保存成功": '保存成功', + "保存失败": '保存失败', + "未知错误": '未知错误', + "未知类型": '未知类型', + "未知操作": '未知操作', + "下载成功": '下载成功', + "下载失败": '下载失败', + "页面不存在": "页面不存在", + "复制的内容为空": '复制的内容为空', + "图片未找到": '图片未找到', + "下载失败,{error}": "下载失败,{error}", + '操作失败:{error}': '操作失败:{error}', + '保存失败:{error}': '保存失败:{error}', + '请输入 {data}': '请输入 {data}', + '请选择 {data}': '请选择 {data}', + '请输入图片链接...': '请输入图片链接...', + "请输入图片地址": '请输入图片地址', + '组件初始化失败: {error}': '组件初始化失败: {error}', + "跳转失败,{error}": "跳转失败,{error}", + "{data} 不能为空": "{data} 不能为空", + "不是一个合法的url地址": "不是一个合法的url地址", + "浏览器不支持流式下载": "浏览器不支持流式下载", + "目的文件/文件夹不存在,{data}": "目的文件/文件夹不存在,{data}", + "源文件或文件夹不存在,{data}": "源文件或文件夹不存在,{data}", + "目的文件或文件夹的父文件夹不存在,{data}": "目的文件或文件夹的父文件夹不存在,{data}", + "输入的不是有效的文件夹地址": "输入的不是有效的文件夹地址", + "下载的文件为空": "下载的文件为空", + "下载图片超时 ({timeout}秒),已重试{maxRetries}次,失败信息:{errorMessage}": "下载图片超时 ({timeout}秒),已重试{maxRetries}次,失败信息:{errorMessage}", + "网络连接失败,无法访问图片地址,已重试 {maxRetries}次,失败信息:{errorMessage}": "网络连接失败,无法访问图片地址,已重试 {maxRetries}次,失败信息:{errorMessage}", + "连接超时,服务器响应缓慢,已重试{maxRetries}次,失败信息:{errorMessage}": "连接超时,服务器响应缓慢,已重试{maxRetries}次,失败信息:{errorMessage}", + "下载图片失败,已重试{maxRetries}次,失败信息: ${errorMessage}": "下载图片失败,已重试{maxRetries}次,失败信息: ${errorMessage}", + "输入的文件地址不是图片文件地址,支持jpg、jpeg、png": "输入的文件地址不是图片文件地址,支持jpg、jpeg、png", + "文件不存在": "文件不存在", + "不支持的文件格式: {ext}。支持的格式: {supportedExt}": "不支持的文件格式: {ext}。支持的格式: {supportedExt}", + "获取图片的宽高失败": "获取图片的宽高失败", + "将base64转换为文件失败,{error}": "将base64转换为文件失败,{error}", + "数据不能为空": "数据不能为空", + "数据解析失败,请检查数据格式": "数据解析失败,请检查数据格式", + "验证错误": "验证错误", + "请修正以下错误,{error}": "请修正以下错误,{error}", + "格式化文本失败,{error}": "格式化文本失败,{error}", + //#endregion + + + //#region 小说 + "分镜计算": '分镜计算', + "分割视频": "分割视频", + "提取音频": "提取音频", + "识别字幕": "识别字幕", + "抽帧": "抽帧", + "MJ生成图片": "MJ生成图片", + "SD生成图片": "SD生成图片", + "flux forge生成图片": "flux forge生成图片", + 'flux api生成图片': 'flux api生成图片', + "D3生成图片": "D3生成图片", + "推理": "推理", + "翻译": "翻译", + "runway生成视频": "runway生成视频", + "luma生成视频": "luma生成视频", + "kling生成视频": "kling生成视频", + "等待": "等待", + "分镜计算中": '分镜计算中', + "分镜计算失败": "分镜计算失败", + "分镜计算完成": '分镜计算完成', + "分割视频中": "分割视频中", + "分割视频失败": "分割视频失败", + "分割视频完成": "分割视频完成", + "提取音频中": "提取音频中", + "提取音频失败": "提取音频失败", + "提取音频完成": "提取音频完成", + "识别字幕中": "识别字幕中", + "识别字幕失败": "识别字幕失败", + "识别字幕完成": "识别字幕完成", + "抽帧中": "抽帧中", + "抽帧失败": "抽帧失败", + "抽帧完成": "抽帧完成", + "反推中": "反推中", + "反推失败": "反推失败", + "反推完成": "反推完成", + "生成图片中": "生成图片中", + "生成图片失败": "生成图片失败", + "生成图片完成": "生成图片完成", + "高清中": "高清中", + "高清失败": "高清失败", + "高清完成": "高清完成", + "合成视频中": "合成视频中", + "合成视频失败": "合成视频失败", + "合成视频完成": "合成视频完成", + "添加草稿完成": "添加草稿完成", + "添加草稿失败": "添加草稿失败", + "图转视频失败": "图转视频失败", + "图转视频成功": "图转视频成功", + "未知": "未知", + "运行中": "运行中", + "暂停": "暂停", + "完成": '完成', + "重连": "重连", + "可灵": "可灵", + "MJ视频": "MJ视频", + "处理中": "处理中", + "高 (High)": "高 (High)", + '低 (Low)': '低 (Low)', + '标清 (SD 480p)': "标清 (SD 480p)", + '高清 (HD 720p)': '高清 (HD 720p)', + "1个视频": "1个视频", + "2个视频": "2个视频", + "4个视频": '4个视频', + "小说ID": "小说ID", + "小说数据为空,无法修改": "小说数据为空,无法修改", + "反推必须传入视频": "反推必须传入视频", + "小说名字 {bookName} 已经存在,请更换小说名字": "小说名字 {bookName} 已经存在,请更换小说名字", + "未知的小说类型": "未知的小说类型", + "修改小说数据失败,缺少小说ID": "修改小说数据失败,缺少小说ID", + "修改小说数据失败,缺少小说数据": "修改小说数据失败,缺少小说数据", + "修改小说数据失败,小说ID对应的数据不存在": "修改小说数据失败,小说ID对应的数据不存在", + "获取修改后的小说数据失败,小说ID对应的数据不存在": "获取修改后的小说数据失败,小说ID对应的数据不存在", + "未找到指定ID的小说数据": "未找到指定ID的小说数据", + "草稿名称不能和任务名称相同,请修改任务名称": '草稿名称不能和任务名称相同,请修改任务名称', + "导出剪映草稿成功!": "导出剪映草稿成功!", + "没有找到导出剪映的执行文件,请检查": "没有找到导出剪映的执行文件,请检查", + "添加/修改小说信息成功!": "添加/修改小说信息成功!", + "添加/修改小说信息失败,{error}": "添加/修改小说信息失败,{error}", + "获取小说数据成功!": "获取小说数据成功!", + "获取小说数据失败,{error}": "获取小说数据失败,{error}", + "修改小说数据成功!": "修改小说数据成功!", + "修改小说数据失败,{error}": "修改小说数据失败,{error}", + "重置小说数据成功!": "重置小说数据成功!", + "重置小说数据失败,{error}": "重置小说数据失败,{error}", + "删除小说数据成功!": "删除小说数据成功!", + "删除小说数据失败,{error}": "删除小说数据失败,{error}", + "获取小说数据错误,未找到指定条件的小说数据": "获取小说数据错误,未找到指定条件的小说数据", + //#endregion + + //#region 小说任务 + '修改小说批次任务信息失败,{error}': '修改小说批次任务信息失败,{error}', + "未找到对应的小说批次任务": '未找到对应的小说批次任务', + "部分分镜的输出图片路径为空": "部分分镜的输出图片路径为空", + "部分分镜的子图片路径数量不足或为空": "部分分镜的子图片路径数量不足或为空", + "没有找到对应的小说批次任务的图片输出地址,请检查": "没有找到对应的小说批次任务的图片输出地址,请检查", + "图片文件 {sourceImagePath} 不存在,请检查": "图片文件 {sourceImagePath} 不存在,请检查", + "将指定的图片放到主图中成功": '将指定的图片放到主图中成功', + "将指定的图片放到主图中失败,{error}": "将指定的图片放到主图中失败,{error}", + "没有要删除的分镜数据,请检查": "没有要删除的分镜数据,请检查", + "删除所有的生图数据成功": "删除所有的生图数据成功", + "删除所有的图片数据失败,{error}": "删除所有的图片数据失败,{error}", + "获取小说批次任务数据成功!": "获取小说批次任务数据成功!", + "获取小说批次任务数据失败,{error}": "获取小说子任务数据失败,{error}", + "修改小说批次任务数据成功!": "修改小说批次任务数据成功!", + "修改小说批次任务数据失败,{error}": "修改小说子任务数据失败,{error}", + "没有找到要删除的小说批次任务数据": "没有找到要删除的小说批次任务数据", + "删除小说批次任务数据成功!": "删除小说批次任务数据成功!", + "删除小说批次任务数据失败,{error}": "删除小说批次任务数据失败,{error}", + "批量添加小说批次任务数据失败,免费版只能添加一条数据": "批量添加小说子任务数据失败,免费版只能添加一条数据", + "批量添加小说批次任务数据失败,小说ID不能为空": "批量添加小说子任务数据失败,小说ID不能为空", + "添加小说批次任务数据成功!": "添加小说批次任务数据成功!", + "添加小说批次任务数据失败,{error}": "添加小说子任务数据失败,{error}", + "获取小说批次任务生成进度成功!": "获取小说批次任务生成进度成功!", + "获取小说批次任务生成进度失败,{error}": "获取小说任务生成进度失败,{error}", + "获取小说批次任务的第一张图片路径成功!": "获取小说批次任务的第一张图片路径成功!", + "获取小说批次任务的第一张图片路径失败,{error}": "获取小说批次任务的第一张图片路径失败,{error}", + "没有对应的小说分镜任务,请先添加分镜任务": "没有对应的小说分镜任务,请先添加分镜任务", + "检测到图片没有出完,请先检查出图": "检测到图片没有出完,请先检查出图", + "有分镜子图数量不足,无法进行一拆四": "有分镜子图数量不足,无法进行一拆四", + "一拆四成功!": "一拆四成功!", + "一拆四失败,{error}": "一拆四失败,{error}", + "重置小说批次任务数据成功!": "重置小说批次任务数据成功!", + "重置小说批次任务数据失败,{error}": "重置小说子任务数据失败,{error}", + "重置小说分镜数据(不包含批次任务本身数据),并且初始化全新的分镜信息失败,小说分镜数据不存在": "重置小说分镜数据(不包含批次任务本身数据),并且初始化全新的分镜信息失败,小说分镜数据不存在", + "重置小说分镜数据(不包含批次任务本身数据),并且初始化全新的分镜信息失败,{error}": "重置小说分镜数据(不包含批次任务本身数据),并且初始化全新的分镜信息失败,{error}", + "没有找到对应的小说数据,请先添加小说": "没有找到对应的小说数据,请先添加小说", + "srt文件路径不能为空!": "srt文件路径不能为空!", + "srt文件后缀不正确,请检查!": "srt文件后缀不正确,请检查!", + "初始视频消息失败,未找到指定小说批次任务的分镜数据": "初始视频消息失败,未找到指定小说批次任务的分镜数据", + '根据ID获取小说批次任务信息失败,ID不能为空!': '根据ID获取小说批次任务信息失败,ID不能为空!', + "未找到对应的小说批次任务信息,请检查!": "未找到对应的小说批次任务信息,请检查!", + //#endregion + + //#region 小说分镜 + "没有找到要更新的小说分镜信息": "没有找到要更新的小说分镜信息", + "修改小说分镜的VideoMessage成功!": "修改小说分镜的VideoMessage成功!", + "修改小说分镜的VideoMessage失败,{error}": "修改小说分镜的VideoMessage失败,{error}", + "查询小说分镜信息,查询条件不能为空!": "查询小说分镜信息,查询条件不能为空!", + '未找到小说分镜数据,请检查!': '未找到小说分镜数据,请检查!', + "没有找到可处理的小说分镜信息": '没有找到可处理的小说分镜信息', + "当前分镜数据的MJ图转视频参数为空或参数校验失败,请检查": "当前分镜数据的MJ图转视频参数为空或参数校验失败,请检查", + '当前Midjourney模式不支持视频生成功能,请更换为MJ API或本地代理模式后重试!': '当前Midjourney模式不支持视频生成功能,请更换为MJ API或本地代理模式后重试!', + 'Midjourney图转视频任务执行失败,失败信息如下:{error}': 'Midjourney图转视频任务执行失败,失败信息如下:{error}', + 'Midjourney图转视频任务执行完成。': 'Midjourney图转视频任务执行完成。', + 'Midjourney图转视频任务执行中...': 'Midjourney图转视频任务执行中...', + '已成功提交Midjourney图转视频任务,任务ID:{taskId}': '已成功提交Midjourney图转视频任务,任务ID:{taskId}', + "小说批次任务的分镜数据的转视频配置为空,请检查": "小说批次任务的分镜数据的转视频配置为空,请检查", + "分镜的图片没有全部出完,不能继续该操作!!": "分镜的图片没有全部出完,不能继续该操作!!", + "分镜 {name} 图片在本地未找到,不能继续该操作,请检查对应分镜的图片路径是否正确": "分镜 {name} 图片在本地未找到,不能继续该操作,请检查对应分镜的图片路径是否正确", + "分镜的图片全部存在,可以进行高清处理": "分镜的图片全部存在,可以进行高清处理", + "未找到指定ID的小说分镜信息,ID: {id}": "未找到指定ID的小说分镜信息,ID: {id}", + "未找到对应的属性,属性: {property}": "未找到对应的属性,属性: {property}", + "数据不完整,缺少小说ID或者小说批次任务ID": "数据不完整,缺少小说ID或者小说批次任务ID", + "没有找到要更新的出图信息": "没有找到要更新的出图信息", + "未找到执行的翻译的小说分镜数据,无法写回": "未找到执行的翻译的小说分镜数据,无法写回", + '缺少必要的条件,必须传入id,bookId或者bookTaskId其中一个': '缺少必要的条件,必须传入id,bookId或者bookTaskId其中一个', + "未找到对应的小说分镜信息": "未找到对应的小说分镜信息", + "所有分镜文案内容为空,无法进行AI合并": "所有分镜文案内容为空,无法进行AI合并", + "不支持的分镜合并类型": "不支持的分镜合并类型", + "上传图片,并修改小说信息成功": "上传图片,并修改小说信息成功", + "上传图片,并修改小说信息失败,{error}": "上传图片,并修改小说信息失败,{error}", + "未检测到分镜的输出图片,无法进行高清处理": '未检测到分镜的输出图片,无法进行高清处理', + "分镜高清图片成功": "分镜高清图片成功", + "分镜高清图片失败,{error}": "分镜高清图片失败,{error}", + "图片裁剪失败": '图片裁剪失败', + "下载指定的图片地址并且分割成功": "下载指定的图片地址并且分割成功", + "下载指定的图片地址并且分割失败,{error}": "下载指定的图片地址并且分割失败,{error}", + "只有MJ模式下才能使用这个功能": "只有MJ模式下才能使用这个功能", + "只有MJ API模式下才能使用这个功能": "只有MJ API模式下才能使用这个功能", + "分镜中没有MJ的消息数据,请检查分镜数据": "分镜中没有MJ的消息数据,请检查分镜数据", + "没有找到对应分镜的MJ Task ID,请检查分镜数据": "没有找到对应分镜的MJ Task ID,请检查分镜数据", + "没有找到对应的分镜的MJ图片链接,请检查分镜数据": "没有找到对应的分镜的MJ图片链接,请检查分镜数据", + "获取图片链接并且下载成功": "获取图片链接并且下载成功", + "获取图片链接并且下载失败,{error}": "获取图片链接并且下载失败,{error}", + "没有找到要推理的分镜数据": "没有找到要推理的分镜数据", + "推理所有数据完成": '推理所有数据完成', + "推理所有数据失败,{error}": "推理所有数据失败,{error}", + "AI分镜头合并成功": 'AI分镜头合并成功', + "未知的合并模式,请检查": '未知的合并模式,请检查', + "合并提示词失败,{error}": "合并提示词失败,{error}", + "分析的类型只能是角色或场景,请检查": "分析的类型只能是角色或场景,请检查", + "没有找到要分析的分镜数据,请先导入文案或者时srt!": "没有找到要分析的分镜数据,请先导入文案或者时srt!", + "未知的分析类型,请检查": '未知的分析类型,请检查', + "自动分析角色或场景成功": '自动分析角色或场景成功', + "自动分析角色或场景失败,{error}": "自动分析角色或场景失败,{error}", + "获取小说分镜数据成功!": "获取小说分镜数据成功!", + "获取小说分镜数据失败,{error}": "获取小说分镜数据失败,{error}", + "修改小说分镜数据成功!": "修改小说分镜数据成功!", + "修改小说分镜数据失败,{error}": "修改小说分镜数据失败,{error}", + "保存小说分镜数据成功!": "保存小说分镜数据成功!", + "保存小说分镜数据失败,{error}": "保存小说分镜数据失败,{error}", + "目前只支持对小说批次任务的文案保存": "目前只支持对小说批次任务的文案保存", + "重置小说分镜数据成功!": "重置小说分镜数据成功!", + "重置小说分镜数据失败,{error}": "重置小说分镜数据失败,{error}", + "初始化分镜视频消息失败,{error}": "初始化分镜视频消息失败,{error}", + "小说分镜ID不能为空": "小说分镜ID不能为空", + "使用GPT翻译不支持拆分": "使用GPT翻译不支持拆分", + "GPT翻译只支持中英互译": "GPT翻译只支持中英互译", + "AI翻译 {source} 译 {target} 成功": "AI翻译 {source} 译 {target} 成功", + "反推提示词的ID不能为空": "反推提示词的ID不能为空", + "全部翻译完成": "全部翻译完成", + "翻译失败,{error}": "翻译失败,{error}", + //#endregion + + //#region 出图 + "ComfyUI生图失败,{error}": "ComfyUI生图失败,{error}", + 'ComfyUI的工作流设置为空,请检查是否正确设置!!': "ComfyUI的工作流设置为空,请检查是否正确设置!!", + "未找到选中的工作流,请检查是否正确设置!!": "未找到选中的工作流,请检查是否正确设置!!", + "本地未找到选中的工作流文件地址,请检查是否正确设置!!": "本地未找到选中的工作流文件地址,请检查是否正确设置!!", + "工作流文件内容不是有效的JSON格式,请检查是否正确设置!!": "工作流文件内容不是有效的JSON格式,请检查是否正确设置!!", + "工作流文件内容不是有效的JSON对象格式,请检查是否正确设置!!": "工作流文件内容不是有效的JSON对象格式,请检查是否正确设置!!", + "错误信息:{error},错误节点:{node}": "错误信息:{error},错误节点:{node}", + "未知错误,未获取到请求ID,请检查是否正确设置!!": "未知错误,未获取到请求ID,请检查是否正确设置!!", + "ComfyUI 生成图片成功!": "ComfyUI 生成图片成功!", + "ComfyUI生图失败,未获取到请求ID,请检查是否正确设置!!": "ComfyUI生图失败,未获取到请求ID,请检查是否正确设置!!", + "未获取到ComfyUI的请求地址,请检查是否正确设置!!": "未获取到ComfyUI的请求地址,请检查是否正确设置!!", + "ComfyUI 生图失败,详细失败信息看启动器控制台": 'ComfyUI 生图失败,详细失败信息看启动器控制台', + "FLUX FORGE 生成图片成功!": "FLUX FORGE 生成图片成功!", + "FLUX FORGE 生成图片失败,{error}": "FLUX FORGE 生成图片失败,{error}", + "未知的合并类型": "未知的合并类型", + "SD合并提示词成功!": "SD合并提示词成功!", + "SD合并提示词失败,{error}": "SD合并提示词失败,{error}", + 'SD生成图片成功!': "SD生成图片成功!", + "SD生成图片失败,{error}": "SD生成图片失败,{error}", + //#endregion + + //#region 关于 + "软件信息": "软件信息", + "LaiTool PRO是一款AI说推文工具,帮助用户提升工作效率,快速高效的完成AI漫画推文的制作": "LaiTool PRO是一款AI说推文工具,帮助用户提升工作效率,快速高效的完成AI漫画推文的制作", + "专业的AI创作工具套件,集成图像生成、智能文案、语音合成等多项前沿AI技术,助力创作者高效产出优质内容": "专业的AI创作工具套件,集成图像生成、智能文案、语音合成等多项前沿AI技术,助力创作者高效产出优质内容", + "授权信息": "授权信息", + "机器码": "机器码", + "授权码": '授权码', + "授权状态": "授权状态", + "激活": "激活", + "停用": "停用", + "授权时间": '授权时间', + "未授权": "未授权", + "到期时间": "到期时间", + "© 2025 LaiTool PRO. 保留所有权利": "© 2025 LaiTool PRO. 保留所有权利", + "您访问的页面不存在或已被移除": "您访问的页面不存在或已被移除", + "返回首页": "返回首页", + "版本 {version}": "版本 {version}", + "快速开始": "快速开始", + "使用文档": "使用文档", + "核心功能": "核心功能", + '更新日志': "更新日志", + "最新版本:{version}": "最新版本:{version}", + '查看全部': "查看全部", + '最新': "最新", + '联系支持': "联系支持", + "需要帮助?": "需要帮助?", + '我们提供多种方式为您解答疑问': "我们提供多种方式为您解答疑问", + '友情链接': "友情链接", + '推荐的工具和资源': "推荐的工具和资源", + '关于软件': "关于软件", + '使用条款': "使用条款", + '隐私政策': "隐私政策", + '问题反馈': "问题反馈", + "聚合API平台,提供多种API服务": "聚合API平台,提供多种API服务", + "B站-向北": "B站-向北", + "向北的B站频道,提供LaiTool相关视频教程": "向北的B站频道,提供LaiTool相关视频教程", + 'AI艺术创作平台': "AI艺术创作平台", + '开源AI绘画工具': "开源AI绘画工具", + '系统状态': "系统状态", + '正常运行': "正常运行", + '已激活': "已激活", + '性能状态': "性能状态", + '良好': "良好", + '服务状态': "服务状态", + '在线': "在线", + 'AI图像生成': "AI图像生成", + "支持MJ、SD等多种AI绘图模型": "支持MJ、SD等多种AI绘图模型", + '智能文案创作': "智能文案创作", + '基于AI的文案生成与优化': "基于AI的文案生成与优化", + '语音合成': "语音合成", + '高质量的AI语音生成服务': "高质量的AI语音生成服务", + '项目管理': "项目管理", + '统一管理创作项目和素材': "统一管理创作项目和素材", + '批量生成': "批量生成", + '支持批量处理和生成任务': "支持批量处理和生成任务", + '二创反推': "二创反推", + '从现有作品反推创作参数': "从现有作品反推创作参数", + '任务队列管理': "任务队列管理", + '管理和监控后台任务执行': "管理和监控后台任务执行", + '新增': "新增", + '优化': '优化', + '修复': "修复", + '其他': "其他", + '打开首页': "打开首页", + '返回软件首页': "返回软件首页", + '邮箱支持': "邮箱支持", + '发送邮件咨询': '发送邮件咨询', + '联系开发者': '联系开发者', + '添加开发者微信获取支持': "添加开发者微信获取支持", + '微信交流群': "微信交流群", + '仅限永久VIP用户可加入': "仅限永久VIP用户可加入", + '加入用户交流群': "加入用户交流群", + "如有问题或建议,欢迎通过邮箱与我们联系:{data}": "如有问题或建议,欢迎通过邮箱与我们联系:{data}", + '发送邮件': '发送邮件', + '已复制到剪贴板': '已复制到剪贴板', + "更新提醒!": "更新提醒!", + "当前版本为 {currentVersion} ,最新版本为 {latestVersion} ,请及时更新!": "当前版本为 {currentVersion} ,最新版本为 {latestVersion} ,请及时更新!", + "更新内容!": "更新内容!", + //#endregion + + //#region 任务 + '失败原因': "失败原因", + "添加出图任务失败,未找到可生成图片的分镜,或者分镜都被锁定": "添加出图任务失败,未找到可生成图片的分镜,或者分镜都被锁定", + "添加出图任务失败,不支持的出图类型": "添加出图任务失败,不支持的出图类型", + "停止出图任务失败,参数错误": "停止出图任务失败,参数错误", + "停止出图任务失败,没有需要停止的任务": "停止出图任务失败,没有需要停止的任务", + "用户手动取消了任务": "用户手动取消了任务", + "停止所有批次的出图任务成功": "停止所有批次的出图任务成功", + "停止当前批次的出图任务成功": "停止当前批次的出图任务成功", + "成功返回数据": "成功返回数据", + "未知报错,没有捕获的错误": "未知报错,没有捕获的错误", + "未找到指定的后台任务数据": "未找到指定的后台任务数据", + '任务被丢弃': "任务被丢弃", + "任务队列过多,{id} 重新提交排队": "任务队列过多,{id} 重新提交排队", + "MJ生图成功,分镜ID:{bookTaskDetailId},任务ID:${taskId}": "MJ生图成功,分镜ID:{bookTaskDetailId},任务ID:${taskId}", + "第 {attempts} 请求失败,开始下一次重试,失败信息如下,{error}": "第 {attempts} 请求失败,开始下一次重试,失败信息如下,{error}", + "失败次数超过 {retries} 错误信息如下,{error}": "失败次数超过 {retries} 错误信息如下,{error}", + '所有重试失败': "所有重试失败", + "请求失败,状态码:{statusCode}": "请求失败,状态码:{statusCode}", + "试用时间已到,请联系客服": "试用时间已到,请联系客服", + "生图失败,共5次尝试,目前第 {retryCount} 次": "生图失败,共5次尝试,目前第 {retryCount} 次", + "任务 {taskId} 执行失败,错误信息:{error},开始清理整个批次,批次ID:{batchId}": "任务 {taskId} 执行失败,错误信息:{error},开始清理整个批次,批次ID:{batchId}", + "修改后台队列任务失败,数据不完整,缺少必要字段": "修改后台队列任务失败,数据不完整,缺少必要字段", + "缺少必要的删除条件,至少需要id、bookId、bookTaskId其中一个": "缺少必要的删除条件,至少需要id、bookId、bookTaskId其中一个", + "任务已提交": "任务已提交", + "任务正在执行中": "任务正在执行中", + "未知的任务类型": "未知的任务类型", + "{taskName}_{taskId} 任务添加调度完成": "{taskName}_{taskId} 任务添加调度完成", + "任务调度失败,请手动重试": "任务调度失败,请手动重试", + "处理 {taskType} 类型任务 {taskName} 失败,失败信息如下,{error}": "处理 {taskType} 类型任务 {taskName} 失败,失败信息如下,{error}", + "启动后台任务成功": "启动后台任务成功", + "启动后台任务失败,{error}": "启动后台任务失败,{error}", + "添加后台任务成功": '添加后台任务成功', + "添加后台任务失败,{error}": "添加后台任务失败,{error}", + "添加多个任务成功": "添加多个任务成功", + "添加多个任务失败,{error}": "添加多个任务失败,{error}", + "获取指定状态的任务成功": "获取指定状态的任务成功", + "获取指定状态的任务失败,{error}": "获取指定状态的任务失败,{error}", + "获取后台任务集合成功": "获取后台任务集合成功", + "获取后台任务集合失败,{error}": "获取后台任务集合失败,{error}", + "修改后台任务成功": "修改后台任务成功", + "修改后台任务失败,{error}": "修改后台任务失败,{error}", + "任务添加成功,任务名称:{taskName}": "任务添加成功,任务名称:{taskName}", + "任务添加失败,失败信息如下:{error}": "任务添加失败,失败信息如下:{error}", + '未找到对应ID的任务,任务ID:{taskId}': '未找到对应ID的任务,任务ID:{taskId}', + //#endregion + + //#region 加载 + "正在初始化系统...": "正在初始化系统...", + "加载授权信息配置...": "加载授权信息配置...", + '连接服务器...': '连接服务器...', + '准备应用资源...': '准备应用资源...', + '即将完成...': '即将完成...', + "软件加载完成,即将进入主界面...": '软件加载完成,即将进入主界面...', + '启动任务队列失败': "启动任务队列失败", + '任务队列启动成功': '任务队列启动成功', + '加载失败,请重试...': '加载失败,请重试...', + "错误信息,{error}": "错误信息,{error}", + '加载系统外观设置失败': "加载系统外观设置失败", + "获取机器码失败,请重启软件或者检查对应权限!!": "获取机器码失败,请重启软件或者检查对应权限!!", + "授权验证失败,即将前往授权界面进行授权!": "授权验证失败,即将前往授权界面进行授权!", + "加载授权码配置失败,{error}": "加载授权码配置失败,{error}", + "初始化通用设置失败,{error}": "初始化通用设置失败,{error}", + "初始化MJ通用设置失败,{error}": "初始化MJ通用设置失败,{error}", + "初始化MJ API设置失败,{error}": "初始化MJ API设置失败,{error}", + "初始化MJ生图包设置失败,{error}": "初始化MJ生图包设置失败,{error}", + "初始化MJ代理模式设置失败,{error}": "初始化MJ代理模式设置失败,{error}", + "初始化MJ本地代理模式设置失败,{error}": "初始化MJ本地代理模式设置失败,{error}", + "初始化推理设置失败,{error}": "初始化推理设置失败,{error}", + "初始化SD设置失败,{error}": "初始化SD设置失败,{error}", + "初始化修手/修脸模型设置失败,{error}": "初始化修手/修脸模型设置失败,{error}", + "初始化剪映关键帧设置失败,{error}": "初始化剪映关键帧设置失败,{error}", + "初始化ComfyUI设置失败,{error}": "初始化ComfyUI设置失败,{error}", + "初始化ComfyUI工作流设置失败,{error}": "初始化ComfyUI工作流设置失败,{error}", + "初始化特殊符号字符串失败,{error}": "初始化特殊符号字符串失败,{error}", + //#endregion + + //#region 授权 + '软件授权': "软件授权", + '机器码自动生成': "机器码自动生成", + '授权文档': "授权文档", + '立即前往': "立即前往", + '确认授权': '确认授权', + '授权说明': "授权说明", + '机器码已复制到剪贴板': "机器码已复制到剪贴板", + '授权验证失败': "授权验证失败", + '同步授权信息失败': "同步授权信息失败", + "授权失败,{error}": "授权失败,{error}", + '机器码为空,无法复制': '机器码为空,无法复制', + ",即将前往授权界面进行授权!": ",即将前往授权界面进行授权!", + "机器码校验错误,即将前往授权界面进行授权!": "机器码校验错误,即将前往授权界面进行授权!", + "授权成功,即将进入加载界面!!": "授权成功,即将进入加载界面!!", + "1、需要授权请查看授权文档,按照操作获取对应的授权": "1、需要授权请查看授权文档,按照操作获取对应的授权", + "2、机器码每次启动软件会自动生成,会检测本机的机器码、主板信息、当前登录的用户信息等": "2、机器码每次启动软件会自动生成,会检测本机的机器码、主板信息、当前登录的用户信息等", + //#endregion + + //#region 工具箱 + '图片压缩工具': "图片压缩工具", + '选择图片': "选择图片 ", + '尺寸设置': "尺寸设置", + '压缩设置': "压缩设置", + '输出格式': "输出格式", + '原始图片': "原始图片", + '未选择图片': "未选择图片", + '压缩后图片': "压缩后图片", + '压缩信息': "压缩信息", + '原始大小': "原始大小", + '压缩后大小': "压缩后大小", + '尺寸减少': "尺寸减少", + '压缩比率': "压缩比率", + '下载压缩图片': "下载压缩图片", + '清空重置': "清空重置", + '已清空所有数据': "已清空所有数据", + '刷新数据': "刷新数据", + '导出数据': "导出数据", + '文件类型': "文件类型", + '上传时间': "上传时间", + '文件地址已复制到剪贴板': "文件地址已复制到剪贴板", + '图片名称': "图片名称", + '点击或拖拽图片到此区域上传': "点击或拖拽图片到此区域上传", + '请等待当前文件处理完成': "请等待当前文件处理完成", + '开始上传': "开始上传", + '清空选择': "清空选择", + '上传说明': "上传说明", + '请先选择文件': "请先选择文件", + '请等待当前文件上传完成': "请等待当前文件上传完成", + '无法加载图片': "无法加载图片", + '无法读取文件': "无法读取文件", + '图片上传工具': "图片上传工具", + '工具总数': "工具总数", + '分类数量': "分类数量", + '路由路径未配置': "路由路径未配置", + '外部链接未配置': "外部链接未配置", + '没有找到相关工具': "没有找到相关工具", + '文档中心': "文档中心", + '点击下方按钮在您的默认浏览器中打开文档': "点击下方按钮在您的默认浏览器中打开文档", + '在浏览器中打开文档': "在浏览器中打开文档", + '复制到剪贴板': "复制到剪贴板", + '媒体工具': "媒体工具", + "LaiTool 图床": "LaiTool 图床", + '转换': "转换", + '格式': "格式", + '图片压缩助手': "图片压缩助手", + '压缩': "压缩", + "将图片进行压缩,支持多种图片格式,减小文件大小": "将图片进行压缩,支持多种图片格式,减小文件大小", + "将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接": "将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接", + "搜索工具...": "搜索工具...", + "上传图片到LaiTool图床,获取图片链接": "上传图片到LaiTool图床,获取图片链接", + "上传失败,{error}": "上传失败,{error}", + '未找到机器ID,请重启软件后重试!!': '未找到机器ID,请重启软件后重试!!', + '图片处理完毕,开始上传文件...': '图片处理完毕,开始上传文件...', + "开始处理图片文件...": "开始处理图片文件...", + "文件超出限制,请压缩后上传!!": "文件超出限制,请压缩后上传!!", + "开始上传文件...": "开始上传文件...", + "已选择文件:{fileName}": "已选择文件:{fileName}", + "单日上传次数限制:5": "单日上传次数限制:5", + "文件大小限制:最大 5MB": "文件大小限制:最大 5MB", + "支持的格式:JPG、PNG、JPEG、WebP": "支持的格式:JPG、PNG、JPEG、WebP", + "每次只能上传一个文件,并且上传的文件会留存在服务器,介意请勿用!!!": "每次只能上传一个文件,并且上传的文件会留存在服务器,介意请勿用!!!", + "选择文件后点击 “开始上传” 按钮进行上传": "选择文件后点击 “开始上传” 按钮进行上传", + "支持 JPG、PNG、GIF、WebP 格式,单个文件不超过 5MB": "支持 JPG、PNG、GIF、WebP 格式,单个文件不超过 5MB", + "正在处理中...": "正在处理中...", + "{data} 导出失败,{error}": "{data} 导出失败,{error}", + "{data} 数据导出成功": "{data} 数据导出成功", + "共 {count} 条": "共 {count} 条", + "导出为 {data}": "导出为 {data}", + "已上传图片 {count}": "已上传图片 {count}", + "压缩错误,{error}": "压缩错误,{error}", + "请选择图片文件 (JPG, PNG, WebP)": "请选择图片文件 (JPG, PNG, WebP)", + "减少 {ratio}%,节省 {saved}": "减少 {ratio}%,节省 {saved}", + '等待压缩...': '等待压缩...', + "压缩中...": "压缩中...", + "图片质量:{quality}%": "图片质量:{quality}%", + "最大高度:{height}px": "最大高度:{height}px", + "最大宽度:{width}px": "最大宽度:{width}px", + "支持 JPG、PNG、WebP 格式": "支持 JPG、PNG、WebP 格式", + "快速压缩图片,本地处理,安全可靠": "快速压缩图片,本地处理,安全可靠", + //#endregion + + //#region 设置 + 'AI推理': "AI推理", + '翻译服务': "翻译服务", + "API服务商": "API服务商", + "API令牌": "API令牌", + '推理模型': "推理模型", + '推理模式': "推理模式", + '自定义推理预设': "自定义推理预设", + '翻译设置': "翻译设置", + '翻译模型': "翻译模型", + '编辑自定义预设': "编辑自定义预设", + '编辑当前选中的自定义推理预设': "编辑当前选中的自定义推理预设", + '删除自定义预设': "删除自定义预设", + '删除当前选中的自定义推理预设': "删除当前选中的自定义推理预设", + '编辑自定义推理预设': "编辑自定义推理预设", + '确定删除': "确定删除", + '未找到要删除的预设': "未找到要删除的预设", + '未知的操作类型': "未知的操作类型", + '设置加载成功': "设置加载成功", + '未知的测试类型': "未知的测试类型", + '更新预设': "更新预设", + '保存预设': "保存预设", + '预设配置': "预设配置", + '系统提示词': "系统提示词", + '用户提示词': "用户提示词", + '占位符测试': "占位符测试", + '清空值': "清空值", + '测试提示词': "测试提示词", + '检测到的占位符': "检测到的占位符", + '占位符值设置': "占位符值设置", + '测试状态': "测试状态", + '角色信息内容': "角色信息内容", + '场景信息内容': "场景信息内容", + '上下文内容': "上下文内容", + '没有选择API服务商': "没有选择API服务商", + '无效的API服务商': "无效的API服务商", + '没有设置推理模型': "没有设置推理模型", + '占位符值已清空': "占位符值已清空", + '暂无预览内容': "暂无预览内容", + '测试结果预览': "测试结果预览", + '新增预设': "新增预设", + "编辑预设": "编辑预设", + '当前修改的预设不存在': "当前修改的预设不存在", + '预设ID不存在': "预设ID不存在", + '预设不存在': "预设不存在", + '未找到对应的预设数据': '未找到对应的预设数据', + '修改MJ账号': "修改MJ账号", + '添加MJ账号': "添加MJ账号", + '服务器ID': "服务器ID", + '频道ID': "频道ID", + 'MJ私信ID': "MJ私信ID", + 'Niji私信ID': "Niji私信ID", + "并发/队列": "并发/队列", + '用户token': "用户token", + '账号并发数': "账号并发数", + '等待队列': "等待队列", + '任务超时时间': "任务超时时间", + '是否启用': "是否启用", + '用户Agent': "用户Agent", + '本地代理模式的请求地址不能为空': "本地代理模式的请求地址不能为空", + '本地代理模式的访问令牌不能为空': "本地代理模式的访问令牌不能为空", + '修改不能没有账号实例ID': "修改不能没有账号实例ID", + '网络请求失败': "网络请求失败", + '账号修改成功': "账号修改成功", + '账号创建成功': "账号创建成功", + '速度模式': "速度模式", + '注意事项': "注意事项", + "API设置加载成功": "API设置加载成功", + '请求地址': "请求地址", + '访问令牌': "访问令牌", + '新增账号': "新增账号", + '同步账号': "同步账号", + '同步服务器中的账号信息到本地': "同步服务器中的账号信息到本地", + '账号列表': "账号列表", + '全量代理模式部署': "全量代理模式部署", + '编辑当前账号信息': "编辑当前账号信息", + '删除本地账号': "删除本地账号", + '删除服务账号': "删除服务账号", + '确认同步': "确认同步", + '请求服务器账号列表失败': "请求服务器账号列表失败", + '远程服务器返回数据格式错误': "远程服务器返回数据格式错误", + '本地代理模式设置加载成功': "本地代理模式设置加载成功", + '本地代理模式设置保存成功': "本地代理模式设置保存成功", + '生图包设置': "生图包设置", + '生图包服务商': "生图包服务商", + '跳转到购买页面': "跳转到购买页面", + '已打开查询页面': "已打开查询页面", + '生图包设置加载成功': "生图包设置加载成功", + '生图包设置保存成功': "生图包设置保存成功", + '代理模式设置': "代理模式设置", + '是否国内转发': "是否国内转发", + '账号删除成功': "账号删除成功", + '同步账号信息成功': "同步账号信息成功", + '代理模式设置加载成功': "代理模式设置加载成功", + '代理模式设置保存成功': "代理模式设置保存成功", + '出图方式': "出图方式", + '查看教程': "查看教程", + '生图机器人': "生图机器人", + '机器人模型': "机器人模型", + '生图比例': "生图比例", + '命令后缀': "命令后缀", + '功能未开放': "功能未开放", + '保存设置': "保存设置", + '执行并发': "执行并发", + '暂无该模式的教程': "暂无该模式的教程", + '外观设置': "外观设置", + '主题颜色': "主题颜色", + '工作流名称': "工作流名称", + '工作流文件': "工作流文件", + '检查工作流文件': "检查工作流文件", + '更新成功': "更新成功", + '未选择任何文件': "未选择任何文件", + '正向提示词': "正向提示词", + '反向提示词': "反向提示词", + '工作流文件检查成功通过': "工作流文件检查成功通过", + "ComfyUI 基础设置": "ComfyUI 基础设置", + '使用工作流': "使用工作流", + '获取ComfyUI通用设置失败': "获取ComfyUI通用设置失败", + '获取ComfyUI工作流设置失败': "获取ComfyUI工作流设置失败", + '添加工作流': "添加工作流", + '编辑工作流': "编辑工作流", + '删除成功': "删除成功", + '剪映草稿目录': "剪映草稿目录", + '手动选择剪映草稿地址': "手动选择剪映草稿地址", + '自动获取剪映草稿地址': "自动获取剪映草稿地址", + '打开当前选择的剪映草稿文件夹': "打开当前选择的剪映草稿文件夹", + '项目文件夹': "项目文件夹", + '手动选择项目文件夹': "手动选择项目文件夹", + '打开当前选择的项目文件夹': "打开当前选择的项目文件夹", + '任务并发数': "任务并发数", + '默认生图方式': "默认生图方式", + '高清倍数': "高清倍数", + '默认图转视频方式': "默认图转视频方式", + '语言': "语言", + '项目文件复制成功': "项目文件复制成功", + "左右关键帧": "左右关键帧", + "上下关键帧": "上下关键帧", + "缩放关键帧": "缩放关键帧", + "随机关键帧": "随机关键帧", + '打帧方式': "打帧方式", + '匀速关键帧': "匀速关键帧", + '匀速关键帧时间': "匀速关键帧时间", + '上下关键帧设置': "上下关键帧设置", + '上关键帧位置': "上关键帧位置", + '下关键帧位置': "下关键帧位置", + '缩放大小': "缩放大小", + '左右关键帧设置': "左右关键帧设置", + '左关键帧位置': "左关键帧位置", + '右关键帧位置': "右关键帧位置", + '缩放关键帧设置': "缩放关键帧设置", + '开始缩放大小': "开始缩放大小", + '结束缩放大小': "结束缩放大小", + "SD 基础设置": "SD 基础设置", + '加载数据': "加载数据", + '单次出图张数': "单次出图张数", + '种子值': "种子值", + '重绘幅度': "重绘幅度", + '采样方式': "采样方式", + '迭代步数': "迭代步数", + '图片分辨率': "图片分辨率", + '已启用的模型': "已启用的模型", + '添加模型': "添加模型", + '重置设置': "重置设置", + '文生图': "文生图", + '图生图': "图生图", + '识别信任度': "识别信任度", + 'SD远程数据加载成功': "SD远程数据加载成功", + '请先保存或取消当前编辑的模型': "请先保存或取消当前编辑的模型", + '重置模型成功': "重置模型成功", + '开发者微信': "开发者微信", + '二维码图片': "二维码图片", + '请联系管理员获取': "请联系管理员获取", + '联系须知': "联系须知", + "加入VIP用户交流群": "加入VIP用户交流群", + '群内福利': "群内福利", + '获取最新版本信息和更新通知': "获取最新版本信息和更新通知", + '与其他用户交流使用技巧和经验': "与其他用户交流使用技巧和经验", + '快速反馈问题和建议': "快速反馈问题和建议", + '获得开发团队的技术支持': "获得开发团队的技术支持", + '联系管理员邀请入群或者联系对应代理': "联系管理员邀请入群或者联系对应代理", + "注:请勿在群内发布广告或无关内容,保持良好交流氛围": "注:请勿在群内发布广告或无关内容,保持良好交流氛围", + "添加时请备注:LAITool用户": "添加时请备注:LAITool用户", + "工作时间:周一至周五 9:00-18:00": "工作时间:周一至周五 9:00-18:00", + "支持技术咨询、BUG反馈、功能建议": "支持技术咨询、BUG反馈、功能建议", + "请详细描述您遇到的问题,以便快速解决": "请详细描述您遇到的问题,以便快速解决", + "如无法扫码,可复制微信号:": "如无法扫码,可复制微信号:", + "或发送邮件至:": "或发送邮件至:", + '微信号已复制到剪贴板': '微信号已复制到剪贴板', + "扫描下方二维码添加开发者微信,获取专业技术支持": "扫描下方二维码添加开发者微信,获取专业技术支持", + '识别信任度必须在0到1之间': "识别信任度必须在0到1之间", + "删除模型成功: {model}": "删除模型成功: {model}", + "更新模型成功,模型 {model}": "更新模型成功,模型 {model}", + "添加模型成功,模型名称 {name}": "添加模型成功,模型名称 {name}", + "SD远程数据加载失败,{error}": "SD远程数据加载失败,{error}", + '请输入 图片高度': '请输入 图片高度', + "请输入 图片宽度": "请输入 图片宽度", + "修脸/修手": "修脸/修手", + "请选择是否开始修脸/修手": "请选择是否开始修脸/修手", + "ADetailer 模型设置": "ADetailer 模型设置", + "请完善所有的关键帧设置!!": '请完善所有的关键帧设置!!', + "LAI API - 香港": "LAI API - 香港", + 'LAI API - 美国': 'LAI API - 美国', + 'LaiTool生图包': 'LaiTool生图包', + "没有找到对应的API的配置,请先检查配置": "没有找到对应的API的配置,请先检查配置", + "API模式": "API模式", + '代理模式': "代理模式", + '本地代理模式(自有账号推荐)': '本地代理模式(自有账号推荐)', + '快速': "快速", + '慢速': "慢速", + "复制文件夹成功!": "复制文件夹成功!", + "复制文件夹失败,{error}": "复制文件夹失败,{error}", + "获取 {description} 失败,{error}": "获取 {description} 失败,{error}", + "获取剪映草稿文件列表成功!": "获取剪映草稿文件列表成功!", + "获取剪映草稿文件列表失败,{error}": "获取剪映草稿文件列表失败,{error}", + "依赖的草稿文件不存在,请检查": "依赖的草稿文件不存在,请检查", + "剪映草稿文件数据文件不存在,请先检查": "剪映草稿文件数据文件不存在,请先检查", + "剪映草稿文件格式错误,请检查": "剪映草稿文件格式错误,请检查", + "剪映草稿文件格式错误,没有轨道,请检查": "剪映草稿文件格式错误,没有轨道,请检查", + "剪映草稿文件格式错误,主轨道不是Video,请检查": "剪映草稿文件格式错误,主轨道不是Video,请检查", + "剪映草稿文件格式错误,主轨道没有对应的图片节点,请检查": "剪映草稿文件格式错误,主轨道没有对应的图片节点,请检查", + "当前新增的任务对应的图片数量和依赖的草稿的主轨道的图片数量不一致,请检查": "当前新增的任务对应的图片数量和依赖的草稿的主轨道的图片数量不一致,请检查", + "图片数量不对应,请检查": "图片数量不对应,请检查", + "草稿文件不存在,请检查": '草稿文件不存在,请检查', + "没有找到对应的节点": "没有找到对应的节点", + "未找到剪映相关数据,请手动填写或选择": "未找到剪映相关数据,请手动填写或选择", + "剪映草稿地址数据错误,请手动填写或选择": "剪映草稿地址数据错误,请手动填写或选择", + "获取默认剪映草稿地址失败,{error}": "获取默认剪映草稿地址失败,{error}", + "勾选匀速关键帧,当分镜时间小于设置的时间时,会按照时间的比例来调整关键帧位置,保证关键帧的匀速效果。": "勾选匀速关键帧,当分镜时间小于设置的时间时,会按照时间的比例来调整关键帧位置,保证关键帧的匀速效果。", + '已取消修改,恢复原始值,请重新手动保存!': '已取消修改,恢复原始值,请重新手动保存!', + "复制项目文件失败,{error}": "复制项目文件失败,{error}", + "正在复制项目文件...": "正在复制项目文件...", + "检测当前的项目地址已修改,继续执行会先复制当前所有的项目文件,请确保当前的文件没有被占用和软件无后台任务执行,若是执行失败,则需手动复制就项目文件夹中的所有数据到新项目文件夹位置,是否继续?": "检测当前的项目地址已修改,继续执行会先复制当前所有的项目文件,请确保当前的文件没有被占用和软件无后台任务执行,若是执行失败,则需手动复制就项目文件夹中的所有数据到新项目文件夹位置,是否继续?", + "打开文件夹失败,文件夹地址为空": "打开文件夹失败,文件夹地址为空", + "自动获取剪映草稿地址失败,{error}": "自动获取剪映草稿地址失败,{error}", + "删除失败,{error}": "删除失败,{error}", + "删除失败: 未找到要删除的工作流": "删除失败: 未找到要删除的工作流", + "确定要删除工作流 “{name}” 吗?此操作不可撤销。是否继续?": "确定要删除工作流 “{name}” 吗?此操作不可撤销。是否继续?", + "当前选中的工作流不存在,请重新选择": "当前选中的工作流不存在,请重新选择", + "请求地址必须以 http 或者 https 开头": "请求地址必须以 http 或者 https 开头", + "初始化设置失败,{error}": "初始化设置失败,{error}", + "3 图像输出节点必须是 保存图像 节点,采样器只支持简单 K采样器和K采样器(高级)": "3 图像输出节点必须是 保存图像 节点,采样器只支持简单 K采样器和K采样器(高级)", + "2. 标题必须对应 正向提示词和反向提示词": "2. 标题必须对应 正向提示词和反向提示词", + "1. Comfy UI的工作流中正向提示词和反向提示必须为 Clip文本编码 节点": "1. Comfy UI的工作流中正向提示词和反向提示必须为 Clip文本编码 节点", + "工作流文件缺少正向提示词或反向提示词,请检查工作流文件,把对应的文本编码模块的标题改为正向提示词和反向提示词!!": "工作流文件缺少正向提示词或反向提示词,请检查工作流文件,把对应的文本编码模块的标题改为正向提示词和反向提示词!!", + "工作流文件的格式不正确,请检查工作流文件": "工作流文件的格式不正确,请检查工作流文件", + "工作流文件内容为空,请选择工作流文件": "工作流文件内容为空,请选择工作流文件", + "读取的文件内容为空,请检查文件": "读取的文件内容为空,请检查文件", + '读取文件成功!': '读取文件成功!', + "读取文件失败,{error}": "读取文件失败,{error}", + "{type}失败,{error}": "{type}失败,{error}", + "工作流名称已存在,请重新输入": "工作流名称已存在,请重新输入", + "加载设置失败,{error}": "加载设置失败,{error}", + "间隔时间(秒)": "间隔时间(秒)", + "打开不同的出图方法的教程,根据选择的出图方式打开对应的教程": "打开不同的出图方法的教程,根据选择的出图方式打开对应的教程", + "保存代理模式设置失败,{error}": "保存代理模式设置失败,{error}", + "加载代理模式设置失败,{error}": "加载代理模式设置失败,{error}", + "更改代理模式配置,ID不能为空": "更改代理模式配置,ID不能为空", + "代理模式的账号ID,服务ID,频道ID,用户Token不能为空": "代理模式的账号ID,服务ID,频道ID,用户Token不能为空", + "开始同步远程账号信息...": "开始同步远程账号信息...", + "此操作将从远程代理服务器同步账号信息,会覆盖本地现有的账号配置。确定要继续吗?": "此操作将从远程代理服务器同步账号信息,会覆盖本地现有的账号配置。确定要继续吗?", + "删除账号失败,{error}": "删除账号失败,{error}", + "确定要删除服务器ID为 “{guildId}” 的账号吗?此操作不可撤销。": "确定要删除服务器ID为 “{guildId}” 的账号吗?此操作不可撤销。", + "3. 开启 “国内转发” 选项可解决部分地区(如河南、福建等)的网络访问问题": "3. 开启 “国内转发” 选项可解决部分地区(如河南、福建等)的网络访问问题", + "2. 通过 “新增账号” 可添加多个MJ账号,实现并行处理,提高效率": "2. 通过 “新增账号” 可添加多个MJ账号,实现并行处理,提高效率", + "1. 日常使用无需开启代理,仅添加账号时需要网络代理": "1. 日常使用无需开启代理,仅添加账号时需要网络代理", + "保存生图包设置失败,{error}": "保存生图包设置失败,{error}", + "加载生图包设置失败,{error}": "加载生图包设置失败,{error}", + "该生图包不支持查询,请联系管理员": "该生图包不支持查询,请联系管理员", + "3. 出图稳定,采用官方接口,不会封号,保障长期稳定使用": "3. 出图稳定,采用官方接口,不会封号,保障长期稳定使用", + "2. 支持 定制套餐,灵活的套餐选择,可根据使用频率和需求定制专属套餐方案": "2. 支持 定制套餐,灵活的套餐选择,可根据使用频率和需求定制专属套餐方案", + "1. 使用 无需科学上网,全球加速访问!延迟 30ms 以内,国内外用户均可稳定使用": "1. 使用 无需科学上网,全球加速访问!延迟 30ms 以内,国内外用户均可稳定使用", + "保存本地代理模式设置失败,{error}": "保存本地代理模式设置失败,{error}", + "加载本地代理模式设置失败,{error}": "加载本地代理模式设置失败,{error}", + "未找到对应的账号,无法更新": "未找到对应的账号,无法更新", + "本地代理模式的账号ID,服务ID,频道ID,用户Token不能为空": "本地代理模式的账号ID,服务ID,频道ID,用户Token不能为空", + "更改本地代理模式配置,ID不能为空": "更改本地代理模式配置,ID不能为空", + "添加失败,{error}": "添加失败,{error}", + "本地代理模式的频道ID,服务器ID,用户Token必填": "本地代理模式的频道ID,服务器ID,用户Token必填", + "代理模式的频道ID,服务器ID,用户Token必填": "代理模式的频道ID,服务器ID,用户Token必填", + "确定要删除服务器ID为 “{guildId}” 的本地账号吗?此操作不会影响服务器账号,只会删除本地记录。": "确定要删除服务器ID为 “{guildId}” 的本地账号吗?此操作不会影响服务器账号,只会删除本地记录。", + "无法删除,账号信息无效": "无法删除,账号信息无效", + "删除本地账号失败,{error}": "删除本地账号失败,{error}", + "未找到对应的账号,无法删除": "未找到对应的账号,无法删除", + "打开编辑账号失败,{error}": "打开编辑账号失败,{error}", + "同步账号失败,{error}": "同步账号失败,{error}", + "同步完成!已同步 {count} 个账号": "同步完成!已同步 {count} 个账号", + "没有配置本地代理模式的基本信息,请检查请求地址和访问令牌": "没有配置本地代理模式的基本信息,请检查请求地址和访问令牌", + "确定要从服务器同步账号信息到本地吗?此操作会覆盖本地的账号列表,是否继续?": "确定要从服务器同步账号信息到本地吗?此操作会覆盖本地的账号列表,是否继续?", + "打开新增账号失败,{error}": "打开新增账号失败,{error}", + "删除服务账号信息,会同步删除本地账号信息": "删除服务账号信息,会同步删除本地账号信息", + "删除本地账号信息,对服务器信息不会有影响": "删除本地账号信息,对服务器信息不会有影响", + "3. 通过账号管理功能可添加多个MJ账号,实现并行处理,提高效率": "3. 通过账号管理功能可添加多个MJ账号,实现并行处理,提高效率", + "2. 访问令牌默认为 admin,如有修改请相应更新": "2. 访问令牌默认为 admin,如有修改请相应更新", + "1. 本地代理模式支持本地部署和服务器部署,需要自己搭建部署,": "1. 本地代理模式支持本地部署和服务器部署,需要自己搭建部署,", + "添加一个新的账号": "添加一个新的账号", + "本地代理模式设置-自行部署服务": "本地代理模式设置-自行部署服务", + "加载API设置失败,{error}": "加载API设置失败,{error}", + "Just My Socks网络加速服务": "Just My Socks网络加速服务", + "确保网络环境稳定,以保证服务正常运行,推荐稳定🪜:": "确保网络环境稳定,以保证服务正常运行,推荐稳定🪜:", + "4. 开启 “国内转发” 选项可解决部分地区(如河南、福建等)的网络访问问题": "4. 开启 “国内转发” 选项可解决部分地区(如河南、福建等)的网络访问问题", + "3. 支持 20并发请求,可同时处理多张图片生成任务,大大提高工作效率": "3. 支持 20并发请求,可同时处理多张图片生成任务,大大提高工作效率", + "2. 提供 快速慢速 两种出图方式,可根据需求选择": '2. 提供 快速慢速 两种出图方式,可根据需求选择', + "1. 使用 无需科学上网,支持香港和美国节点,香港节点对大陆做了优化,延迟 100ms 以内": "1. 使用 无需科学上网,支持香港和美国节点,香港节点对大陆做了优化,延迟 100ms 以内", + "必填字段核心线程数,队列大小,超时时间不能为空": "必填字段核心线程数,队列大小,超时时间不能为空", + "必填字段服务器ID,频道ID,用户token不能为空": "必填字段服务器ID,频道ID,用户token不能为空", + "获取自定义预设失败,{error}": "获取自定义预设失败,{error}", + "预设名称已存在,请更换一个名称": "预设名称已存在,请更换一个名称", + "未添加角色占位符,不能勾选 “必须包含角色信息”": "未添加角色占位符,不能勾选 “必须包含角色信息”", + "打开预览失败,{error}": "打开预览失败,{error}", + "AI 回复结果": "AI 回复结果", + "没有设置API Token": "没有设置API Token", + "提示词预设测试失败,{error}": "提示词预设测试失败,{error}", + "测试完成,请在预览下面查看提示词": "测试完成,请在预览下面查看提示词", + "提示词预览已生成,开始测试连接": "提示词预览已生成,开始测试连接", + "占位符 {{placeholder}} 的值无效": "占位符 {{placeholder}} 的值无效", + "当前文本内容(分镜文案)": "当前文本内容(分镜文案)", + "提示词测试完成,点击右上角 “预览” 查看详细内容": "提示词测试完成,点击右上角 “预览” 查看详细内容", + "在提示词中使用占位符,这里会自动检测并显示": "在提示词中使用占位符,这里会自动检测并显示", + "请输入用户提示词,支持占位符(如 {textContent})": "请输入用户提示词,支持占位符(如 {textContent})", + "请输入系统提示词,支持占位符(如 {textContent})": "请输入系统提示词,支持占位符(如 {textContent})", + "必须包含角色信息(检测角色分析)": "必须包含角色信息(检测角色分析)", + "可用占位符-软件会自动替换对应的占位符为实际文本": "可用占位符-软件会自动替换对应的占位符为实际文本", + "确定要删除预设 “{name}” 吗?此操作不可撤销。": "确定要删除预设 “{name}” 吗?此操作不可撤销。", + "连接成功!{serverName} 运行正常": "连接成功!{serverName} 运行正常", + "测试链接失败,{error}": "测试链接失败,{error}", + "正在测试链接...": "正在测试链接...", + "API服务商未选择或服务地址无效,请检查设置": "API服务商未选择或服务地址无效,请检查设置", + "初始化数据失败,{error}": "初始化数据失败,{error}", + "【自定义】": "【自定义】", + "获取自定义推理预设失败,{error}": "获取自定义推理预设失败,{error}", + "获取设置失败,{error}": "获取设置失败,{error}", + "正在加载设置...": "正在加载设置...", + "删除预设失败,{error}": "删除预设失败,{error}", + "自定义推理预设删除成功!": "自定义推理预设删除成功!", + "确定要删除自定义推理预设 {name} 吗?此操作不可撤销。": "确定要删除自定义推理预设 {name} 吗?此操作不可撤销。", + "打开编辑预设失败,{error}": "打开编辑预设失败,{error}", + "打开自定义推理预设失败,{error}": "打开自定义推理预设失败,{error}", + "自定义推理预设保存成功!": "自定义推理预设保存成功!", + "购买链接不存在,请联系管理员": "购买链接不存在,请联系管理员", + "测试 翻译 链接": "测试 翻译 链接", + "测试 AI 链接": "测试 AI 链接", + "AI 设置": "AI 设置", + '设置 -> 推理设置': '设置 -> 推理设置', + '设置 -> 通用设置': '设置 -> 通用设置', + "设置 -> MJ设置": "设置 -> MJ设置", + "设置 -> MJ设置 -> 生图包模式": "设置 -> MJ设置 -> 生图包模式", + "设置 -> MJ设置 -> 本地代理模式": "设置 -> MJ设置 -> 本地代理模式", + "设置 -> MJ设置 -> API模式": "设置 -> MJ设置 -> API模式", + "设置 -> MJ设置 -> 代理模式": "设置 -> MJ设置 -> 代理模式", + '设置 -> 分镜AI模型': '设置 -> 分镜AI模型', + '设置 -> SD设置': '设置 -> SD设置', + "设置 -> ComfyUI 设置": "设置 -> ComfyUI 设置", + "设置-> 通用设置 -> 项目地址": "设置-> 通用设置 -> 项目地址", + "文案处理->设置": "文案处理->设置", + '未找到设置,请检查 {setting} 设置!': '未找到设置,请检查 {setting} 设置!', + "请检查 ‘{path}’ 的API提供商、API令牌、推理模型、推理模式等是不是存在!": "请检查 ‘{path}’ 的API提供商、API令牌、推理模型、推理模式等是不是存在!", + "对应Midjourney模式的Token不能为空,请前往 {settingPath} 中配置": "对应Midjourney模式的Token不能为空,请前往 {settingPath} 中配置", + "请检查推理模型是否存在!": "请检查推理模型是否存在!", + "当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!": "当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!", + "未找到对应的分镜预设的请求数据,请检查": "未找到对应的分镜预设的请求数据,请检查", + "背景音乐文件夹或文件不存在,请检查": "背景音乐文件夹或文件不存在,请检查", + "背景音乐文件夹下面未存在有效的音频文件": "背景音乐文件夹下面未存在有效的音频文件", + "字幕时间信息不完整": "字幕时间信息不完整", + "字幕内容信息不完整": "字幕内容信息不完整", + "初始视频消息成功!": "初始视频消息成功!", + "获取成功 OptionKey: {key}": "获取成功 OptionKey: {key}", + "获取失败 OptionKey: {key},失败原因:{error}": "获取失败 OptionKey: {key},失败原因:{error}", + "修改成功 OptionKey: {key}": "修改成功 OptionKey: {key}", + "修改失败 OptionKey: {key},失败原因:{error}": "修改失败 OptionKey: {key},失败原因:{error}", + "请到 “{checkString}” 检查设置!": "请到 “{checkString}” 检查设置!", + "当前值为空!": "当前值为空!", + "未找到选项对象,请检查所有的选项设置是否存在!": "未找到选项对象,请检查所有的选项设置是否存在!", + "设置数据不完整,请检查提示词类型,提示词预设数据是否完整": "设置数据不完整,请检查提示词类型,提示词预设数据是否完整", + "没有找到需要处理的文案ID对应的数据,请检查数据是否正确": "没有找到需要处理的文案ID对应的数据,请检查数据是否正确", + "文案处理API设置不完整,请检查API地址,密钥和模型是否设置正确": '文案处理API设置不完整,请检查API地址,密钥和模型是否设置正确', + "文案生成成功": "文案生成成功", + "没有需要处理的文案ID": "没有需要处理的文案ID", + "AI处理文案成功": "AI处理文案成功", + "AI处理文案失败,{error}": "AI处理文案失败,{error}", + "检测系统当前的语言已修改,继续执行会刷新当前页面并应用当前语言,是否继续?": "检测系统当前的语言已修改,继续执行会刷新当前页面并应用当前语言,是否继续?", + //#endregion + + //#region 预设 + "风格预设": "风格预设", + '人物预设': "人物预设", + '场景预设': "场景预设", + '新建预设': "新建预设", + '正在初始化预设库模块': "正在初始化预设库模块", + '释放添加图片': '释放添加图片', + '点击或拖拽添加图片': '点击或拖拽添加图片', + '预设名称': '预设名称', + '请输入预设名称': '请输入预设名称', + '预设分类': '预设分类', + '请选择预设分类': '请选择预设分类', + '请先选择预设分类': '请先选择预设分类', + '请在上方选择预设分类以显示对应配置选项': '请在上方选择预设分类以显示对应配置选项', + '没有选择任何文件': '没有选择任何文件', + '输入别名或标签后按Enter添加': '输入别名或标签后按Enter添加', + '显示': '显示', + '不显示': '不显示', + '是否显示': '是否显示', + '风格提示词': '风格提示词', + '人物提示词': '人物提示词', + '请输入人物垫图链接': '请输入人物垫图链接', + '请输入人物权重值': '请输入人物权重值', + '请选择Lora模型': '请选择Lora模型', + 'Lora权重': 'Lora权重', + '修改预设显示状态成功': '修改预设显示状态成功', + '删除预设成功': '删除预设成功', + '选择分类': '选择分类', + '选择标签': '选择标签', + '搜索预设数据成功': '搜索预设数据成功', + '选择合适的风格可以影响最终生成图像的整体效果': '选择合适的风格可以影响最终生成图像的整体效果', + '已选风格': '已选风格', + '请输入风格垫图链接': '请输入风格垫图链接', + '请输入风格垫图权重': "请输入风格垫图权重", + '未找到指定的预设数据': "未找到指定的预设数据", + "不支持的合并类型": "不支持的合并类型", + "删除预设成功!": "删除预设成功!", + "修改预设成功!": "修改预设成功!", + "修改预设失败,{error}": "修改预设失败,{error}", + "添加预设成功!": "添加预设成功!", + "添加预设失败,{error}": "添加预设失败,{error}", + "获取预设成功!": "获取预设成功!", + "获取预设失败,{error}": "获取预设失败,{error}", + "图片格式不合法!只支持 png、jpg、webp 格式的图片!": "图片格式不合法!只支持 png、jpg、webp 格式的图片!", + "获取预设列表成功!": "获取预设列表成功!", + "获取预设列表失败,{error}": "获取预设列表失败,{error}", + "加载预设列表...": "加载预设列表...", + "sw 值(0-1000)": "sw 值(0-1000)", + 'MJ:风格垫图链接(sref)': 'MJ:风格垫图链接(sref)', + '批量设置选中的风格会覆盖原有的风格,覆盖结果不可恢复': '批量设置选中的风格会覆盖原有的风格,覆盖结果不可恢复', + '3. 若风格仅应用于部分分镜,视为个性化设置,此处不会默认选中': '3. 若风格仅应用于部分分镜,视为个性化设置,此处不会默认选中', + '2. 初始选中项为所有分镜共同使用的风格': '2. 初始选中项为所有分镜共同使用的风格', + '1. 此处仅显示预设库中已勾选“显示”的风格预设': '1. 此处仅显示预设库中已勾选“显示”的风格预设', + '搜索预设数据失败,{error}': '搜索预设数据失败,{error}', + '正在搜索预设数据...': '正在搜索预设数据...', + '搜索预设名称...': '搜索预设名称...', + '确定要删除此预设吗?删除之后将无法恢复。请谨慎操作。': + '确定要删除此预设吗?删除之后将无法恢复。请谨慎操作。', + '修改预设显示状态失败,{error} ': '修改预设显示状态失败,{error} ', + '获取Lora模型列表失败,请到 “{path}” 中加载远程Lora模型列表!': '获取Lora模型列表失败,请到 “{path}” 中加载远程Lora模型列表!', + '没有可用的Lora模型,请到 “{path}” 中加载远程Lora模型列表!': '没有可用的Lora模型,请到 “{path}” 中加载远程Lora模型列表!', + 'SD:Lora选择': 'SD:Lora选择', + 'cw 值(0-100)': 'cw 值(0-100)', + 'MJ:人物图片链接(cref)': 'MJ:人物图片链接(cref)', + '别名/标签': '别名/标签', + "新增预设成功,预设名称 '{name}'": "新增预设成功,预设名称 '{name}'", + "修改预设成功,预设名称 '{name}'": "修改预设成功,预设名称 '{name}'", + '预设分类不能为空!': '预设分类不能为空!', + '预设名称不能为空!': '预设名称不能为空!', + "预设ID不能为空!": "预设ID不能为空!", + '是否取消当前操作?继续执行会丢失当前操作的所有数据,请谨慎操作!': '是否取消当前操作?继续执行会丢失当前操作的所有数据,请谨慎操作!', + '选择文件失败,{error}': '选择文件失败,{error}', + "选择文件夹失败,{error}": "选择文件夹失败,{error}", + "在相同类型下已存在当前的预设名字,请修改后再试!": "在相同类型下已存在当前的预设名字,请修改后再试!", + //#endregion + + //#region 原创 + '场景推理': '场景推理', + '手动添加': '手动添加', + '导入到自定义场景预设': '导入到自定义场景预设', + '删除选中': '删除选中', + '编辑数据': '编辑数据', + '角色名称': '角色名称', + '请输入角色名称': '请输入角色名称', + '角色提示词': '角色提示词', + '请输入角色提示词': '请输入角色提示词', + '场景名称': '场景名称', + '请输入场景名称': '请输入场景名称', + '场景提示词': '场景提示词', + '请输入场景提示词': '请输入场景提示词', + '场景推理成功': '场景推理成功', + '角色推理成功': '角色推理成功', + '修改成功': '修改成功', + '请先选择要删除的行': '请先选择要删除的行', + '请先选择要导出的行': '请先选择要导出的行', + '导入所有选中成功的场景设置到预设中完成': '导入所有选中成功的场景设置到预设中完成', + '导入所有选中成功的人物角色到预设中完成': '导入所有选中成功的人物角色到预设中完成', + '角色推理': '角色推理', + '导入到自定义角色预设': '导入到自定义角色预设', + '定位到对应行': '定位到对应行', + '已定位到对应分镜': '已定位到对应分镜', + '提示词命令': '提示词命令', + '合成视频': '合成视频', + '无效的ID': '无效的ID', + '未找到指定ID的分镜数据': '未找到指定ID的分镜数据', + '主图操作': '主图操作', + '上传图片': '上传图片', + '下载图片': '下载图片', + '高清': '高清', + '打开文件夹': '打开文件夹', + '打开文件夹成功': '打开文件夹成功', + "打开文件/文件夹成功": '打开文件/文件夹成功', + "打卡开文件/文件夹失败,{error}": '打开文件/文件夹失败,{error}', + "没有选择的文件或文件夹": '没有选择的文件或文件夹', + "选择文件/文件夹成功": '选择文件/文件夹成功', + "选择文件/文件夹失败,{error}": '选择文件/文件夹失败,{error}', + "选择文件夹": "选择文件夹", + "选择类型": "选择类型", + "请选择要选择的类型:": "请选择要选择的类型:", + '子图操作': '子图操作', + '上传图片成功': '上传图片成功', + '下载图片失败': '下载图片失败', + '合并生图提示词的排序顺序': '合并生图提示词的排序顺序', + '修改排序顺序的按钮': '修改排序顺序的按钮', + '提示词排序顺序格式不正确': '提示词排序顺序格式不正确', + '修改提示词排序顺序': '修改提示词排序顺序', + '选择当前的prompt排序方式': '选择当前的prompt排序方式', + '图片描述': '图片描述', + '选择完成': '选择完成', + '加载数据中': '加载数据中', + '图片只能拖拽到主图片上': '图片只能拖拽到主图片上', + '单句生图': '单句生图', + '生成当前分镜图片': '生成当前分镜图片', + '下生图': '下生图', + '锁定图片': '锁定图片', + '清空图片': '清空图片', + '导入图片': '导入图片', + '单句推理': '单句推理', + '下推理': '下推理', + '可以导入网络图片或者是本地图片地址': '可以导入网络图片或者是本地图片地址', + '本地图片地址无效或图片不存在': '本地图片地址无效或图片不存在', + '导入图片成功': '导入图片成功', + '请生成或推理提示词': '请生成或推理提示词', + '推理当前分镜的提示词': '推理当前分镜的提示词', + '推理当前及后续所有分镜的提示词': '推理当前及后续所有分镜的提示词', + '翻译提示词语言': '翻译提示词语言', + '命令预览': '命令预览', + '显示完整的提示词命令预览': '显示完整的提示词命令预览', + '未知的翻译类型': '未知的翻译类型', + '没有需要翻译的数据': '没有需要翻译的数据', + '翻译完成': '翻译完成', + '没有返回的分镜ID和数据信息': '没有返回的分镜ID和数据信息', + '没有找到对应的分镜ID的数据': '没有找到对应的分镜ID的数据', + '完整命令预览': '完整命令预览', + '推理完成': '推理完成', + '批量风格': '批量风格', + '出图': '出图', + '暂不支持的出图方式': '暂不支持的出图方式', + '一键锁定': '一键锁定', + '一键锁定图片提示': '一键锁定图片提示', + '一键解锁图片提示': '一键解锁图片提示', + '一键解锁': '一键解锁', + '一键锁定图片成功': '一键锁定图片成功', + '一键解锁图片成功': '一键解锁图片成功', + 'MJ采集全部图片': 'MJ采集全部图片', + '一拆四': '一拆四', + '应用当前': '应用当前', + '请选择需要添加的标签': '请选择需要添加的标签', + '传入的数据中没有找到对应的数据': '传入的数据中没有找到对应的数据', + 'tag传入的数据格式不正确': 'tag传入的数据格式不正确', + '界面显示修改完成': '界面显示修改完成', + '选择文件': '选择文件', + '背景音乐': '背景音乐', + '剪映草稿模板': '剪映草稿模板', + '选择剪映草稿模板': '选择剪映草稿模板 ', + '保存配置': '保存配置', + '生成剪映草稿': '生成剪映草稿', + '草稿数据保存成功': '草稿数据保存成功', + '请选择音频文件': '请选择音频文件', + '音频文件': '音频文件', + 'SRT文件': 'SRT文件', + '出图文件夹': '出图文件夹', + '反推提示词': '反推提示词', + '未设置': '未设置', + '文件路径未设置': '文件路径未设置', + '未知的定位类型': '未知的定位类型', + '字幕分组': '字幕分组', + '角色场景': '角色场景', + '一键推理': '一键推理', + '一键出图': '一键出图', + '图片预览': '图片预览', + '一键高清': '一键高清', + '导出剪映': '导出剪映', + '初始化分镜': '初始化分镜', + '手动分组': '手动分组', + '文案分组': '文案分组', + '角色分析': '角色分析', + '场景分析': '场景分析', + '推理所有提示词': '推理所有提示词', + '推理空白分镜提示词': '推理空白分镜提示词', + '生成所有图片': '生成所有图片', + '生成未生成图片分镜': '生成未生成图片分镜', + '生成失败图片分镜': '生成失败图片分镜', + '停止当前批次生图任务': '停止当前批次生图任务', + '停止所有批次生图任务': '停止所有批次生图任务', + '小说分镜初始化成功': '小说分镜初始化成功', + 'AI智能分组': 'AI智能分组', + '未知字幕分组操作': '未知字幕分组操作', + '未知角色场景操作': '未知角色场景操作', + '未知一键推理操作': '未知一键推理操作', + '未知一键出图操作': '未知一键出图操作', + '全部图片高清处理完成': '全部图片高清处理完成 ', + '空行清理完成': '空行清理完成', + '未找到上一行数据': '未找到上一行数据', + "未找到指定的字幕项": '未找到指定的字幕项', + "未找到当前行数据": '未找到当前行数据', + "已自动创建新行": '已自动创建新行 ', + "添加分镜字幕": '添加分镜字幕', + "文案信息已成功保存": '文案信息已成功保存', + "没有数据可以处理": '没有数据可以处理', + "智能批量合并": '智能批量合并', + "智能合并部分完成": '智能合并部分完成', + "处理分镜文案": '处理分镜文案', + "保存文案信息": '保存文案信息', + "下合并": '下合并', + "初始化分镜数据": '初始化分镜数据', + "短文本": '短文本', + "长文本": '长文本', + "分镜文案": '分镜文案', + "AI文案分镜": 'AI文案分镜', + "模型名称": '模型名称', + "字幕": '字幕', + "批量合并": '批量合并', + "向上": '向上', + "向下": '向下', + "时间范围": '时间范围', + "时间轴检查": '时间轴检查', + "请输入文案内容": '请输入文案内容', + "请输入有效的文案内容": '请输入有效的文案内容', + "字幕列表": '字幕列表', + "显示区": '显示区', + "时间": '时间', + "向上合并": '向上合并', + "向上拆分": '向上拆分', + "向下合并": '向下合并', + "向下拆分": '向下拆分', + "导入分镜文案": '导入分镜文案', + "加载数据成功": '加载数据成功', + "请选择图片": '请选择图片', + "编辑小说": '编辑小说', + "添加新小说": '添加新小说', + "小说名称": '小说名称', + "小说类型": '小说类型', + "小说批次任务名称": "小说批次任务名称", + "小说分镜名称": "小说分镜名称", + "任务类型": "任务类型", + "配音地址": '配音地址', + "项目文件夹将自动生成": '项目文件夹将自动生成', + "图片输出文件夹": '图片输出文件夹', + "图片输出文件夹将自动生成": '图片输出文件夹将自动生成', + "SD反推": 'SD反推', + "MJ反推": 'MJ反推', + "选择配音文件": '选择配音文件', + "新增批次数": '新增批次数', + "选择风格": '选择风格', + "风格目前需要到每个批次中单独设置": '风格目前需要到每个批次中单独设置', + "选择旧批次": '选择旧批次', + "选择要复制数据的已存在的批次": '选择要复制数据的已存在的批次', + "使用旧批次数据": '使用旧批次数据', + "复制的数据": '复制的数据', + "文案": '文案', + "生图风格": '生图风格', + "选择角色": '选择角色', + "生图提示词": '生图提示词', + "添加成功": '添加成功', + "进入小说批次信息": '进入小说批次信息', + "删除子项目": '删除子项目', + "批次出图方式": '批次出图方式', + "批次号": '批次号', + "未命名": '未命名', + "删除小说任务并刷新数据成功": '删除小说任务并刷新数据成功', + "还未选择项目": '还未选择项目', + "选择项目": '选择项目', + "请选择项目": '请选择项目', + "创建新项目": '创建新项目', + "修改小说任务": '修改小说任务', + "任务编号": '任务编号', + "重置名称": '重置名称', + "开启视频生成": '开启视频生成', + "保存修改": '保存修改', + "创建账号": "创建账号", + "打开小说文件夹": '打开小说文件夹', + "打开图片文件夹": '打开图片文件夹', + "查看详情": '查看详情', + "查看项目的详细信息和配置": '查看项目的详细信息和配置', + "重置小说": '重置小说', + "删除小说": '删除小说', + "重置小说数据成功": '重置小说数据成功', + "删除小说数据成功": '删除小说数据成功', + "小说管理": '小说管理', + "新建小说": '新建小说', + "搜索指定的小说名称": '搜索指定的小说名称', + "重置筛选条件": '重置筛选条件', + "加载设置失败": '加载设置失败', + "修改任务信息成功": '修改任务信息成功', + "重置任务数据成功": '重置任务数据成功', + "确认删除": '确认删除', + "任务已删除": '任务已删除', + "新增小说批次": '新增小说批次', + "新增小说批次成功": '新增小说批次成功 ', + "点击添加一个新的小说批次任务": '点击添加一个新的小说批次任务', + "暂无小说批次任务": '暂无小说批次任务', + "创建第一个小说批次任务": '创建第一个小说批次任务', + "查看小说数据": '查看小说数据', + "查看小说批次任务信息": '查看小说批次任务信息', + "生成视频文件夹": '生成视频文件夹', + "依赖草稿": '依赖草稿', + "前缀提示词": '前缀提示词', + "后缀提示词": '后缀提示词', + "自动解析角色": '自动解析角色', + "自动解析的角色信息": '自动解析的角色信息', + "删除本地账号成功": "删除本地账号成功", + "删除服务器账号请求失败": "删除服务器账号请求失败", + "服务器删除账号失败": "服务器删除账号失败", + "删除服务器账号成功": "删除服务器账号成功", + "深色模式": "深色模式", + "打开任务": "打开任务", + "查看任务信息": "查看任务信息", + "编辑任务": "编辑任务", + "导出剪映草稿": "导出剪映草稿", + "重置任务": "重置任务", + "删除任务": "删除任务", + "查看当前批次任务的所有的数据信息": "查看当前批次任务的所有的数据信息", + "正在初始化原创模块": "正在初始化原创模块", + "加载小说信息中...": "加载小说信息中...", + "加载小说信息失败,{error}": "加载小说信息失败,{error}", + "预设-{name}": "预设-{name}", + '加载中...': '加载中...', + "打开当前任务,进入图转视频的信息操作界面": "打开当前任务,进入图转视频的信息操作界面", + "将当前批次任务数据和分镜数据全部删除,删除后不可恢复,请谨慎操作": "将当前批次任务数据和分镜数据全部删除,删除后不可恢复,请谨慎操作", + '将项目数据恢复至新建状态,所有的分镜数据都会被删除,包含提示词、图片等信息': '将项目数据恢复至新建状态,所有的分镜数据都会被删除,包含提示词、图片等信息', + "将当前批次任务导出到剪映草稿,包含背景音乐、字幕、图片、关键帧等信息": "将当前批次任务导出到剪映草稿,包含背景音乐、字幕、图片、关键帧等信息", + "编辑当前批次任务,可以修改批次名称、SRT地址、配音地址等信息": "编辑当前批次任务,可以修改批次名称、SRT地址、配音地址等信息", + "打开当前任务,进入分镜操作的详细界面": "打开当前任务,进入分镜操作的详细界面", + "删除服务器账号失败,{error}": "删除服务器账号失败,{error}", + "确定要删除服务器ID为 “{guildId}” 的服务器账号吗?此操作会从服务器删除账号,并同时删除本地记录,操作不可撤销!": "确定要删除服务器ID为 “{guildId}” 的服务器账号吗?此操作会从服务器删除账号,并同时删除本地记录,操作不可撤销!", + "无法删除服务器账号,账号信息无效或缺少服务器账号ID": "无法删除服务器账号,账号信息无效或缺少服务器账号ID", + '打开 文件/文件夹 失败,{error}': '打开 文件/文件夹 失败,{error}', + '打开 文件/文件夹 成功': '打开 文件/文件夹 成功', + '文件/文件夹 路径为空': '文件/文件夹 路径为空', + '子项目列表 - {bookTaskName}': '子项目列表 - {bookTaskName}', + '删除任务失败,{error}': '删除任务失败,{error}', + '正在删除任务...': '正在删除任务...', + '确定要删除任务 {bookTaskName} 吗?删除会将当前批次任务数据和分镜数据全部删除,删除后不可恢复,请确认是否继续?': '确定要删除任务 {bookTaskName} 吗?删除会将当前批次任务数据和分镜数据全部删除,删除后不可恢复,请确认是否继续?', + '重置任务数据失败,{error}': '重置任务数据失败,{error}', + '正在重置任务数据...': '正在重置任务数据...', + '是否重置任务 {bookTaskMame} 的数据?重置后数据将恢复至新建状态,所有的分镜数据(包括提示词、图片、分镜分组等)都会被删除,请确认是否继续!': '是否重置任务 {bookTaskMame} 的数据?重置后数据将恢复至新建状态,所有的分镜数据(包括提示词、图片、分镜分组等)都会被删除,请确认是否继续!', + '未选择任务,请返回重新选择': '未选择任务,请返回重新选择', + '正在初始化图文转视频模块,{bookName}_{bookTaskName}': '正在初始化图文转视频模块,{bookName}_{bookTaskName}', + '打开任务失败,{error}': '打开任务失败,{error}', + '初始化小说分镜模块失败,{error}': '初始化小说分镜模块失败,{error}', + '正在初始化小说分镜模块,{bookName}_{bookTaskName}': '正在初始化小说分镜模块,{bookName}_{bookTaskName}', + '分镜出图的进度:{progress}% ': '分镜出图的进度:{progress}% ', + '正在删除小说数据...': '正在删除小说数据...', + '是否删除该小说,删除后数据将无法恢复,请确认是否继续!': '是否删除该小说,删除后数据将无法恢复,请确认是否继续!', + '正在重置小说数据。。。': '正在重置小说数据。。。', + '是否重置小说数据,重置后数据将恢复至新建状态,所有的批次数据和文件数据都会被删除,请确认是否继续!': '是否重置小说数据,重置后数据将恢复至新建状态,所有的批次数据和文件数据都会被删除,请确认是否继续!', + "查看小说,名称 '{name}'": "查看小说,名称 '{name}'", + '永久删除后项目数据,所有的批次数据和文件数据都会被删除': '永久删除后项目数据,所有的批次数据和文件数据都会被删除', + '将项目数据恢复至新建状态,所有的批次数据和文件数据都会被删除': '将项目数据恢复至新建状态,所有的批次数据和文件数据都会被删除', + '修改小说名称、配音地址和SRT地址等基本信息': '修改小说名称、配音地址和SRT地址等基本信息', + ' {count} 个子项目': ' {count} 个子项目', + '修改失败,{error}': '修改失败,{error}', + '修改任务信息成功,三秒后自动关闭当前界面!': '修改任务信息成功,三秒后自动关闭当前界面!', + '请选择背景音乐文件/文件夹': '请选择背景音乐文件/文件夹', + '选择一个项目来查看子项目,或者创建一个新项目': '选择一个项目来查看子项目,或者创建一个新项目', + '确定要删除小说任务吗?该操作不可逆,会将对应的子任务数据和分镜数据全部删除,请谨慎操作!': '确定要删除小说任务吗?该操作不可逆,会将对应的子任务数据和分镜数据全部删除,请谨慎操作!', + '正在加载小说批次信息...': '正在加载小说批次信息...', + '创建时间 {time}': '创建时间 {time}', + '反推/GPT提示词': '反推/GPT提示词', + '添加小说失败,{error}': '添加小说失败,{error}', + '添加/修改小说信息成功,三秒后自动关闭当前界面!': '添加/修改小说信息成功,三秒后自动关闭当前界面!', + '未找到对应的图片数据,请检查': '未找到对应的图片数据,请检查', + '未找到对应的批次数据,请检查': '未找到对应的批次数据,请检查', + '未找到对应的图片ID,请检查': '未找到对应的图片ID,请检查', + '未找到对应的批次数据名称,请检查': '未找到对应的批次数据名称,请检查', + '未找到对应的操作,请检查': '未找到对应的操作,请检查', + '未找到对应的被删除的图片数据,请检查!!': '未找到对应的被删除的图片数据,请检查!!', + '点击会自动将当前行的 “分镜文案” 与 “字幕” 进行智能合并,合并之后会自动对齐时间轴,可能会失败,失败需要自行定位到自定的行,把失败行对齐之后,再下一行点击 “下合并”,后续就会自动合并': '点击会自动将当前行的 “分镜文案” 与 “字幕” 进行智能合并,合并之后会自动对齐时间轴,可能会失败,失败需要自行定位到自定的行,把失败行对齐之后,再下一行点击 “下合并”,后续就会自动合并', + '点击保存后,会再之前的分镜数据上进行修改,多余的分镜会自动创建,超出的分镜会自动删除,可能会导致数据丢失或者是提示词和出图数据对不上,是否继续?': '点击保存后,会再之前的分镜数据上进行修改,多余的分镜会自动创建,超出的分镜会自动删除,可能会导致数据丢失或者是提示词和出图数据对不上,是否继续?', + '点击取消后,当前操作将被放弃,所有的修改数据不会记录,是否继续?': '点击取消后,当前操作将被放弃,所有的修改数据不会记录,是否继续?', + '当前行是最后一行,无法向下合并': '当前行是最后一行,无法向下合并', + '向下拆分失败,{error}': '向下拆分失败,{error}', + '当前行只存在一行字幕,无法向下拆分': '当前行只存在一行字幕,无法向下拆分', + '向上拆分失败,{error}': '向上拆分失败,{error}', + '当前行只存在一行字幕,无法向上拆分': '当前行只存在一行字幕,无法向上拆分', + '该字幕已存在于目标行中,无法重复合并': '该字幕已存在于目标行中,无法重复合并', + '当前行是第一行,无法向上合并': '当前行是第一行,无法向上合并', + '请在此输入内容...': '请在此输入内容...', + '共 {wordCount} 行分镜,共 {count} 个字,自动删除空行': '共 {wordCount} 行分镜,共 {count} 个字,自动删除空行', + '请输入分镜文案,每行一个分镜...': '请输入分镜文案,每行一个分镜...', + '请输入分镜好的文案,软件只会检查输入文案的空行,不会对文案进行修改,请修改处理后写入!': '请输入分镜好的文案,软件只会检查输入文案的空行,不会对文案进行修改,请修改处理后写入!', + '设置AI模型名称失败,{error}': '设置AI模型名称失败,{error}', + '正在设置AI模型名称为:{modelName}': '正在设置AI模型名称为:{modelName}', + '从当前行开始,合并下面所有的分镜与字幕': '从当前行开始,合并下面所有的分镜与字幕', + '点击会自动将当前行的分镜文案与字幕进行智能合并,合并之后会自动对齐时间轴,可能会失败,失败需要自行定位到指定的行,把失败行对齐之后,再下一行点击下合并,后续就会自动合并': '点击会自动将当前行的分镜文案与字幕进行智能合并,合并之后会自动对齐时间轴,可能会失败,失败需要自行定位到指定的行,把失败行对齐之后,再下一行点击下合并,后续就会自动合并', + '请输入分镜文案...': '请输入分镜文案...', + '请输入调用分组的模型名称,不填写调用的是 “{path}” 中设置的模型': '请输入调用分组的模型名称,不填写调用的是 “{path}” 中设置的模型', + 'AI文案分镜-{typeText} 处理完成': 'AI文案分镜-{typeText} 处理完成', + 'AI文案分镜-{typeText} 处理失败,失败原因: {error}': 'AI文案分镜-{typeText} 处理失败,失败原因: {error}', + '正在调用AI文案分镜-{typeText}处理...': '正在调用AI文案分镜-{typeText}处理...', + '正在调用AI进行分镜合并,请稍后...': '正在调用AI进行分镜合并,请稍后...', + '根据字幕列,将数据拆分为单句,重置分镜分组数据': '根据字幕列,将数据拆分为单句,重置分镜分组数据', + 'AI文案分镜-长文本': 'AI文案分镜-长文本', + '将调用AI对文案进行分析,合并成35字以上的长文本分镜。合并的长度由AI决定,由AI处理,可能会与原文案存在差异,导致字幕和分镜不能完全对上,需要手动调整对齐': '将调用AI对文案进行分析,合并成35字以上的长文本分镜。合并的长度由AI决定,由AI处理,可能会与原文案存在差异,导致字幕和分镜不能完全对上,需要手动调整对齐', + 'AI文案分镜-短文本': 'AI文案分镜-短文本', + '将调用AI对文案进行分析,合并成15-35字左右的短文本分镜,由AI处理,可能会与原文案存在差异,导致字幕和分镜不能完全对上,需要手动调整对齐': '将调用AI对文案进行分析,合并成15-35字左右的短文本分镜,由AI处理,可能会与原文案存在差异,导致字幕和分镜不能完全对上,需要手动调整对齐', + '将分镜数据上进行修改,多余的分镜会自动创建,超出的分镜会自动删除': '将分镜数据上进行修改,多余的分镜会自动创建,超出的分镜会自动删除', + '点击该按钮,会弹出导入分镜文案的对话框,可以输入已经分好镜的文案,方便我们的字幕根据分镜文案进行分组对齐,导入之后点击 “批量合并” 按钮,进行自动对齐': '点击该按钮,会弹出导入分镜文案的对话框,可以输入已经分好镜的文案,方便我们的字幕根据分镜文案进行分组对齐,导入之后点击 “批量合并” 按钮,进行自动对齐', + '时间轴检查和改写完成,请手动保存!!!': '时间轴检查和改写完成,请手动保存!!!', + '第 {index} 行字幕结束时间为空,请检查': '第 {index} 行字幕结束时间为空,请检查', + '第 {indx}行字幕为空,请检查': '第 {indx}行字幕为空,请检查', + '确定要检查时间轴吗?该操作会更具对应的字幕进行时间的强制对齐,是不是继续操作?': '确定要检查时间轴吗?该操作会更具对应的字幕进行时间的强制对齐,是不是继续操作?', + '智能合并失败,{error}': '智能合并失败,{error}', + '智能批量合并完成,所有字幕都已正确匹配': '智能批量合并完成,所有字幕都已正确匹配', + '智能合并完成,{count} 个剩余字幕已写入后续行': '智能合并完成,{count} 个剩余字幕已写入后续行', + '处理过程中遇到错误,已停止智能匹配:\n\n{message}\n\n已处理字幕:${totalProcessed} 个\n剩余字幕:${totalRemaining} 个\n\n剩余字幕已按顺序写入后续行中,请手动调整文案匹配。': '处理过程中遇到错误,已停止智能匹配:\n\n{message}\n\n已处理字幕:${totalProcessed} 个\n剩余字幕:${totalRemaining} 个\n\n剩余字幕已按顺序写入后续行中,请手动调整文案匹配。', + '第 {rowIndex} 行没有找到任何匹配的字幕': '第 {rowIndex} 行没有找到任何匹配的字幕', + '第 {rowIndex} 行在处理字幕 “{subtitleText}” 时无法匹配,已累积文本 “{accumulatedText}” 与目标文案 “{text}”不匹配': '第 {rowIndex} 行在处理字幕 “{subtitleText}” 时无法匹配,已累积文本 “{accumulatedText}” 与目标文案 “{text}”不匹配', + '第 {rowIndex} 行的第一个字幕 “{subtitleText}” 与文案 “{text}” 不匹配': '第 {rowIndex} 行的第一个字幕 “{subtitleText}” 与文案 “{text}” 不匹配', + '第 {index} 行字幕为空,无法继续处理': '第 {index} 行字幕为空,无法继续处理', + '确定要进行智能批量合并吗?将从第 {startIndex} 行开始处理。此操作会根据分镜文案内容自动将字幕合并到对应的行中。': '确定要进行智能批量合并吗?将从第 {startIndex} 行开始处理。此操作会根据分镜文案内容自动将字幕合并到对应的行中。', + '起始索引 {startIndex} 无效,有效范围是 0 到 {endIndex}': '起始索引 {startIndex} 无效,有效范围是 0 到 {endIndex}', + '正在保存文案信息...': '正在保存文案信息...', + '点击保存后,会在之前的分镜数据上进行修改,多余的分镜会自动创建,超出的分镜会自动删除,可能会导致数据丢失或者是提示词和出图数据对不上,是否继续?': '点击保存后,会在之前的分镜数据上进行修改,多余的分镜会自动创建,超出的分镜会自动删除,可能会导致数据丢失或者是提示词和出图数据对不上,是否继续?', + "保存数据失败,数据不完整,请检查'文案'、'时间'、'字幕' 等,数据是否完整": "保存数据失败,数据不完整,请检查'文案'、'时间'、'字幕' 等,数据是否完整", + '向下合并失败,{error}': '向下合并失败,{error}', + '字幕向下合并成功,移动了 {count} 个字幕项': '字幕向下合并成功,移动了 {count} 个字幕项', + '向上合并失败,{error}': '向上合并失败,{error}', + '字幕向上合并成功,移动了 {itemIndex} 个字幕项': '字幕向上合并成功,移动了 {itemIndex} 个字幕项', + '当前是第一行,不能向上合并': '当前是第一行,不能向上合并', + '当前行索引:{rowIndex}, 当前字幕项索引:{itemIndex}': '当前行索引:{rowIndex}, 当前字幕项索引:{itemIndex}', + '操作进行中,请稍后...': '操作进行中,请稍后...', + '导出剪映草稿失败:{error}': '导出剪映草稿失败:{error}', + '高清失败:{error}': '高清失败:{error}', + '正在高清图片... {current} / {total}': '正在高清图片... {current} / {total}', + '所有分镜的图片会被替换为高清图片,是否继续?\n当前高清倍数:{hdScale} 倍': '所有分镜的图片会被替换为高清图片,是否继续?\n当前高清倍数:{hdScale} 倍', + '分镜的图片全部存在,开始进行高清处理': '分镜的图片全部存在,开始进行高清处理', + '正在检查当前分镜的图片是否存在...': '正在检查当前分镜的图片是否存在...', + '执行操作会将所有批次正在等待中的出图任务停止,是否继续?': '执行操作会将所有批次正在等待中的出图任务停止,是否继续?', + '执行操作会将当前批次正在等待中的出图任务停止,是否继续?': '执行操作会将当前批次正在等待中的出图任务停止,是否继续?', + '执行操作会将生图失败的分镜的出图任务添加到队列中,MJ生图错误任务不可重新提交,会直接出现错误提示,是否继续?': '执行操作会将生图失败的分镜的出图任务添加到队列中,MJ生图错误任务不可重新提交,会直接出现错误提示,是否继续?', + '执行操作会将未出图的分镜的出图任务添加到队列中,除了生图失败的分镜外,是否继续?': '执行操作会将未出图的分镜的出图任务添加到队列中,除了生图失败的分镜外,是否继续?', + '执行操作会添加所有的分镜任务到后台任务队列中,除了被锁定的分镜外,其余的分镜的出图任务都会被添加到后台任务队列中,可能会导致部分图片数据重置,是否继续?': '执行操作会添加所有的分镜任务到后台任务队列中,除了被锁定的分镜外,其余的分镜的出图任务都会被添加到后台任务队列中,可能会导致部分图片数据重置,是否继续?', + 'MJ生图,错误的任务不可重新提交,请单句提交!': 'MJ生图,错误的任务不可重新提交,请单句提交!', + '会将当前批次的不存在提示词的分镜进行推理,不会影响已有提示词的分镜,是否继续?': '会将当前批次的不存在提示词的分镜进行推理,不会影响已有提示词的分镜,是否继续?', + '会将当前批次的所有的提示词进行推理,并且当前已有的提示词会被覆盖,是否继续?': '会将当前批次的所有的提示词进行推理,并且当前已有的提示词会被覆盖,是否继续?', + '打开AI智能分组失败: {error}': '打开AI智能分组失败: {error}', + '正在准备AI智能分组组件,请稍等...': '正在准备AI智能分组组件,请稍等...', + '打开文案分组失败: {error}': '打开文案分组失败: {error}', + '正在准备文案分组组件,请稍等...': '正在准备文案分组组件,请稍等...', + '打开手动分组失败: {error}': '打开手动分组失败: {error}', + '正在准备手动分组组件,请稍等...': '正在准备手动分组组件,请稍等...', + '初始化小说分镜失败,{error}': '初始化小说分镜失败,{error}', + '正在开始初始化小说分镜,请稍等。。。': '正在开始初始化小说分镜,请稍等。。。', + '该操作会将当前的分镜数据全部删除,然后根据当前的批次任务的 SRT文件,重新初始化分镜(生成的分镜是未分组的,需要重新进行分镜的分组)': '该操作会将当前的分镜数据全部删除,然后根据当前的批次任务的 SRT文件,重新初始化分镜(生成的分镜是未分组的,需要重新进行分镜的分组)', + '使用 AI 大模型对分镜进行分组,由AI对文案进行分析,自动生成分组建议': '使用 AI 大模型对分镜进行分组,由AI对文案进行分析,自动生成分组建议', + '通过以分镜好的文案进行分镜的分组,导入的SRT文件中的字幕会和分镜的文案进行匹配,可能会出现文案和SRT不能完全对齐,需要手动进行处理': '通过以分镜好的文案进行分镜的分组,导入的SRT文件中的字幕会和分镜的文案进行匹配,可能会出现文案和SRT不能完全对齐,需要手动进行处理', + '手动将所有当前手动的字幕进行分组,可以向上/向下合并,向上/向下拆分': '手动将所有当前手动的字幕进行分组,可以向上/向下合并,向上/向下拆分', + '将当前的分镜的数据全部删除,然后根据当前的批次任务的 SRT文件,重新生成分镜(生成的分镜是单个,需要重新进行分镜的分组)': '将当前的分镜的数据全部删除,然后根据当前的批次任务的 SRT文件,重新生成分镜(生成的分镜是单个,需要重新进行分镜的分组)', + '将当前批次的所有的提示词进行推理,并且当前已有的提示词会被覆盖,是否继续?': '将当前批次的所有的提示词进行推理,并且当前已有的提示词会被覆盖,是否继续?', + '全部已完成,不必跳转': '全部已完成,不必跳转', + '打开文件夹失败,{error}': '打开文件夹失败,{error}', + '剪映草稿 {draftName} 生成成功': '剪映草稿 {draftName} 生成成功', + '正在生成剪映草稿。。。': '正在生成剪映草稿。。。', + '表单验证失败,{error}': '表单验证失败,{error}', + '未传入任务或小说信息,无法加载数据': '未传入任务或小说信息,无法加载数据', + '在生成草稿前要先保存数据,选择的音频文件要和当前的字幕文件保持一致': '在生成草稿前要先保存数据,选择的音频文件要和当前的字幕文件保持一致', + '注意:选择的草稿主轨道的图片数量要和当前的相同,会生成新的草稿并且只会替换图片,保留其余的数据,依赖的草稿必须是剪映 5.9 级以下版本,不支持 6.0 及以上版本': '注意:选择的草稿主轨道的图片数量要和当前的相同,会生成新的草稿并且只会替换图片,保留其余的数据,依赖的草稿必须是剪映 5.9 级以下版本,不支持 6.0 及以上版本', + '选择文件/文件夹': '选择文件/文件夹', + '请选择背景音乐文件夹或音频文件...': '请选择背景音乐文件夹或音频文件...', + '请选择配音文件...': '请选择配音文件...', + '配音文件 (MP3/WAV)': '配音文件 (MP3/WAV)', + '获取数据失败,失败原因:没有找到对应的数据': '获取数据失败,失败原因:没有找到对应的数据', + '是否保存当前的提示词的排序顺序?只会修改当前分镜的提示词的顺序!': '是否保存当前的提示词的排序顺序?只会修改当前分镜的提示词的顺序!', + '获取数据失败,{error}': '获取数据失败,{error}', + '修改提示词排序顺序失败,{error}': '修改提示词排序顺序失败,{error}', + '正在保存提示词的排序顺序,请稍等...': '正在保存提示词的排序顺序,请稍等...', + '是否保存当前的提示词的排序顺序?会修改当前任务下的所有提示词的顺序!': '是否保存当前的提示词的排序顺序?会修改当前任务下的所有提示词的顺序!', + '当前的标签数量不够,需要添加 {count} 个标签': '当前的标签数量不够,需要添加 {count} 个标签', + '下载结果 (成功: {successCount}/{totalCount})': '下载结果 (成功: {successCount}/{totalCount})', + '是否一拆四(一拆四是个泛指,根据出的子图数量最少的),会自动生成多个批次任务并设置对应的图片!': '是否一拆四(一拆四是个泛指,根据出的子图数量最少的),会自动生成多个批次任务并设置对应的图片!', + '正在采集图片,请稍后...': '正在采集图片,请稍后...', + '即将开始采集所有的MJ生图图片,满足一下条件的分镜才会被采集:有生图信息,有消息ID,没有生成图片,图片的有效期为24小时,是否继续?': '即将开始采集所有的MJ生图图片,满足一下条件的分镜才会被采集:有生图信息,有消息ID,没有生成图片,图片的有效期为24小时,是否继续?', + '没有需要锁定/解锁的分镜,请先出图!': '没有需要锁定/解锁的分镜,请先出图!', + '即将开始锁定当前批次的所有图片,只有有输出主图的分镜才会被锁定,是否继续?': '即将开始锁定当前批次的所有图片,只有有输出主图的分镜才会被锁定,是否继续?', + '即将开始解锁当前批次的所有图片,只有被锁定的分镜才会被解锁,是否继续?': '即将开始解锁当前批次的所有图片,只有被锁定的分镜才会被解锁,是否继续?', + '正在批量设置风格...': '正在批量设置风格...', + '批量设置风格失败,{error}': '批量设置风格失败,{error}', + '选择对应的风格,应用所有的分镜': '选择对应的风格,应用所有的分镜', + '人物/场景/风格': '人物/场景/风格', + '即将开始执行推理提示词任务,请稍等。。。': '即将开始执行推理提示词任务,请稍等。。。', + '命令预览生成错误:{error}': '命令预览生成错误:{error}', + '正在生成命令预览...': '正在生成命令预览...', + '中文 2 英文': '中文 2 英文', + '英文 2 中文': '英文 2 中文', + '下翻译 英文 2 中文': '下翻译 英文 2 中文', + '下翻译 中文 2 英文': '下翻译 中文 2 英文', + '清空 {name} 图片成功': '清空 {name} 图片成功', + '是否清空当前分镜的图片?清空之后数据将无法恢复,请谨慎操作!': '是否清空当前分镜的图片?清空之后数据将无法恢复,请谨慎操作!', + '当前分镜图片已被锁定,无法清空图片': '当前分镜图片已被锁定,无法清空图片', + '未找到可生成图片的分镜,或者分镜都被锁定!': '未找到可生成图片的分镜,或者分镜都被锁定!', + '添加出图任务到任务队列中成功!': '添加出图任务到任务队列中成功!', + '当前分镜图片已被锁定,无法生成图片': '当前分镜图片已被锁定,无法生成图片', + '清除当前分镜的图片,包括子图和主图': '清除当前分镜的图片,包括子图和主图', + '锁定/解锁当前图片,防止被修改': '锁定/解锁当前图片,防止被修改', + '为当前分镜及后续分镜生成图片,会跳过已锁定的分镜,但是不会跳过已生成图片的分镜': '为当前分镜及后续分镜生成图片,会跳过已锁定的分镜,但是不会跳过已生成图片的分镜', + '图片锁定/解锁成功': '图片锁定/解锁成功', + '没有生成图片,不能锁定': '没有生成图片,不能锁定', + '图片文件不存在或已被删除,无法锁定': '图片文件不存在或已被删除,无法锁定', + '检查图片文件出错: {error}': '检查图片文件出错: {error}', + '选择缓存区文件__主图__{bookTaksName}__{bookTaskDetailName}': '选择缓存区文件__主图__{bookTaksName}__{bookTaskDetailName}', + 'MJ任务ID:{id}': 'MJ任务ID:{id}', + '修改风格标签失败,{error}': '修改角色标签失败,{error}', + '修改风格标签成功!': '修改角色标签成功!', + '修改场景标签失败,{error}': '修改场景标签失败,{error}', + '修改场景标签成功!': '修改场景标签成功!', + '修改角色标签成功!': '修改角色标签成功!', + '修改角色标签失败,{error}': '修改角色标签失败,{error}', + '获取标签数据失败,{error}': '获取标签数据失败,{error}', + '下载图片失败,{error}': '下载图片失败,{error}', + '正在下载图片,请稍后...': '正在下载图片,请稍后...', + '没有MJ生图信息,不能下载': '没有MJ生图信息,不能下载', + '没有消息ID,不能采集图片': '没有消息ID,不能采集图片', + '当前图片不是MJ图片,不能下载': '当前图片不是MJ图片,不能下载', + '单个图片的下载,只支持 MJ API 生图,当前有图片的话会被覆盖掉,是否继续?': '单个图片的下载,只支持 MJ API 生图,当前有图片的话会被覆盖掉,是否继续?', + '正在生成高清图片...': '正在生成高清图片...', + '将当前分镜的主图高清,高清倍数:{hdScale} 倍,是否继续?': '将当前分镜的主图高清,高清倍数:{hdScale} 倍,是否继续?', + '注意:上传的图片(png)到主图,并且会覆盖原有的图片,不可恢复,上传到图片只能是裁剪过的,该操作不会自动裁剪上传,是否继续?': '注意:上传的图片(png)到主图,并且会覆盖原有的图片,不可恢复,上传到图片只能是裁剪过的,该操作不会自动裁剪上传,是否继续?', + '翻译中 ... {current} / {total}': '翻译中 ... {current} / {total}', + '正在推理提示词,推理进度 {current} / {total}': '正在推理提示词,推理进度 {current} / {total}', + '字幕/文案': '字幕/文案', + '无法定位:找不到对应的ID': '无法定位:找不到对应的ID', + '分镜_{name}': '分镜_{name}', + '图片 {index}': '图片 {index}', + '删除失败,未找到对应的行': '删除失败,未找到对应的行', + '场景推理失败:{error}': '场景推理失败:{error}', + '角色推理失败:{error}': '角色推理失败:{error}', + '删除 {name} 成功': '删除 {name} 成功', + '导入 {name} 到场景预设成功': '导入 {name} 到场景预设成功', + '导入 {name} 到角色预设成功': '导入 {name} 到角色预设成功', + '1. {type}的数据是由当前的小说文案进行分析的结果': '1. {type}的数据是由当前的小说文案进行分析的结果', + '2. {type}的数据只会在部分推理模式中生效,请到 “设置 -> 推理设置” 中查看哪些推理模式支持': '2. {type}的数据只会在部分推理模式中生效,请到 “设置 -> 推理设置” 中查看哪些推理模式支持', + '3. {type}的数据会在每次推理时进行更新,推理完成后会自动保存到当前小说的{type}数据中': '3. {type}的数据会在每次推理时进行更新,推理完成后会自动保存到当前小说的{type}数据中', + '4. 也可自定义或者修改指定的{type}数据,点击 “编辑” 按钮进行修改或 “添加数据” 按钮进行添加': '4. 也可自定义或者修改指定的{type}数据,点击 “编辑” 按钮进行修改或 “添加数据” 按钮进行添加', + '5. 数据修改之后需要手动保存,点击 “保存” 按钮进行保存': '5. 数据修改之后需要手动保存,点击 “保存” 按钮进行保存', + '6. 删除数据后,推理时会自动删除当前小说的{type}中的对应数据': '6. 删除数据后,推理时会自动删除当前小说的{type}中的对应数据', + '7. 需要在外部手动选择需要的{type}数据时,请点击“{button}” 按钮进行导入到标签集中': '7. 需要在外部手动选择需要的{type}数据时,请点击“{button}” 按钮进行导入到标签集中', + '即将开始自动推理,该操作会将之前的 {type} 数据覆盖,是否继续?': '即将开始自动推理,该操作会将之前的场景数据覆盖,是否继续?', + '正在推理,请稍等...': '正在推理,请稍等...', + //#endregion + + //#region 转视频 + "视频配置": '视频配置', + "批量设置": '批量设置', + "选中视频": '选中视频', + "暂无选择视频": '暂无选择视频', + "暂无可选视频": '暂无可选视频', + "视频选择": '视频选择', + "返回图转视频列表": '返回图转视频列表', + "视频配置信息": '视频配置信息', + "右侧面板已显示": '右侧面板已显示', + "请选择左侧表格中的任务查看详细信息": '请选择左侧表格中的任务查看详细信息', + "已取消批量设置视频类型": '已取消批量设置视频类型', + "选择转视频方式": '选择转视频方式', + "提示词": '提示词', + "保存选中视频": '保存选中视频', + "视频生成类型": '视频生成类型', + "视频总数量": '视频总数量', + "是否删除小说数据,删除后数据将被永久删除,所有的子项目数据和文件数据都会被删除,请确认是否继续?": "是否删除小说数据,删除后数据将被永久删除,所有的子项目数据和文件数据都会被删除,请确认是否继续?", + "正在打开任务...": "正在打开任务...", + "正在初始化图/文转视频模块": "正在初始化图/文转视频模块", + '已开启分页,表格高度已调整': '已开启分页,表格高度已调整', + '已关闭分页,显示全部数据,表格高度已调整': '已关闭分页,显示全部数据,表格高度已调整', + '右侧面板已隐藏,点击查看按钮可在抽屉中查看详情': '右侧面板已隐藏,点击查看按钮可在抽屉中查看详情', + 'Runway 视频配置': 'Runway 视频配置', + 'Luma 视频配置': 'Luma 视频配置', + 'Kling 视频配置': 'Kling 视频配置', + 'Midjourney 视频配置': 'Midjourney 视频配置', + 'PIKA 视频配置': 'PIKA 视频配置', + '确定要将所有 {taskLength} 个任务的视频类型设置为 {typeInfo} 吗?此操作不可撤销。': '确定要将所有 {taskLength} 个任务的视频类型设置为 {typeInfo} 吗?此操作不可撤销。', + '已批量设置 {tasksLength} 个任务的视频类型为: {optionLabel}': '已批量设置 {tasksLength} 个任务的视频类型为: {optionLabel}', + '批量设置视频类型失败: {error}': '批量设置视频类型失败: {error}', + '确定要重新加载任务 “{name}” 吗?\n\n重新加载会将当前的任务重新获取,只能再当前任务视频下载失败时才能使用。否则会导致重复的视频显示,请确认是否继续!': '确定要重新加载任务 “{name}” 吗?\n\n重新加载会将当前的任务重新获取,只能再当前任务视频下载失败时才能使用。否则会导致重复的视频显示,请确认是否继续!', + '正在重新加载视频任务...': '正在重新加载视频任务...', + '未找到当前分镜的视频配置,不可重新加载!!': '未找到当前分镜的视频配置,不可重新加载!!', + '重新加载任务失败,{error}': '重新加载任务失败,{error}', + '暂无视频,请先生成视频': '暂无视频,请先生成视频', + '当前分镜没有转视频相关的信息,请先进行转视频操作!': '当前分镜没有转视频相关的信息,请先进行转视频操作!', + '当前未选择任何视频,请先选择 {cout} 个视频!': '当前未选择任何视频,请先选择 {cout} 个视频!', + '任务ID不能为空,请检查!': '任务ID不能为空,请检查!', + '视频信息详情:': '视频信息详情:', + '修改选择的视频之后,请点击 “保存视频选择” 按钮将操作进行保存生效!!!': '修改选择的视频之后,请点击 “保存视频选择” 按钮将操作进行保存生效!!!', + '正在加载任务数据...': '正在加载任务数据...', + '加载数据失败,{error}': '加载数据失败,{error}', + '处理数据时发生错误,{error}': '处理数据时发生错误,{error}', + "视频生成成功": "视频生成成功", + "图转视频失败,失败信息如下:{error}": "图转视频失败,失败信息如下:{error}", + '不支持的图片链接,仅支持网络图片链接!': '不支持的图片链接,仅支持网络图片链接!', + + //#region MJ + '基本信息': '基本信息', + "任务名称": '任务名称', + "消息名称": "消息名称", + "任务状态": '任务状态', + "任务描述": '任务描述', + "暂无视频配置信息": '暂无视频配置信息', + '视频类型已更改为:{type}': '视频类型已更改为:{type}', + '更新视频类型失败:{error}': '更新视频类型失败:{error}', + "数据加载中...": "数据加载中...", + "选择大分类": "选择大分类", + "文案处理设置": "文案处理设置", + "设置文案处理相关的API和设置": "设置文案处理相关的API和设置", + "暂无视频,请先生成视频!": "暂无视频,请先生成视频!", + '图片链接(首帧)': '图片链接(首帧)', + '请输入图片链接': '请输入图片链接', + '尾帧图片链接': '尾帧图片链接', + '提示词(可选)': '提示词(可选)', + "图生视频": '图生视频', + "视频拓展": '视频拓展', + "任务ID": '任务ID', + "视频索引": '视频索引', + "视频类型": '视频类型', + "更新到": '更新到', + '未知的修改键: {key}': '未知的修改键: {key}', + "视频预览": '视频预览', + "请选择一个视频": '请选择一个视频', + "任务详情": '任务详情', + "保存任务选择": '保存任务选择', + "分镜名称": '分镜名称', + "本地路径": '本地路径', + "远端地址": '远端地址', + "视频总数": '视频总数', + '视频 {index}': '视频 {index}', + "没有找到对应的API的配置,请检查 ‘{data}’ 配置!": "没有找到对应的API的配置,请检查 ‘{data}’ 配置!", + "当前API不支持MJ出图,请检查 ‘{data}’ 配置!": "当前API不支持MJ出图,请检查 ‘{data}’ 配置!", + "没有找到对应的生图包的配置或配置有误,请检查 ‘{data}’ 配置!": "没有找到对应的生图包的配置或配置有误,请检查 ‘{data}’ 配置!", + "当前选择的包不支持,请检查 ‘{data}’ 配置!": "当前选择的包不支持,请检查 ‘{data}’ 配置!", + "本地代理模式的设置不完善或配置错误,请检查 ‘{data}’ 配置!": "本地代理模式的设置不完善或配置错误,请检查 ‘{data}’ 配置!", + "当前的MJ出图模式不支持,请检查 ‘{data}’ 配置!": "当前的MJ出图模式不支持,请检查 ‘{data}’ 配置!", + "MJ反推的类型不支持,反推只支持API和代理模式": "MJ反推的类型不支持,反推只支持API和代理模式", + "MJ出图的类型不支持": 'MJ出图的类型不支持', + "提示词里面出现了 feishu.cn 飞书的链接,请检查并复制正确的链接": '提示词里面出现了 feishu.cn 飞书的链接,请检查并复制正确的链接', + "返回的数据为空": "返回的数据为空", + "{emptyName} 的提示词为空,请先推理提示词": "{emptyName} 的提示词为空,请先推理提示词", + "MJ模式合并数据成功!": "MJ模式合并数据成功!", + "MJ模式合并数据失败,{error}": "MJ模式合并数据失败,{error}", + "MJ生图失败,{error}": "MJ生图失败,{error}", + '当前选中的视频的 taskId 或 videoIndex 为空,请检查视频信息': '当前选中的视频的 taskId 或 videoIndex 为空,请检查视频信息', + '修改选择的视频任务之后,会将对应的选择的视频的': '修改选择的视频任务之后,会将对应的选择的视频的', + '父任务ID': '父任务ID', + '选择父任务': '选择父任务', + '请输入需要操作的视频任务ID': '请输入需要操作的视频任务ID', + '父任务ID说明:': '父任务ID说明:', + '点击按钮可以选择当前分镜的父任务ID,转视频类别为 MJ_VIDEO 的任务。': '点击按钮可以选择当前分镜的父任务ID,转视频类别为 MJ_VIDEO 的任务。', + '也可手动输入 MJ_VIDEO 的任务ID,手动输入时,视频索引也需手动输入!!': '也可手动输入 MJ_VIDEO 的任务ID,手动输入时,视频索引也需手动输入!!', + '注意:如果当前任务不是 MJ_VIDEO 类型,则无法选择父任务。': '注意:如果当前任务不是 MJ_VIDEO 类型,则无法选择父任务。', + '当前任务不是 MJ_VIDEO 类型,无法选择父任务!': '当前任务不是 MJ_VIDEO 类型,无法选择父任务!', + '视频索引 (Index)': '视频索引 (Index)', + '视频索引说明:': '视频索引说明:', + '选择要进行视频拓展的具体视频索引,范围为 0-3。': '选择要进行视频拓展的具体视频索引,范围为 0-3。', + '索引 0:': '索引 0:', + '第一个生成的视频': '第一个生成的视频', + '索引 1:': '索引 1:', + '第二个生成的视频': '第二个生成的视频', + '索引 2:': '索引 2:', + '第三个生成的视频': '第三个生成的视频', + '索引 3:': '索引 3:', + '第四个生成的视频': '第四个生成的视频', + '选择对应的索引后,会对该索引的视频进行拓展操作。': '选择对应的索引后,会对该索引的视频进行拓展操作。', + '视频类型 (SD/HD)': '视频类型 (SD/HD)', + '视频质量说明:': '视频质量说明:', + '选择视频类型': '选择视频类型', + 'SD(标清):': 'SD(标清):', + '480p 质量,适合一般用途和快速生成。': '480p 质量,适合一般用途和快速生成。', + 'HD(高清):': 'HD(高清):', + '720p 质量,更高的清晰度,需要相应的订阅套餐。': '720p 质量,更高的清晰度,需要相应的订阅套餐。', + '不同宽高比的视频尺寸:': '不同宽高比的视频尺寸:', + '* 视频的具体形状和大小取决于起始图像的宽高比': '* 视频的具体形状和大小取决于起始图像的宽高比', + '运动变化 (Motion)': '运动变化 (Motion)', + '运动变化程度说明:': '运动变化程度说明:', + '生成视频时,您有两个运动设置选项可供选择:“低运动”和“高运动”。请使用网站上的相应按钮,或在视频提示末尾添加': '生成视频时,您有两个运动设置选项可供选择:“低运动”和“高运动”。请使用网站上的相应按钮,或在视频提示末尾添加', + '或参数': '或参数', + '低运动(默认):': '低运动(默认):', + '更有可能产生静止场景、低摄像机运动、慢动作或细微的角色动作。': '更有可能产生静止场景、低摄像机运动、慢动作或细微的角色动作。', + '高运动:': '高运动:', + '更有可能导致大的摄像机运动和更大的角色运动,但也可能产生不切实际或有故障的动作。': '更有可能导致大的摄像机运动和更大的角色运动,但也可能产生不切实际或有故障的动作。', + '选择运动变化程度': '选择运动变化程度', + '批次数量 (Batch)': '批次数量 (Batch)', + '批次数量说明:': '批次数量说明:', + '选择一次生成的视频数量。生成多个视频可以提供更多选择,但会消耗更多的配额和时间。': '选择一次生成的视频数量。生成多个视频可以提供更多选择,但会消耗更多的配额和时间。', + '1个视频:': '1个视频:', + '生成单个视频,速度快,配额消耗少。': '生成单个视频,速度快,配额消耗少。', + '2个视频:': '2个视频:', + '生成2个不同的视频变体,提供更多选择。': '生成2个不同的视频变体,提供更多选择。', + '4个视频:': '4个视频:', + '生成4个不同的视频变体,最多选择,但配额消耗最多。': '生成4个不同的视频变体,最多选择,但配额消耗最多。', + '注意:批次数量越大,消耗的配额越多,生成时间也越长。': '注意:批次数量越大,消耗的配额越多,生成时间也越长。', + '选择批次数量': '选择批次数量', + '视频原始 (Raw)': '视频原始 (Raw)', + '视频原始开关说明:': '视频原始开关说明:', + '为了更精确地控制视频创作的运动,使用': '为了更精确地控制视频创作的运动,使用', + '参数会很有帮助。此功能的作用类似于图像的 Raw 模式。': '参数会很有帮助。此功能的作用类似于图像的 Raw 模式。', '当您添加': '当您添加', + ',它会减少 Midjourney 添加的额外创意天赋,从而使您的提示文本对结果产生更大的影响。': ',它会减少 Midjourney 添加的额外创意天赋,从而使您的提示文本对结果产生更大的影响。', '注意:视频原始只有在有提示词的时候才会生效': '注意:视频原始只有在有提示词的时候才会生效', + '首尾循环(首尾帧相同)': '首尾循环(首尾帧相同)', + '首尾循环说明:': '首尾循环说明:', + '开启此选项将创建一个无缝循环的视频,视频的最后一帧将平滑过渡到第一帧。': '开启此选项将创建一个无缝循环的视频,视频的最后一帧将平滑过渡到第一帧。', + '首尾循环和尾帧图片链接不能同时使用,如果同时启用,尾帧图片链接将被忽略。': '首尾循环和尾帧图片链接不能同时使用,如果同时启用,尾帧图片链接将被忽略。', + '注意:循环视频适合制作动画效果或背景视频。': '注意:循环视频适合制作动画效果或背景视频。', + '执行视频拓展': '执行视频拓展', + '请先选择父任务ID和视频索引!': '请先选择父任务ID和视频索引!', + '添加视频拓展任务成功!': '添加视频拓展任务成功!', + '请选择运动变化程度': '请选择运动变化程度', + '生成视频': '生成视频', + '图片加载失败,请检查图片链接是否有效!': '图片加载失败,请检查图片链接是否有效!', + '视频生成时长为5秒,但并非仅限于此。视频制作完成后,您可以在当前界面为选定的适配进行延长!': '视频生成时长为5秒,但并非仅限于此。视频制作完成后,您可以在当前界面为选定的适配进行延长!', + '您可以随意将视频延长最多 4 次,每次延长 4 秒,直至达到 21 秒(即可用的最大长度)。': '您可以随意将视频延长最多 4 次,每次延长 4 秒,直至达到 21 秒(即可用的最大长度)。', + + //#endregion + //#endregion + + //#region 文案处理 + "【LaiTool】场景提示大师(上下文-提示词不包含人物)": "【LaiTool】场景提示大师(上下文-提示词不包含人物)", + "【LaiTool】分镜大师-特效增强版(上下文-人物场景固定)": "【LaiTool】分镜大师-特效增强版(上下文-人物场景固定)", + '【LaiTool】分镜大师-通用版(上下文-人物场景固定-类型推理)': '【LaiTool】分镜大师-通用版(上下文-人物场景固定-类型推理)', + '【LaiTool】分镜大师-全面版-AI增强(上下文-人物场景固定-单帧)': '【LaiTool】分镜大师-全面版-AI增强(上下文-人物场景固定-单帧)', + '【LaiTool】分镜大师-全能优化版(上下文-人物固定)': '【LaiTool】分镜大师-全能优化版(上下文-人物固定)', + '【LaiTool】分镜大师-MJ古风版(上下文-人物场景固定-MJ古风提示词)': '【LaiTool】分镜大师-MJ古风版(上下文-人物场景固定-MJ古风提示词)', + '【LaiTool】分镜大师-SD英文版(上下文-人物场景固定-SD-英文提示词)': '【LaiTool】分镜大师-SD英文版(上下文-人物场景固定-SD-英文提示词)', + '【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物自动推理)': '【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物自动推理)', + "没有找到对应的AI选项,请先检查配置": "没有找到对应的AI选项,请先检查配置", + '添加分割标识符': '添加分割标识符', + '获取特殊字符失败:{error}': '获取特殊字符失败:{error}', + '格式化成功': '格式化成功', + '格式化失败:{error}': '格式化失败:{error}', + "添加分割符": '添加分割符', + "请输入分割符": '请输入分割符', + "正在初始化文案处理模块": "正在初始化文案处理模块", + "选择分类失败": "选择分类失败", + "设置已更新": "设置已更新", + "设置更新失败,{error}": "设置更新失败,{error}", + "数据加载失败": "数据加载失败", + "分割合并文案成功": "分割合并文案成功", + "分割合并文案失败, {error}": "分割合并文案失败, {error}", + "分割合并文案失败:没有数据({data})": "分割合并文案失败:没有数据({data})", + "初始化文案处理设置失败,{error}": "初始化文案处理设置失败,{error}", + "同步/初始化/加载 文案处理界面数据成功": "同步/初始化/加载 文案处理界面数据成功", + "初始化AI设置失败,{error}": "初始化AI设置失败,{error}", + "同步/初始化/加载 文案处理AI设置成功": "同步/初始化/加载 文案处理AI设置成功", + "数据加载失败,{error}": "数据加载失败,{error}", + "没有选择任何分类,请重新选择": "没有选择任何分类,请重新选择", + "没有选择有效的分类,请重新选择": "没有选择有效的分类,请重新选择", + 'API 设置': 'API 设置', + "AI生成设置": 'AI生成设置', + "是否流式发送": '是否流式发送', + "是否拆分发送": '是否拆分发送', + "单次最大字符数": '单次最大字符数', + '注意:爆款开头不要拆分发送': '注意:爆款开头不要拆分发送', + '同步通用设置Key': '同步通用设置Key', + '测试连接': '测试连接', + '请先填写完整的API设置后再进行测试!': '请先填写完整的API设置后再进行测试!', + '测试连接失败': '测试连接失败', + '测试连接失败: {error}': '测试连接失败: {error}', + '测试连接失败,返回结果异常,请检查设置!': '测试连接失败,返回结果异常,请检查设置!', + '测试连接成功': '测试连接成功', + '测试连接成功!请保存数据后使用!': '测试连接成功!请保存数据后使用!', + '确认要同步 “设置 -> 推理设置” 中的 “API Key” 和 “推理模型” 吗?这将覆盖当前设置。': '确认要同步 “设置 -> 推理设置” 中的 “API Key” 和 “推理模型” 吗?这将覆盖当前设置。', + '取消同步': '取消同步', + '已同步通用设置,请测试成功后保存后使用!': '已同步通用设置,请测试成功后保存后使用!', + '同步失败': '同步失败', + '同步通用设置失败,错误信息:{error}': '同步通用设置失败,错误信息:{error}', + '确认保存设置?该操作不会检测数据的可用性,请确保数据填写正确!!!': '确认保存设置?该操作不会检测数据的可用性,请确保数据填写正确!!!', + '请填写完整选择的AI相关的设置!': '请填写完整选择的AI相关的设置!', + '处理前文本': '处理前文本', + '导入需要AI处理的文本,新导入的数据会覆盖当前的旧数据': '导入需要AI处理的文本,新导入的数据会覆盖当前的旧数据', + '导入文本': '导入文本', + '取消导入操作': '取消导入操作', + '导入失败:文本不能为空': '导入失败:文本不能为空', + '导入失败': '导入失败:{error}', + '当前已经存在数据,继续操作会删除之前的数据,包括生成之后的数据,若只是简单调整数据,可在外面显示的表格中进行直接修改,是否继续?': '当前已经存在数据,继续操作会删除之前的数据,包括生成之后的数据,若只是简单调整数据,可在外面显示的表格中进行直接修改,是否继续?', + '将当前文本按照设定的单次最大次数进行分割,分割后会清空已生成的内容': '将当前文本按照设定的单次最大次数进行分割,分割后会清空已生成的内容', + '文案分割': '文案分割', + '将所有分割出来的文案进行AI处理,再处理之前会删除当前已有的处理后文本,若是需要生成部分,请使用单个的生成': '将所有分割出来的文案进行AI处理,再处理之前会删除当前已有的处理后文本,若是需要生成部分,请使用单个的生成', + '全部生成': '全部生成', + '请输入文本': '请输入文本', + '处理后文本': '处理后文本', + '将所有的处理后文本合并显示,在里面可以格式化和一键复制,但是在里面对于文本的操作不会被保存,修改后请及时复制': '将所有的处理后文本合并显示,在里面可以格式化和一键复制,但是在里面对于文本的操作不会被保存,修改后请及时复制', + '显示生成文本': '显示生成文本', + '将所有处理后文本合并之后,直接复制到剪贴板': '将所有处理后文本合并之后,直接复制到剪贴板', + '复制生成文本': '复制生成文本', + '将所有生成后文本为空的数据进行AI处理,有数据的则跳过': '将所有生成后文本为空的数据进行AI处理,有数据的则跳过', + '生成失败:不存在未生成的文本!': '生成失败:不存在未生成的文本!', + '生成空文本': '生成空文本', + '清空所有的生成后文本,数据删除后不可恢复': '清空所有的生成后文本,数据删除后不可恢复', + 'AI 改写后的文本': 'AI 改写后的文本', + 'AI生成文本': 'AI生成文本', + '确定要清空所有的AI生成文本吗?清空后不可恢复,是否继续?': '确定要清空所有的AI生成文本吗?清空后不可恢复,是否继续?', + '清空成功': '清空成功', + '清空失败:': '清空失败:{error}', + '取消清空': '取消清空', + '直接复制会将所有的AI生成后的数据直接进行复制,不会进行格式之类的调整,若有需求可以再下面表格直接修改,或者是再左边的显示生成文本中修改,是否继续复制?': '直接复制会将所有的AI生成后的数据直接进行复制,不会进行格式之类的调整,若有需求可以再下面表格直接修改,或者是再左边的显示生成文本中修改,是否继续复制?', + '复制失败:存在未生成的文本,请先生成文本!': '复制失败:存在未生成的文本,请先生成文本!', + '复制成功': '复制成功', + '复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}': '复制失败,请在左边的显示生成文本中进行手动复制,失败信息如下:{error}', + '复制失败:{error}': '复制失败:{error}', + "复制失败,请手动复制:{data}": "复制失败,请手动复制:{data}", + '取消复制': '取消复制', + '批量生成确认': '批量生成确认', + '生成确认': '生成确认', + '确定要重新生成选中的 {count} 行AI生成文本吗?重新生成后会清空之前的生成文本并且不可恢复,是否继续?': '确定要重新生成选中的 {count} 行AI生成文本吗?重新生成后会清空之前的生成文本并且不可恢复,是否继续?', + '确定重新生成当前行的AI生成文本吗?重新生成后会清空之前的生成文本并且不可恢复,是否继续?': '确定重新生成当前行的AI生成文本吗?重新生成后会清空之前的生成文本并且不可恢复,是否继续?', + '批量生成中 ({count} 条)......': '批量生成中 ({count} 条)......', + '生成中......': '生成中......', + '生成失败:未找到对应的数据': '生成失败:未找到对应的数据', + '警告:{allCount} 个ID未找到对应数据,将生成 {validCount} 条记录': '警告:{allCount} 个ID未找到对应数据,将生成 {validCount} 条记录', + '批量生成完成,共处理 {conut} 条记录': '批量生成完成,共处理 {conut} 条记录', + '确定要清空当前行的AI生成文本吗?数据删除后不可恢复,是否继续?': '确定要清空当前行的AI生成文本吗?数据删除后不可恢复,是否继续?', + '清空失败:未找到对应的数据': '清空失败:未找到对应的数据', + '生成完成': '生成完成', + '生成失败:{error}': '生成失败:{error}', + '取消批量生成': '取消批量生成', + '取消生成': '取消生成', + '确定要将当前文本按照设定的单次最大次数进行分割吗?分割后会清空已生成的内容,是否继续?': '确定要将当前文本按照设定的单次最大次数进行分割吗?分割后会清空已生成的内容,是否继续?', + '分割失败:{error}': '分割失败:{error}', + '取消分割': '取消分割', + '确定要将所有的文案进行AI生成吗?在生成前会将生成后文本删除,是否继续?': '确定要将所有的文案进行AI生成吗?在生成前会将生成后文本删除,是否继续?', + '批量生成失败:{error}': '批量生成失败:{error}', + '批量生成完成': '批量生成完成', + '注意:这边的格式化不一定会完全格式化,需要自己手动检查': '注意:这边的格式化不一定会完全格式化,需要自己手动检查', + '注意:当前弹窗的修改和格式化只在该界面有效,关闭或重新打开会重新加载同步外部表格数据,之前修改的数据会丢失': '注意:当前弹窗的修改和格式化只在该界面有效,关闭或重新打开会重新加载同步外部表格数据,之前修改的数据会丢失', + //#endregion + + //#region 菜单 + "首页": "首页", + "原创": "原创", + "转视频": "转视频", + "预设库": "预设库", + "文案处理": "文案处理", + "工具箱": "工具箱", + "任务": "任务", + "文档": "文档", + "关于": "关于", + "设置": "设置", + "通用设置": "通用设置", + "推理设置": "推理设置", + "MJ设置": "MJ设置", + "SD/FLUX设置": "SD/FLUX设置", + "ComfyUI 设置": "ComfyUI 设置", + '剪映关键帧设置': "剪映关键帧设置", + '外观': "外观", + //#endregion + + //#region + + "按钮,风格预设": "风格预设", + "按钮,场景预设": "场景预设", + "按钮,角色预设": "角色预设", + + "按钮,单句推理": "单句推理", + "按钮,下推理": "下推理", + "按钮,中文to英文": "中文 2 英文", + "按钮,命令预览": "命令预览", + + "按钮,单句生图": "单句生图", + "按钮,下生图": "下生图", + "按钮,锁定图片": "锁定图片", + "按钮,清空图片": "清空图片", + "按钮,导入图片": "导入图片", + + //#endregion +} \ No newline at end of file diff --git a/src/i18n/locales/zh-tw.ts b/src/i18n/locales/zh-tw.ts new file mode 100644 index 0000000..56004c9 --- /dev/null +++ b/src/i18n/locales/zh-tw.ts @@ -0,0 +1 @@ +export default {} \ No newline at end of file diff --git a/src/i18n/main.ts b/src/i18n/main.ts new file mode 100644 index 0000000..b3dec80 --- /dev/null +++ b/src/i18n/main.ts @@ -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 diff --git a/src/i18n/types.ts b/src/i18n/types.ts new file mode 100644 index 0000000..0f83a60 --- /dev/null +++ b/src/i18n/types.ts @@ -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 +} diff --git a/src/i18n/utils/helpers.ts b/src/i18n/utils/helpers.ts new file mode 100644 index 0000000..b253c0a --- /dev/null +++ b/src/i18n/utils/helpers.ts @@ -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 { + 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 = { + '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, + params2?: Record +): 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 | 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 +} diff --git a/src/main/service/ai/aiStoryboard.ts b/src/main/service/ai/aiStoryboard.ts index 1db6de0..58823fa 100644 --- a/src/main/service/ai/aiStoryboard.ts +++ b/src/main/service/ai/aiStoryboard.ts @@ -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分镜处理类 @@ -31,21 +32,20 @@ export class AIStoryboard extends BookBasicHandle { try { // 初始化书籍基础处理器 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,13 +65,13 @@ export class AIStoryboard extends BookBasicHandle { } else if (type === 'short') { request = cloneDeep(AIWordMergeShort) } else { - throw new Error('不支持的分镜合并类型') + throw new Error(t('不支持的分镜合并类型')) } - + // 将当前分组的文本合并为一个字符串 const element = groupWord[i] let newWord = element.map((item) => item).join('\n') - + // 替换消息模板中的占位符为实际文本内容 for (let j = 0; j < request.messages.length; j++) { const messageItem = request.messages[j] @@ -98,8 +98,7 @@ export class AIStoryboard extends BookBasicHandle { result.push(res) } - - console.log('分镜合并结果:', result) + return result } catch (error) { throw error diff --git a/src/main/service/aiReason/aiReasonCommon.ts b/src/main/service/aiReason/aiReasonCommon.ts index b5f7cad..8ea537d 100644 --- a/src/main/service/aiReason/aiReasonCommon.ts +++ b/src/main/service/aiReason/aiReasonCommon.ts @@ -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( 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, { diff --git a/src/main/service/book/bookIndex/bookExportEntrance.ts b/src/main/service/book/bookIndex/bookExportEntrance.ts index fc19e5e..2649aa8 100644 --- a/src/main/service/book/bookIndex/bookExportEntrance.ts +++ b/src/main/service/book/bookIndex/bookExportEntrance.ts @@ -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 } diff --git a/src/main/service/book/bookIndex/bookVideoEntrance.ts b/src/main/service/book/bookIndex/bookVideoEntrance.ts new file mode 100644 index 0000000..265a632 --- /dev/null +++ b/src/main/service/book/bookIndex/bookVideoEntrance.ts @@ -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 + ) { + return this.bookVideoServiceHandle.UpdateBookTaskDetailVideoMessage( + bookTaskDetailId, + videoMessage + ) + } + + /** 图文转视频 */ + async MediaToVideo(task: TaskModal.Task) { + return this.bookVideoServiceHandle.MediaToVideo(task) + } +} diff --git a/src/main/service/book/index.ts b/src/main/service/book/index.ts index 9da9fbb..92fd352 100644 --- a/src/main/service/book/index.ts +++ b/src/main/service/book/index.ts @@ -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() { diff --git a/src/main/service/book/subBookHandle/bookBasicHandle.ts b/src/main/service/book/subBookHandle/bookBasicHandle.ts index 56010f2..09bc1d5 100644 --- a/src/main/service/book/subBookHandle/bookBasicHandle.ts +++ b/src/main/service/book/subBookHandle/bookBasicHandle.ts @@ -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) { diff --git a/src/main/service/book/subBookHandle/bookVideoHandle.ts b/src/main/service/book/subBookHandle/bookExportHandle.ts similarity index 90% rename from src/main/service/book/subBookHandle/bookVideoHandle.ts rename to src/main/service/book/subBookHandle/bookExportHandle.ts index 409c4cc..2c13361 100644 --- a/src/main/service/book/subBookHandle/bookVideoHandle.ts +++ b/src/main/service/book/subBookHandle/bookExportHandle.ts @@ -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('字幕内容信息不完整')) } } @@ -190,10 +191,10 @@ export class BookVideoHandle extends BookBasicHandle { // 处理子字幕数组,同样进行时间单位转换 element.subValue = Array.isArray(element.subValue) ? (element.subValue as BookTaskDetail.CopywritingSubValue[]).map((sub) => { - sub.start_time = sub.start_time / 1000 - sub.end_time = sub.end_time / 1000 - return sub - }) + sub.start_time = sub.start_time / 1000 + sub.end_time = sub.end_time / 1000 + return sub + }) : [] // 构建单帧数据对象 @@ -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' ) } diff --git a/src/main/service/book/subBookHandle/bookImageHandle.ts b/src/main/service/book/subBookHandle/bookImageHandle.ts index 79d9598..d6f6732 100644 --- a/src/main/service/book/subBookHandle/bookImageHandle.ts +++ b/src/main/service/book/subBookHandle/bookImageHandle.ts @@ -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 { 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 { 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 { 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,54 +582,47 @@ 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' + ) + } } /** * 获取Midjourney图片URL并下载应用到分镜 - * + * * 该方法从MJ API获取已生成任务的图片URL,下载这些图片并应用到相应分镜中。 * 支持批量处理整个书籍任务的分镜,或单独处理指定的单个分镜。下载过程包括 * 获取MJ图片链接、下载图片、分割处理、保存到项目目录和更新数据库信息。 - * + * * 注意:此方法仅在MJ API模式下有效,其他图片生成模式不支持此操作。 - * - * @param {string} id - 目标ID,可以是书籍任务ID或分镜ID,取决于operateBookType参数 - * @param {OperateBookType} operateBookType - 操作类型,指定id参数代表的是书籍任务还是单个分镜 - * - BOOKTASK: 处理整个书籍任务下的所有分镜 - * - BOOKTASKDETAIL: 仅处理单个指定分镜 - * @param {boolean} coverData - 是否覆盖现有图片数据 - * - true: 覆盖所有分镜的图片数据,包括已有图片 - * - false: 仅处理尚未有图片的分镜 - * - * @returns {Promise} - * 成功时返回包含下载和处理后的图片数据的对象数组,失败时返回错误信息 + * @returns {Promise} + 成功时返回包含下载和处理后的图片数据的对象数组,失败时返回错误信息 * * @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( 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' ) } diff --git a/src/main/service/book/subBookHandle/bookPromptHandle.ts b/src/main/service/book/subBookHandle/bookPromptHandle.ts index 9dd2d23..a65b40b 100644 --- a/src/main/service/book/subBookHandle/bookPromptHandle.ts +++ b/src/main/service/book/subBookHandle/bookPromptHandle.ts @@ -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( generalSettingOption, - '‘设置 -> 通用设置’' + t('设置 -> 通用设置') ) let tasks = [] as Array<() => Promise> @@ -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 => { 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' ) } diff --git a/src/main/service/book/subBookHandle/bookServiceHandle.ts b/src/main/service/book/subBookHandle/bookServiceHandle.ts index e6e061a..bdba0c9 100644 --- a/src/main/service/book/subBookHandle/bookServiceHandle.ts +++ b/src/main/service/book/subBookHandle/bookServiceHandle.ts @@ -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' ) } diff --git a/src/main/service/book/subBookHandle/bookTaskDetailServiceHandle.ts b/src/main/service/book/subBookHandle/bookTaskDetailServiceHandle.ts index 35141a3..5931e7d 100644 --- a/src/main/service/book/subBookHandle/bookTaskDetailServiceHandle.ts +++ b/src/main/service/book/subBookHandle/bookTaskDetailServiceHandle.ts @@ -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 { 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 { 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 { 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 { 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' ) } diff --git a/src/main/service/book/subBookHandle/bookTaskServiceHandle.ts b/src/main/service/book/subBookHandle/bookTaskServiceHandle.ts index 7a7d070..91a1e02 100644 --- a/src/main/service/book/subBookHandle/bookTaskServiceHandle.ts +++ b/src/main/service/book/subBookHandle/bookTaskServiceHandle.ts @@ -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 { - 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 { 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 { 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 = {} // 开始处理所有的批次 @@ -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' ) } @@ -679,26 +682,26 @@ export class BookTaskServiceHandle extends BookBasicHandle { /** * 一拆四操作 - 根据子图数量最少的分镜创建多个批次任务 - * + * * 该方法用于将一个小说批次任务中的子图分别创建为独立的批次任务。 * 例如:如果某个分镜有4张子图,则会创建4个新的批次任务,每个任务使用其中一张子图。 - * + * * 工作流程: * 1. 获取指定批次任务及其所有分镜详情 * 2. 检查所有分镜是否都有子图,并找到子图数量最少的分镜 * 3. 根据最少的子图数量决定可以创建多少个新批次 * 4. 调用复制方法创建新的批次任务,每个新批次使用不同的子图 - * + * * @param {string} bookTaskId - 需要进行一拆四操作的小说批次任务ID * @returns {Promise} 操作结果 * - 成功时返回成功消息 * - 失败时返回错误信息(如分镜数据不足、子图缺失等) - * + * * @example * // 假设某个批次有3个分镜,分别有4、3、5张子图 * // 则以最少的3张为基准,创建3个新批次任务 * // 每个新批次都包含3个分镜,但使用不同索引的子图 - * + * * @throws {Error} 当以下情况发生时抛出错误: * - 找不到对应的小说任务 * - 没有分镜任务数据 @@ -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) 个新批次 @@ -753,13 +754,15 @@ export class BookTaskServiceHandle extends BookBasicHandle { copyCount - 1, CopyImageType.ONE ) - + // 返回成功结果 - 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' ) } diff --git a/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts b/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts new file mode 100644 index 0000000..e997804 --- /dev/null +++ b/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts @@ -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( + OptionKeyName.Software.GeneralSetting, + t("设置 -> 通用设置") + ) + let defaultVideoMode = generalSettingString.defaultImg2Video ?? ImageToVideoModels.MJ_VIDEO + + let inferenceSetting = + this.optionRealmService.GetOptionDataByKey( + 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 + ) { + 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 +} diff --git a/src/main/service/common/srtHandle.ts b/src/main/service/common/srtHandle.ts index 5e40258..198219f 100644 --- a/src/main/service/common/srtHandle.ts +++ b/src/main/service/common/srtHandle.ts @@ -1,4 +1,5 @@ import { CheckFileOrDirExist } from '@/define/Tools/file' +import { t } from '@/i18n' import fs from 'fs' import { isEmpty } from 'lodash' @@ -9,7 +10,7 @@ interface SrtDataModel { } export class SrtHandle { - constructor() {} + constructor() { } /** * 获取srt文件数据 @@ -23,13 +24,15 @@ export class SrtHandle { async GetSrtDataByPath(srtPath: string): Promise { 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') diff --git a/src/main/service/jianying/jianyingService.ts b/src/main/service/jianying/jianyingService.ts index eb22474..83d320c 100644 --- a/src/main/service/jianying/jianyingService.ts +++ b/src/main/service/jianying/jianyingService.ts @@ -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( 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] } diff --git a/src/main/service/mj/mjApiService.ts b/src/main/service/mj/mjApiService.ts index 316ab27..35d7b78 100644 --- a/src/main/service/mj/mjApiService.ts +++ b/src/main/service/mj/mjApiService.ts @@ -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 { + async InitMJSetting(outputMode?: ImageGenerateMode): Promise { 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设置') + })) } } @@ -287,8 +311,8 @@ export class MJApiService extends MJBasic { imagePath: resData.imageUrl, imageUrls: resData.imageUrls ? resData.imageUrls - .filter((item) => item.url != null && !isEmpty(item.url)) - .map((item) => item.url) + .filter((item) => item.url != null && !isEmpty(item.url)) + .map((item) => item.url) : [], messageId: taskId, status: status, @@ -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 { // 这边校验是不是在提示词包含不正确的链接 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,表示队列已满,需要重新请求 diff --git a/src/main/service/mj/mjBasic.ts b/src/main/service/mj/mjBasic.ts index a5fa50c..623a296 100644 --- a/src/main/service/mj/mjBasic.ts +++ b/src/main/service/mj/mjBasic.ts @@ -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( generalSetting, - '‘设置 -> MJ设置’' + t('设置 -> MJ设置') ) } @@ -93,7 +94,7 @@ export class MJBasic { let apiSetting = this.optionRealmService.GetOptionByKey(OptionKeyName.Midjourney.ApiSetting) this.mjApiSetting = optionSerialization( apiSetting, - '‘设置 -> MJ设置 -> API设置’' + t('设置 -> MJ设置 -> API模式') ) } @@ -116,7 +117,7 @@ export class MJBasic { ) this.mjPackageSetting = optionSerialization( packageSetting, - '‘设置 -> MJ设置 -> 生图包设置’' + t("设置 -> MJ设置 -> 生图包模式") ) } @@ -140,7 +141,7 @@ export class MJBasic { ) this.mjRemoteSetting = optionSerialization( remoteSetting, - '‘设置 -> MJ设置 -> 代理模式设置’' + t("设置 -> MJ设置 -> 代理模式") ) } @@ -162,7 +163,7 @@ export class MJBasic { let localSetting = this.optionRealmService.GetOptionByKey(OptionKeyName.Midjourney.LocalSetting) this.mjLocalSetting = optionSerialization( localSetting, - '‘设置 -> MJ设置 -> 本地代理设置’' + t("设置 -> MJ设置 -> 本地代理模式") ) } } diff --git a/src/main/service/mj/mjServiceHandle.ts b/src/main/service/mj/mjServiceHandle.ts index 02a2df8..3b15ff1 100644 --- a/src/main/service/mj/mjServiceHandle.ts +++ b/src/main/service/mj/mjServiceHandle.ts @@ -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 { 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++) { diff --git a/src/main/service/option/optionCommonService.ts b/src/main/service/option/optionCommonService.ts index e9cd71b..e5d000d 100644 --- a/src/main/service/option/optionCommonService.ts +++ b/src/main/service/option/optionCommonService.ts @@ -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 } diff --git a/src/main/service/option/optionOptions.ts b/src/main/service/option/optionOptions.ts index 515aa97..35415eb 100644 --- a/src/main/service/option/optionOptions.ts +++ b/src/main/service/option/optionOptions.ts @@ -1,10 +1,11 @@ import { OptionRealmService } from '@/define/db/service/optionService' import { OptionType } from '@/define/enum/option' import { ErrorItem, SuccessItem } from '@/define/model/generalResponse' -import { errorMessage, successMessage } from '@/public/generalTools' +import { t } from '@/i18n' +import { errorMessage, successMessage } from '@/public/generalTools' export class OptionOptions { optionRealmService!: OptionRealmService - constructor() {} + constructor() { } /** 初始化数据库服务 */ async InitService() { @@ -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' ) } diff --git a/src/main/service/option/optionSerialization.ts b/src/main/service/option/optionSerialization.ts index 4e6d53b..de52c43 100644 --- a/src/main/service/option/optionSerialization.ts +++ b/src/main/service/option/optionSerialization.ts @@ -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(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 = ( 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') diff --git a/src/main/service/preset/presetBasicService.ts b/src/main/service/preset/presetBasicService.ts index 4e2f3ca..c704faf 100644 --- a/src/main/service/preset/presetBasicService.ts +++ b/src/main/service/preset/presetBasicService.ts @@ -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('不支持的合并类型')) } } } diff --git a/src/main/service/preset/presetServiceHandle.ts b/src/main/service/preset/presetServiceHandle.ts index ce702d9..5a4f632 100644 --- a/src/main/service/preset/presetServiceHandle.ts +++ b/src/main/service/preset/presetServiceHandle.ts @@ -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' ) } diff --git a/src/main/service/sd/comfyUIServiceHandle.ts b/src/main/service/sd/comfyUIServiceHandle.ts index dd4055a..d1d23ce 100644 --- a/src/main/service/sd/comfyUIServiceHandle.ts +++ b/src/main/service/sd/comfyUIServiceHandle.ts @@ -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( comfyuiSimpleSettingOption, - '设置 -> ComfyUI 设置' + t("设置 -> ComfyUI 设置") ) let comfyuiWorkFlowSettingOption = optionRealmService.GetOptionByKey( @@ -162,12 +159,12 @@ export class ComfyUIServiceHandle extends SDServiceHandle { let comfyuiWorkFlowList = optionSerialization( 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 { 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 生图失败,详细失败信息看启动器控制台') } } } diff --git a/src/main/service/sd/fluxServiceHandle.ts b/src/main/service/sd/fluxServiceHandle.ts index e00d505..23e11d3 100644 --- a/src/main/service/sd/fluxServiceHandle.ts +++ b/src/main/service/sd/fluxServiceHandle.ts @@ -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, diff --git a/src/main/service/sd/sdServiceHandle.ts b/src/main/service/sd/sdServiceHandle.ts index 6baa501..d9c6c59 100644 --- a/src/main/service/sd/sdServiceHandle.ts +++ b/src/main/service/sd/sdServiceHandle.ts @@ -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, diff --git a/src/main/service/setting/settingService.ts b/src/main/service/setting/settingService.ts index e33be7c..fddef48 100644 --- a/src/main/service/setting/settingService.ts +++ b/src/main/service/setting/settingService.ts @@ -7,12 +7,13 @@ 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() {} + constructor() { } /** 初始化数据库服务 */ - async InitService() {} + async InitService() { } //#region 剪映设置 @@ -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' ) } diff --git a/src/main/service/system/electronInterface.ts b/src/main/service/system/electronInterface.ts index f2ca660..9049219 100644 --- a/src/main/service/system/electronInterface.ts +++ b/src/main/service/system/electronInterface.ts @@ -1,22 +1,24 @@ 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 } /** 一些对electron接口的封装,配合业务逻辑 */ export default class ElectronInterface { - constructor() {} + constructor() { } /** * 打开指定的文件,试用默认的打开方式 @@ -25,10 +27,14 @@ export default class ElectronInterface { */ public async OpenFile(value: string): Promise { 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) { @@ -201,35 +219,37 @@ export default class ElectronInterface { filters: extensions && extensions.length > 0 ? [ - { name: 'Audio Files', extensions }, - { name: 'All Files', extensions: ['*'] } - ] + { name: 'Audio Files', extensions }, + { 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 { + 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' + ) + } + } } diff --git a/src/main/service/system/userSoftware.ts b/src/main/service/system/userSoftware.ts index c568a00..2a4b163 100644 --- a/src/main/service/system/userSoftware.ts +++ b/src/main/service/system/userSoftware.ts @@ -1,7 +1,8 @@ +import { t } from '@/i18n' import { errorMessage } from '@/public/generalTools' export class UserSoftware { - constructor() {} + constructor() { } /** * 同步授权信息 @@ -19,7 +20,7 @@ export class UserSoftware { } console.log('授权信息', global.am) } catch (error) { - errorMessage('同步授权信息失败', 'SystemIpc_SyncAuthorization') + errorMessage(t("同步授权信息失败"), 'SystemIpc_SyncAuthorization') } } } diff --git a/src/main/service/task/index.ts b/src/main/service/task/index.ts index 41dbcc8..82dcacf 100644 --- a/src/main/service/task/index.ts +++ b/src/main/service/task/index.ts @@ -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) diff --git a/src/main/service/task/taskManage.ts b/src/main/service/task/taskManage.ts index 4298610..bb72303 100644 --- a/src/main/service/task/taskManage.ts +++ b/src/main/service/task/taskManage.ts @@ -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' ) } diff --git a/src/main/service/task/taskServiceHandle.ts b/src/main/service/task/taskServiceHandle.ts index e3821aa..561aabf 100644 --- a/src/main/service/task/taskServiceHandle.ts +++ b/src/main/service/task/taskServiceHandle.ts @@ -1,14 +1,15 @@ 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 - constructor() {} + constructor() { } private async InitTaskServiceHandle() { if (!this.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} - * 返回操作结果,成功则包含任务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 { 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' ) } diff --git a/src/main/service/translate/translateCommon.ts b/src/main/service/translate/translateCommon.ts index 5aa20c3..6fe9b4e 100644 --- a/src/main/service/translate/translateCommon.ts +++ b/src/main/service/translate/translateCommon.ts @@ -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 { /** 请求的地址 */ @@ -28,7 +29,7 @@ export class TranslateCommon { optionRealmService!: OptionRealmService - constructor() {} + constructor() { } /** * 初始化翻译设置 @@ -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) { diff --git a/src/main/service/translate/translateServiceHandle.ts b/src/main/service/translate/translateServiceHandle.ts index 43fd14a..b20491f 100644 --- a/src/main/service/translate/translateServiceHandle.ts +++ b/src/main/service/translate/translateServiceHandle.ts @@ -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( 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' ) } diff --git a/src/main/service/video/index.ts b/src/main/service/video/index.ts new file mode 100644 index 0000000..51af892 --- /dev/null +++ b/src/main/service/video/index.ts @@ -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) + } +} diff --git a/src/main/service/video/lumaVideo.ts b/src/main/service/video/lumaVideo.ts new file mode 100644 index 0000000..5991eb4 --- /dev/null +++ b/src/main/service/video/lumaVideo.ts @@ -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) + } +} diff --git a/src/main/service/video/mjVideo.ts b/src/main/service/video/mjVideo.ts new file mode 100644 index 0000000..d5de5d3 --- /dev/null +++ b/src/main/service/video/mjVideo.ts @@ -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 + * @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} 无返回值的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 +} diff --git a/src/main/service/write/copyWritingServiceHandle.ts b/src/main/service/write/copyWritingServiceHandle.ts index 95f88ac..1ce2eee 100644 --- a/src/main/service/write/copyWritingServiceHandle.ts +++ b/src/main/service/write/copyWritingServiceHandle.ts @@ -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( 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( 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' ) } diff --git a/src/preload/subPreload/book.ts b/src/preload/subPreload/book.ts index 0cb622d..2f4d46b 100644 --- a/src/preload/subPreload/book.ts +++ b/src/preload/subPreload/book.ts @@ -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 } diff --git a/src/preload/subPreload/bookProload/bookVideoPreload.ts b/src/preload/subPreload/bookProload/bookVideoPreload.ts new file mode 100644 index 0000000..e029789 --- /dev/null +++ b/src/preload/subPreload/bookProload/bookVideoPreload.ts @@ -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 + ) => { + return await ipcRenderer.invoke( + DEFINE_STRING.BOOK.UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE, + bookTaskDetailId, + videoMessage + ) + } +} diff --git a/src/preload/subPreload/system.ts b/src/preload/subPreload/system.ts index 24b9cc2..321fece 100644 --- a/src/preload/subPreload/system.ts +++ b/src/preload/subPreload/system.ts @@ -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 系统相关 diff --git a/src/public/generalTools.ts b/src/public/generalTools.ts index 1ff39de..4a77f01 100644 --- a/src/public/generalTools.ts +++ b/src/public/generalTools.ts @@ -1,4 +1,5 @@ import { GeneralResponse } from '@/define/model/generalResponse' +import { t } from '@/i18n' /** * 返回成功的消息,包含code,data,message @@ -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, diff --git a/src/renderer/components.d.ts b/src/renderer/components.d.ts index 43e9237..418b119 100644 --- a/src/renderer/components.d.ts +++ b/src/renderer/components.d.ts @@ -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'] } diff --git a/src/renderer/index.html b/src/renderer/index.html index 329c3e9..77be2c9 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -4,10 +4,22 @@ LaiTool PRO - + 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'; + " + /> --> diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index daa8d3f..38f161a 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -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() diff --git a/src/renderer/src/common/book.ts b/src/renderer/src/common/book.ts index 88126d2..bab8567 100644 --- a/src/renderer/src/common/book.ts +++ b/src/renderer/src/common/book.ts @@ -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("分镜的图片全部存在,可以进行高清处理")) } diff --git a/src/renderer/src/common/initialData.ts b/src/renderer/src/common/initialData.ts index 4b9d33e..1f16f8a 100644 --- a/src/renderer/src/common/initialData.ts +++ b/src/renderer/src/common/initialData.ts @@ -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 } } diff --git a/src/renderer/src/common/task.ts b/src/renderer/src/common/task.ts index 3f77d44..df5e891 100644 --- a/src/renderer/src/common/task.ts +++ b/src/renderer/src/common/task.ts @@ -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对象 + * + * @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 { + 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 { 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 { 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 ({ - common: { - primaryColor: themeStore.menuPrimaryColor, - primaryColorHover: createHoverColor(themeStore.menuPrimaryColor), - primaryColorPressed: createActiveColor(themeStore.menuPrimaryColor), - borderRadius: '6px' - }, - Message: { - // 修改成功消息的颜色 - iconColorSuccess: themeStore.menuPrimaryColor // 这里使用您想要的颜色代码,例如紫色 - }, - Progress: { - fillColor: themeStore.menuPrimaryColor, // 进度条填充颜色 - railColor: 'var(--n-divider-color)', // 进度条轨道颜色 - iconColor: themeStore.menuPrimaryColor, // 图标颜色 - textColorLineOuter: themeStore.menuPrimaryColor, // 外部文字颜色 - textColorLineInner: '#ffffff' // 内部文字颜色 - }, - Slider: { - primaryColor: themeStore.menuPrimaryColor - }, - Button: { - textColorPrimary: themeStore.isDarkMode ? '#ffffff' : undefined, // 主要按钮的文本颜色 - textColorInfo: themeStore.isDarkMode ? '#ffffff' : undefined, // 信息按钮的文本颜色 - textColorSuccess: themeStore.isDarkMode ? '#ffffff' : undefined, // 成功按钮的文本颜色 - textColorWarning: themeStore.isDarkMode ? '#ffffff' : undefined, // 警告按钮的文本颜色 - textColorError: themeStore.isDarkMode ? '#ffffff' : undefined, // 错误按钮的文本颜色 - - // 添加悬停状态文字颜色 - textColorHoverPrimary: themeStore.isDarkMode ? '#eeeeee' : undefined, - textColorHoverInfo: themeStore.isDarkMode ? '#eeeeee' : undefined, - textColorHoverSuccess: themeStore.isDarkMode ? '#eeeeee' : undefined, - textColorHoverWarning: themeStore.isDarkMode ? '#eeeeee' : undefined, - textColorHoverError: themeStore.isDarkMode ? '#eeeeee' : undefined, - - // 添加按下状态文字颜色 - textColorFocusPrimary: themeStore.isDarkMode ? '#dddddd' : undefined, - textColorPressedInfo: themeStore.isDarkMode ? '#dddddd' : undefined, - textColorPressedSuccess: themeStore.isDarkMode ? '#dddddd' : undefined, - textColorPressedWarning: themeStore.isDarkMode ? '#dddddd' : undefined, - textColorPressedError: themeStore.isDarkMode ? '#dddddd' : undefined, - - // 可以根据需要添加其他按钮状态的颜色 - borderHover: themeStore.isDarkMode ? 'rgba(255, 255, 255, 0.2)' : undefined, - borderFocus: themeStore.isDarkMode ? 'rgba(255, 255, 255, 0.25)' : undefined, - borderPressed: themeStore.isDarkMode ? 'rgba(255, 255, 255, 0.3)' : undefined - } -})) diff --git a/src/renderer/src/common/toolData.ts b/src/renderer/src/common/toolData.ts index 124b41c..4fdb410 100644 --- a/src/renderer/src/common/toolData.ts +++ b/src/renderer/src/common/toolData.ts @@ -1,8 +1,9 @@ +import { t } from '@/i18n' import { CloudUploadOutline } from '@vicons/ionicons5' // 工具分类 export const categories = [ - { key: 'media', label: '媒体工具', color: '#2080f0' } + { key: 'media', label: t('媒体工具'), color: '#2080f0' } // { key: 'document', label: '文档处理', color: '#18a058' }, // { key: 'development', label: '开发工具', color: '#f0a020' }, // { key: 'design', label: '设计工具', color: '#d03050' }, @@ -17,12 +18,12 @@ export const toolsData = [ // 媒体工具 { id: 'image-converter', - name: 'LaiTool 图床', - description: '将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接', + name: t('LaiTool 图床'), + description: t('将图片上传到 LaiTool 图床,支持多种图片格式,获得可分享的链接'), category: 'media', icon: CloudUploadOutline, color: '#2080f0', - tags: ['图片', '转换', '格式'], + tags: [t('图片'), t('转换'), t('格式')], quickAccess: true, action: { type: 'route', @@ -33,12 +34,12 @@ export const toolsData = [ }, { id: 'image-compress', - name: '图片压缩助手', - description: '将图片进行压缩,支持多种图片格式,减小文件大小', + name: t('图片压缩助手'), + description: t('将图片进行压缩,支持多种图片格式,减小文件大小'), category: 'media', icon: CloudUploadOutline, color: '#2080f0', - tags: ['图片', '压缩', '格式'], + tags: [t('图片'), t('压缩'), t('格式')], quickAccess: true, action: { type: 'route', diff --git a/src/renderer/src/components/CopyWriting/CWInputWord.vue b/src/renderer/src/components/CopyWriting/CWInputWord.vue index a11aa6a..56a1415 100644 --- a/src/renderer/src/components/CopyWriting/CWInputWord.vue +++ b/src/renderer/src/components/CopyWriting/CWInputWord.vue @@ -1,9 +1,9 @@ - 添加分割标识符 + {{ t('添加分割标识符') }} - + -
- 取消 - 导入 +
+ {{ t('取消') }} + {{ t('导入') }}
- - diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue new file mode 100644 index 0000000..468a45b --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue new file mode 100644 index 0000000..a90df63 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue @@ -0,0 +1,483 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoEmptyState.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoEmptyState.vue new file mode 100644 index 0000000..d7e8145 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoEmptyState.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue new file mode 100644 index 0000000..f4de193 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue @@ -0,0 +1,505 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoImageToVideo.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoImageToVideo.vue new file mode 100644 index 0000000..316a3ad --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoImageToVideo.vue @@ -0,0 +1,475 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoInfo.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoInfo.vue new file mode 100644 index 0000000..2cfcce7 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoInfo.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoSelectParentTask.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoSelectParentTask.vue new file mode 100644 index 0000000..f430725 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoSelectParentTask.vue @@ -0,0 +1,863 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskDetail.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskDetail.vue new file mode 100644 index 0000000..0a72c47 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskDetail.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskList.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskList.vue new file mode 100644 index 0000000..53d86ef --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskList.vue @@ -0,0 +1,641 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskOptions.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskOptions.vue new file mode 100644 index 0000000..17eb4f8 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskOptions.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoConfig.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoConfig.vue new file mode 100644 index 0000000..4320a0b --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoConfig.vue @@ -0,0 +1,156 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue new file mode 100644 index 0000000..24c84a0 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue @@ -0,0 +1,782 @@ + + + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfoHome.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfoHome.vue new file mode 100644 index 0000000..44f2313 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfoHome.vue @@ -0,0 +1,332 @@ + + + + + diff --git a/src/renderer/src/components/Original/Analysis/SceneAnalysis.vue b/src/renderer/src/components/Original/Analysis/SceneAnalysis.vue index 4061b91..3a9d0cf 100644 --- a/src/renderer/src/components/Original/Analysis/SceneAnalysis.vue +++ b/src/renderer/src/components/Original/Analysis/SceneAnalysis.vue @@ -18,27 +18,27 @@ > - 推理场景 + {{ t('场景推理') }} - 手动添加 + {{ t('手动添加') }} - 导入到自定义场景预设 + {{ t('导入到自定义场景预设') }} - 删除选中 + {{ t('删除选中') }} @@ -56,22 +56,62 @@ -

1. 角色分析数据是由当前的小说文案进行分析的结果

- 2. 角色分析的数据只会在部分推理模式中生效,请到 - “设置 -> 推理设置” 中查看哪些推理模式支持 + {{ + t('1. {type}的数据是由当前的小说文案进行分析的结果', { + type: t('场景推理') + }) + }}

-

3. 角色分析的数据会在每次推理时进行更新,推理完成后会自动保存到当前小说的角色数据中

+

- 4. 也可自定义或者修改指定的角色分析数据,点击 ‘编辑’ 按钮进行修改或 - “添加数据” 按钮进行添加 + {{ + t( + '3. {type}的数据会在每次推理时进行更新,推理完成后会自动保存到当前小说的{type}数据中', + { + type: t('场景推理') + } + ) + }}

-

5. 数据修改之后需要手动保存,点击 “保存” 按钮进行保存

-

6. 删除数据后,推理时会自动删除当前小说的角色数据中的对应数据

+

+

- 7. 需要在外部手动选择需要的角色时,请点击 - “导入到自定义角色标签” 按钮进行导入到标签集中 + {{ + t('6. 删除数据后,推理时会自动删除当前小说的{type}中的对应数据', { + type: t('场景推理') + }) + }}

+

@@ -79,7 +119,7 @@ v-model:show="showEditModal" preset="dialog" :show-icon="false" - title="编辑数据" + :title="t('编辑数据')" :style="{ width: '800px' }" @@ -93,21 +133,21 @@ label-placement="left" label-width="100" > - - + + - + - 取消 - 保存 + {{ t('取消') }} + {{ t('保存') }} @@ -140,6 +180,7 @@ import { useBookStore, usePresetStore } from '@/renderer/src/stores' import { PresetCategory } from '@/define/data/presetData' import { ValidateErrorString, ValidateJsonAndParse } from '@/define/Tools/validate' import { isEmpty } from 'lodash' +import { t } from '@/i18n' const bookStore = useBookStore() const presetStore = usePresetStore() @@ -182,12 +223,12 @@ let formRef = ref(null) const rules = { name: { required: true, - message: '请输入场景名称', + message: t('请输入场景名称'), trigger: 'blur' }, prompt: { required: true, - message: '请输入场景提示词', + message: t('请输入场景提示词'), trigger: 'blur' } } @@ -205,17 +246,17 @@ const createColumns = () => { sorter: 'default' }, { - title: '名称', + title: t('名称'), key: 'name', width: 150, sorter: (a, b) => a.name.localeCompare(b.name) }, { - title: '提示词描述', + title: t('提示词'), key: 'prompt' }, { - title: '操作', + title: t('操作'), key: 'actions', width: 200, render(row, index) { @@ -233,7 +274,7 @@ const createColumns = () => { }, { icon: () => h(NIcon, null, { default: () => h(EditIcon) }), - default: () => '编辑' + default: () => t('编辑') } ), h( @@ -245,7 +286,7 @@ const createColumns = () => { }, { icon: () => h(NIcon, null, { default: () => h(DeleteIcon) }), - default: () => '删除' + default: () => t('删除') } ) ] @@ -266,15 +307,17 @@ function handleCheckedRowKeysChange(keys) { // AI自动推理 async function handleAnalysisUser() { let da = dialog.warning({ - title: '场景推理提示', - content: '该操作会将之前的场景数据覆盖,是否继续?', - positiveText: '继续', - negativeText: '取消', + title: t('操作提示'), + content: t('即将开始自动推理,该操作会将之前的 {type} 数据覆盖,是否继续?', { + type: t('场景推理') + }), + positiveText: t('继续'), + negativeText: t('取消'), onPositiveClick: async () => { try { da.destroy() spin.value = true - tip.value = '正在推理场景数据,请稍等...' + tip.value = t('正在推理,请稍等...') let res = await window.book.AutoAnalyzeCharacterOrScene( bookStore.selectBookTaskDetail[0].bookTaskId, PresetCategory.Scene @@ -285,9 +328,9 @@ async function handleAnalysisUser() { // 修改数据 tableData.value = res.data[PresetCategory.Scene] bookStore.selectBookTask.autoAnalyzeCharacter = JSON.stringify(res.data) - message.success('场景推理成功') + message.success(t('场景推理成功')) } catch (error) { - message.error('场景推理失败: ' + error.message) + message.error(t('场景推理失败:{error}', { error: error.message })) } finally { spin.value = false } @@ -358,7 +401,7 @@ function handleSaveRow() { message.error(res.message) } else { bookStore.selectBookTask.autoAnalyzeCharacter = newData - message.success('修改成功') + message.success(t('修改成功')) } showEditModal.value = false }) @@ -374,7 +417,7 @@ function handleSaveRow() { async function handleDeleteRow(row, index) { // const index = tableData.value.findIndex((item) => item.id === row.id) if (index == -1) { - message.error('删除失败,未找到对应的行') + message.error(t('删除失败,未找到对应的行')) return } tableData.value.splice(index, 1) @@ -389,14 +432,14 @@ async function handleDeleteRow(row, index) { message.error(res.message) } else { bookStore.selectBookTask.autoAnalyzeCharacter = newData - message.success(`删除 ${row.name} 成功`) + message.success(t('删除 {name} 成功', { name: row.name })) } } // 批量选中删除 async function handleBatchDelete() { if (selectedRowKeys.value.length === 0) { - message.warning('请先选择要删除的行') + message.warning(t('请先选择要删除的行')) return } @@ -414,7 +457,7 @@ async function handleBatchDelete() { // 导出自定义角色预设库 async function handleExport() { if (selectedRowKeys.value.length === 0) { - message.warning('请先选择要导出的行') + message.warning(t('请先选择要导出的行')) return } @@ -437,10 +480,10 @@ async function handleExport() { return } presetStore.showCharacterPresetArray.unshift(res.data) - message.success(`导入 ${element.name} 到场景预设成功`) + message.success(t('导入 {name} 到场景预设成功', { name: element.name })) } presetStore.presetChangeCount++ - message.success('导入所有选中成功的场景设置到预设中完成') + message.success(t('导入所有选中成功的场景设置到预设中完成')) } diff --git a/src/renderer/src/components/Original/Analysis/UserAnalysis.vue b/src/renderer/src/components/Original/Analysis/UserAnalysis.vue index 7994e29..0b5625e 100644 --- a/src/renderer/src/components/Original/Analysis/UserAnalysis.vue +++ b/src/renderer/src/components/Original/Analysis/UserAnalysis.vue @@ -18,27 +18,27 @@ > - 推理角色 + {{ t('角色推理') }} - 手动添加 + {{ t('手动添加') }} - 导入到自定义角色预设 + {{ t('导入到自定义角色预设') }} - 删除选中 + {{ t('删除选中') }} @@ -56,22 +56,62 @@ -

1. 角色分析数据是由当前的小说文案进行分析的结果

- 2. 角色分析的数据只会在部分推理模式中生效,请到 - “设置 -> 推理设置” 中查看哪些推理模式支持 + {{ + t('1. {type}的数据是由当前的小说文案进行分析的结果', { + type: t('角色推理') + }) + }}

-

3. 角色分析的数据会在每次推理时进行更新,推理完成后会自动保存到当前小说的角色数据中

+

- 4. 也可自定义或者修改指定的角色分析数据,点击 ‘编辑’ 按钮进行修改或 - “添加数据” 按钮进行添加 + {{ + t( + '3. {type}的数据会在每次推理时进行更新,推理完成后会自动保存到当前小说的{type}数据中', + { + type: t('角色推理') + } + ) + }}

-

5. 数据修改之后需要手动保存,点击 “保存” 按钮进行保存

-

6. 删除数据后,推理时会自动删除当前小说的角色数据中的对应数据

+

+

- 7. 需要在外部手动选择需要的角色时,请点击 - “导入到自定义角色标签” 按钮进行导入到标签集中 + {{ + t('6. 删除数据后,推理时会自动删除当前小说的{type}中的对应数据', { + type: t('角色推理') + }) + }}

+

@@ -79,7 +119,7 @@ v-model:show="showEditModal" preset="dialog" :show-icon="false" - title="编辑数据" + :title="t('编辑数据')" :style="{ width: '800px' }" @@ -93,21 +133,21 @@ label-placement="left" label-width="100" > - - + + - + - 取消 - 保存 + {{ t('取消') }} + {{ t('保存') }} @@ -140,6 +180,7 @@ import { useBookStore, usePresetStore } from '@/renderer/src/stores' import { PresetCategory } from '@/define/data/presetData' import { ValidateErrorString, ValidateJsonAndParse } from '@/define/Tools/validate' import { isEmpty } from 'lodash' +import { t } from '@/i18n' const bookStore = useBookStore() const presetStore = usePresetStore() @@ -182,12 +223,12 @@ let formRef = ref(null) const rules = { name: { required: true, - message: '请输入人物名称', + message: t('请输入角色名称'), trigger: 'blur' }, prompt: { required: true, - message: '请输入人物提示词', + message: t('请输入角色提示词'), trigger: 'blur' } } @@ -205,17 +246,17 @@ const createColumns = () => { sorter: 'default' }, { - title: '名称', + title: t('名称'), key: 'name', width: 150, sorter: (a, b) => a.name.localeCompare(b.name) }, { - title: '提示词描述', + title: t('提示词'), key: 'prompt' }, { - title: '操作', + title: t('操作'), key: 'actions', width: 200, render(row, index) { @@ -233,7 +274,7 @@ const createColumns = () => { }, { icon: () => h(NIcon, null, { default: () => h(EditIcon) }), - default: () => '编辑' + default: () => t('编辑') } ), h( @@ -245,7 +286,7 @@ const createColumns = () => { }, { icon: () => h(NIcon, null, { default: () => h(DeleteIcon) }), - default: () => '删除' + default: () => t('删除') } ) ] @@ -266,15 +307,17 @@ function handleCheckedRowKeysChange(keys) { // AI自动推理 async function handleAnalysisUser() { let da = dialog.warning({ - title: '角色推理提示', - content: '该操作会将之前的角色数据覆盖,是否继续?', - positiveText: '继续', - negativeText: '取消', + title: t('操作提示'), + content: t('即将开始自动推理,该操作会将之前的 {type} 数据覆盖,是否继续?', { + type: t('角色推理') + }), + positiveText: t('继续'), + negativeText: t('取消'), onPositiveClick: async () => { try { da.destroy() spin.value = true - tip.value = '正在推理角色数据,请稍等...' + tip.value = t('正在推理,请稍等...') let res = await window.book.AutoAnalyzeCharacterOrScene( bookStore.selectBookTaskDetail[0].bookTaskId, PresetCategory.Character @@ -285,9 +328,13 @@ async function handleAnalysisUser() { // 修改数据 tableData.value = res.data[PresetCategory.Character] bookStore.selectBookTask.autoAnalyzeCharacter = JSON.stringify(res.data) - message.success('角色推理成功') + message.success(t('角色推理成功')) } catch (error) { - message.error('角色推理失败: ' + error.message) + message.error( + t('角色推理失败:{error}', { + error: error.message + }) + ) } finally { spin.value = false } @@ -358,7 +405,7 @@ function handleSaveRow() { message.error(res.message) } else { bookStore.selectBookTask.autoAnalyzeCharacter = newData - message.success('修改成功') + message.success(t('修改成功')) } showEditModal.value = false }) @@ -374,7 +421,7 @@ function handleSaveRow() { async function handleDeleteRow(row, index) { // const index = tableData.value.findIndex((item) => item.id === row.id) if (index == -1) { - message.error('删除失败,未找到对应的行') + message.error(t('删除失败,未找到对应的行')) return } tableData.value.splice(index, 1) @@ -389,14 +436,18 @@ async function handleDeleteRow(row, index) { message.error(res.message) } else { bookStore.selectBookTask.autoAnalyzeCharacter = newData - message.success(`删除 ${row.name} 成功`) + message.success( + t('删除 {name} 成功', { + name: row.name + }) + ) } } // 批量选中删除 async function handleBatchDelete() { if (selectedRowKeys.value.length === 0) { - message.warning('请先选择要删除的行') + message.warning(t('请先选择要删除的行')) return } @@ -414,7 +465,7 @@ async function handleBatchDelete() { // 导出自定义角色预设库 async function handleExport() { if (selectedRowKeys.value.length === 0) { - message.warning('请先选择要导出的行') + message.warning(t('请先选择要导出的行')) return } @@ -437,10 +488,10 @@ async function handleExport() { return } presetStore.showCharacterPresetArray.unshift(res.data) - message.success(`导入 ${element.name} 到角色预设成功`) + message.success(t('导入 {name} 到角色预设成功', { name: element.name })) } presetStore.presetChangeCount++ - message.success('导入所有选中成功的人物角色到预设中完成') + message.success(t('导入所有选中成功的人物角色到预设中完成')) } diff --git a/src/renderer/src/components/Original/BookTaskDetail/AllImagePreview.vue b/src/renderer/src/components/Original/BookTaskDetail/AllImagePreview.vue index adc775a..193b5f2 100644 --- a/src/renderer/src/components/Original/BookTaskDetail/AllImagePreview.vue +++ b/src/renderer/src/components/Original/BookTaskDetail/AllImagePreview.vue @@ -6,7 +6,7 @@
@@ -30,7 +30,7 @@
{{ image.name }}
-
+
@@ -47,6 +47,7 @@ import { ImageOutline } from '@vicons/ionicons5' import { LocateOutline } from '@vicons/ionicons5' // 添加定位图标 import { useBookStore, useThemeStore } from '@/renderer/src/stores' import { isEmpty } from 'lodash' +import { t } from '@/i18n' const bookStore = useBookStore() const themeStore = useThemeStore() @@ -59,14 +60,18 @@ onMounted(() => { if (!isEmpty(item.outImagePath)) { images.value.push({ url: item.outImagePath.split('?t')[0] + '?t=' + Date.now(), // 添加时间戳以避免缓存 - alt: '分镜_' + item.name, + alt: t('分镜_{name}', { + name: item.name + }), name: item.name, id: item.id // 保存ID用于定位 }) } else { images.value.push({ url: '', - alt: '分镜_' + item.name, + alt: t('分镜_{name}', { + name: item.name + }), name: item.name, id: item.id // 保存ID用于定位 }) @@ -102,7 +107,7 @@ const isValidImage = (url) => { // 定位到表格对应行 const locateTableRow = (id) => { if (!id) { - message.warning('无法定位:找不到对应的ID') + message.warning(t('无法定位:找不到对应的ID')) return } @@ -113,7 +118,7 @@ const locateTableRow = (id) => { }) ) - message.success('已定位到对应分镜') + message.success(t('已定位到对应分镜')) } const labelBackgroundColor = computed(() => { diff --git a/src/renderer/src/components/Original/BookTaskDetail/BookTaskDetailTable.vue b/src/renderer/src/components/Original/BookTaskDetail/BookTaskDetailTable.vue index 1fe4ffc..8b2a22d 100644 --- a/src/renderer/src/components/Original/BookTaskDetail/BookTaskDetailTable.vue +++ b/src/renderer/src/components/Original/BookTaskDetail/BookTaskDetailTable.vue @@ -25,9 +25,9 @@ import DatatableGenerateImage from './DatatableGenerateImage.vue' import DataTableAction from './DataTableAction.vue' import DatatableHeaderImage from './DatatableHeaderImage.vue' import DatatableHeaderCharacter from './DatatableHeaderCharacter.vue' +import { t } from '@/i18n' const bookStore = useBookStore() -const presetStore = usePresetStore() const softwareStore = useSoftwareStore() const message = useMessage() @@ -66,7 +66,7 @@ const columns = computed(() => { // title(row) { // return h(DatatableHeaderAfterGpt) // }, - title: '字幕/文案', + title: t('字幕/文案'), key: 'srt', width: '250', className: 'empty-margin', @@ -93,7 +93,7 @@ const columns = computed(() => { // title(row) { // return h(ODataTableHeaderGptPrompt) // }, - title: '提示词', + title: t('提示词'), key: 'gpt_prompt', className: 'empty-margin', @@ -109,7 +109,7 @@ const columns = computed(() => { // title(row) { // return h(ODataTableHeaderPrompt) // }, - title: '提示词命令', + title: t('提示词命令'), key: 'prompt', width: '300', minWidth: softwareStore.showCompletePrompt ? 300 : 0, @@ -120,18 +120,15 @@ const columns = computed(() => { }, { // 参数 - title: '合成视频', + title: t('合成视频'), key: 'generateVideoPath', className: 'empty-margin', width: '200', minWidth: bookStore.selectBookTask.openVideoGenerate ? 130 : 0 - // render(row, index) { - // return h(DataTableGenerateVideo, { bookTaskDetail: row, index: index }) - // } }, { // 出图 - title(row) { + title() { return h(DatatableHeaderImage, { style: ' margin-left: 5px' }) }, key: 'options', @@ -149,7 +146,7 @@ const columns = computed(() => { // title(row) { // return h(BookTaskDetailOptions, { style: 'font-size: 24px;margin-left: 5px' }) // }, - title: '操作', + title: t('操作'), key: 'action', fixed: 'right', // 添加此行,将列固定在右侧 width: '100', @@ -168,19 +165,22 @@ function OriginalGetPromptReturn(value) { console.log(' OriginalGetPromptReturn ', value) if (value.code == 1) { if (!value.id) { - message.error('没有返回的ID') + message.error(t('无效的ID')) } let index = bookStore.selectBookTaskDetail.findIndex((item) => item.id == value.id) if (index == -1) { - message.error('没有找到对应的分镜ID的数据') + message.error(t('未找到指定ID的分镜数据')) } bookStore.selectBookTaskDetail[index].gptPrompt = value.data.content - softwareStore.spin.tip = `正在推理提示词,推理进度 ${value.data.progress.current + 1} / ${value.data.progress.total}` + softwareStore.spin.tip = t('正在推理提示词,推理进度 {current} / {total}', { + current: value.data.progress.current + 1, + total: value.data.progress.total + }) } else { dialog.error({ - title: '失败', + title: t('失败'), content: value.message, - positiveText: '确定' + positiveText: t('确定') }) } } @@ -196,12 +196,15 @@ function OriginalGPTPromptReturn(value) { if (findIndex != -1) { bookStore.selectBookTaskDetail[findIndex].gptPrompt = value.data.prompt } - softwareStore.spin.tip = `翻译中 ... ${value.data.progress + 1} / ${value.data.total}` + softwareStore.spin.tip = t('翻译中 ... {current} / {total}', { + current: value.data.progress + 1, + total: value.data.total + }) } else { dialog.error({ - title: '失败', + title: t('失败'), content: value.message, - positiveText: '确定' + positiveText: t('确定') }) } } @@ -221,7 +224,7 @@ function OriginalMJImageResponseReturn(value) { } if (value.code == 0 || value.data?.status == 'error') { notification.error({ - title: '失败', + title: t('失败'), content: value.message, duration: 3000 }) @@ -242,7 +245,7 @@ function OriginalSDImageResponseReturn(value) { } if (value.code == 0 || value.data?.status == 'error') { notification.error({ - title: '失败', + title: t('失败'), content: value.message, duration: 3000 }) @@ -323,7 +326,7 @@ function scrollToTableRow(event) { const id = event.detail.id let findIndex = bookStore.selectBookTaskDetail.findIndex((item) => item.id == id) if (findIndex == -1) { - message.error('没有找到对应的分镜ID的数据') + message.error(t('未找到指定ID的分镜数据')) return } // 如果表格有ref diff --git a/src/renderer/src/components/Original/BookTaskDetail/DataTableAction.vue b/src/renderer/src/components/Original/BookTaskDetail/DataTableAction.vue index 693a842..fb6ff25 100644 --- a/src/renderer/src/components/Original/BookTaskDetail/DataTableAction.vue +++ b/src/renderer/src/components/Original/BookTaskDetail/DataTableAction.vue @@ -37,16 +37,17 @@ diff --git a/src/renderer/src/components/Original/BookTaskDetail/DatatableAfterGpt.vue b/src/renderer/src/components/Original/BookTaskDetail/DatatableAfterGpt.vue index 6636805..9a5da9f 100644 --- a/src/renderer/src/components/Original/BookTaskDetail/DatatableAfterGpt.vue +++ b/src/renderer/src/components/Original/BookTaskDetail/DatatableAfterGpt.vue @@ -44,8 +44,7 @@ const softwareStore = useSoftwareStore() // 创建一个防抖函数,只创建一次 const debouncedHandleInput = debounce(async () => { - console.log('开始修改') - let res = await window.db.UpdateBookTaskDetailData(data.value.id, { + let res = await await window.book.ModifyBookTaskDetailById(data.value.id, { afterGpt: data.value.afterGpt, word: data.value.afterGpt }) @@ -64,7 +63,7 @@ function InputDebounced() { \ No newline at end of file + diff --git a/src/renderer/src/components/ToolBox/ImageUpload/ImageDisplay.vue b/src/renderer/src/components/ToolBox/ImageUpload/ImageDisplay.vue index 149dca8..4b10deb 100644 --- a/src/renderer/src/components/ToolBox/ImageUpload/ImageDisplay.vue +++ b/src/renderer/src/components/ToolBox/ImageUpload/ImageDisplay.vue @@ -2,13 +2,19 @@
-

已上传图片 ({{ total ?? 0 }})

+

+ {{ + t('已上传图片 {count}', { + count: total ?? 0 + }) + }} +

- 刷新数据 + {{ t('刷新数据') }} - 导出数据 + {{ t('导出数据') }} @@ -64,6 +70,7 @@ import { CopyOutline, ChevronDownOutline // 添加这个导入 } from '@vicons/ionicons5' +import { t } from '@/i18n' // Emits const emit = defineEmits(['paginationChange']) @@ -80,12 +87,16 @@ const message = useMessage() // 导出选项 const exportOptions = [ { - label: '导出为 JSON', + label: t('导出为 {data}', { + data: 'JSON' + }), key: 'json', icon: () => h(NIcon, () => h(DownloadOutline)) }, { - label: '导出为 CSV', + label: t('导出为 {data}', { + data: 'CSV' + }), key: 'csv', icon: () => h(NIcon, () => h(DownloadOutline)) } @@ -104,7 +115,9 @@ const paginationReactive = reactive({ refreshList() }, prefix({ itemCount }) { - return `共 ${itemCount} 条` + return t('共 {count} 条', { + count: itemCount + }) }, onUpdatePageSize: (pageSize) => { paginationReactive.pageSize = pageSize @@ -116,7 +129,7 @@ const paginationReactive = reactive({ // 表格列配置 const columns = [ { - title: '序号', + title: t('序号'), key: 'index', width: 60, render: (row, index) => { @@ -124,7 +137,7 @@ const columns = [ } }, { - title: '预览', + title: t('预览'), key: 'preview', width: 80, render: (row) => { @@ -138,7 +151,7 @@ const columns = [ } }, { - title: '文件地址', + title: t('图片链接'), key: 'url', width: 300, render: (row) => { @@ -167,13 +180,13 @@ const columns = [ } }, { - title: '文件大小', + title: t('文件大小'), key: 'fileSize', width: 100, render: (row) => formatFileSize(row.fileSize) }, { - title: '文件类型', + title: t('文件类型'), key: 'type', width: 100, render: (row) => { @@ -181,13 +194,13 @@ const columns = [ } }, { - title: '上传时间', + title: t('上传时间'), key: 'uploadTime', width: 160, render: (row) => formatDate(row.uploadTime) }, { - title: '操作', + title: t('操作'), key: 'actions', width: 150, fixed: 'right', @@ -201,7 +214,7 @@ const columns = [ quaternary: true, onClick: () => copyBase64(row) }, - { default: () => '复制', icon: () => h(NIcon, () => h(CopyOutline)) } + { default: () => t('复制'), icon: () => h(NIcon, () => h(CopyOutline)) } ) ]) } @@ -211,9 +224,13 @@ const columns = [ async function copyBase64(image) { try { await navigator.clipboard.writeText(image.url) - message.success('文件地址已复制到剪贴板') + message.success(t('文件地址已复制到剪贴板')) } catch (error) { - message.error('复制失败,请手动复制') + message.error( + t('复制失败:{error}', { + error: error.message + }) + ) } } @@ -240,10 +257,19 @@ function exportAsJson() { const data = imageList.value const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) downloadFile(blob, `images_export_${getTimestamp()}.json`) - message.success('JSON 数据导出成功') + message.success( + t('{data} 数据导出成功', { + data: 'JSON' + }) + ) } catch (error) { console.error('JSON 导出失败:', error) - message.error('JSON 导出失败,请重试') + message.error( + t('{data} 导出失败,{error}', { + data: 'JSON', + error: error.message + }) + ) } } @@ -257,7 +283,14 @@ function exportAsCsv() { } // CSV 表头 - const headers = ['序号', '文件名', '文件地址', '文件大小', '文件类型', '上传时间'] + const headers = [ + t('序号'), + t('图片名称'), + t('图片链接'), + t('文件大小'), + t('文件类型'), + t('上传时间') + ] // 转换数据 const csvRows = [ @@ -281,10 +314,19 @@ function exportAsCsv() { const blob = new Blob([bom + csvContent], { type: 'text/csv;charset=utf-8' }) downloadFile(blob, `images_export_${getTimestamp()}.csv`) - message.success('CSV 数据导出成功') + message.success( + t('{data} 数据导出成功', { + data: 'CSV' + }) + ) } catch (error) { console.error('CSV 导出失败:', error) - message.error('CSV 导出失败,请重试') + message.error( + t('{data} 导出失败,{error}', { + data: 'CSV', + error: error.message + }) + ) } } diff --git a/src/renderer/src/components/ToolBox/ImageUpload/ImageUploadHome.vue b/src/renderer/src/components/ToolBox/ImageUpload/ImageUploadHome.vue index e7bbb16..016335b 100644 --- a/src/renderer/src/components/ToolBox/ImageUpload/ImageUploadHome.vue +++ b/src/renderer/src/components/ToolBox/ImageUpload/ImageUploadHome.vue @@ -2,8 +2,8 @@
@@ -24,16 +24,16 @@ - - diff --git a/src/renderer/src/components/ToolBox/ToolGrid.vue b/src/renderer/src/components/ToolBox/ToolGrid.vue index 583696f..6fe8b80 100644 --- a/src/renderer/src/components/ToolBox/ToolGrid.vue +++ b/src/renderer/src/components/ToolBox/ToolGrid.vue @@ -29,12 +29,13 @@
- +
- - diff --git a/src/renderer/src/components/common/DisabledWrapper.vue b/src/renderer/src/components/common/DisabledWrapper.vue index 1b9d10d..114266a 100644 --- a/src/renderer/src/components/common/DisabledWrapper.vue +++ b/src/renderer/src/components/common/DisabledWrapper.vue @@ -19,7 +19,7 @@
- +
@@ -31,6 +31,7 @@ import { computed } from 'vue' import { NWatermark } from 'naive-ui' import { useThemeStore } from '@renderer/stores' import { createLighterColor } from '@/renderer/src/common/color' +import { t } from '@/i18n' const themeStore = useThemeStore() @@ -43,7 +44,7 @@ const props = defineProps({ // 禁用时显示的文本 disabledText: { type: String, - default: '功能已禁用' + default: t('功能未开放') }, // 是否为单行/小组件模式 oneCell: { @@ -167,4 +168,4 @@ const fontColor = computed(() => { cursor: not-allowed; border-radius: inherit; } - \ No newline at end of file + diff --git a/src/renderer/src/components/common/LoadingComponent.vue b/src/renderer/src/components/common/LoadingComponent.vue index 528921c..4998a3f 100644 --- a/src/renderer/src/components/common/LoadingComponent.vue +++ b/src/renderer/src/components/common/LoadingComponent.vue @@ -9,7 +9,12 @@
- + {{ char === ' ' ? '\u00A0' : char }} @@ -27,12 +32,13 @@ diff --git a/src/renderer/src/views/Home.vue b/src/renderer/src/views/Home.vue index 6ac3b33..5f97972 100644 --- a/src/renderer/src/views/Home.vue +++ b/src/renderer/src/views/Home.vue @@ -104,6 +104,7 @@ import { useRouter } from 'vue-router' import CommonDialog from '../components/common/CommonDialog.vue' import { useThemeStore, useMenuStore } from '../stores' import { DEFINE_STRING } from '@/define/ipcDefineString' +import { t } from '@/i18n' const router = useRouter() const themeStore = useThemeStore() @@ -323,12 +324,12 @@ onMounted(() => { window.system.setEventListen([DEFINE_STRING.SYSTEM.SHOW_MESSAGE_DIALOG], (value) => { let message = value.message let type = 'success' - let title = '成功' + let title = t('成功') if (value.code == 0) { type = 'error' - title = '成功但失败' + title = t('失败') } else if (value.code == 2) { - ;(type = 'warning'), (title = '警告') + ;(type = 'warning'), (title = t('警告')) } dialog.create({ type: type, @@ -337,20 +338,20 @@ onMounted(() => { content: message, style: `width : 400px;`, maskClosable: false, - positiveText: '确定' + positiveText: t('确定') }) }) window.system.setEventListen([DEFINE_STRING.SYSTEM.SHOW_MAIN_NOTIFICATION], (value) => { let message = value.message let type = 'success' - let title = '成功' + let title = t('成功') if (value.code == 0) { type = 'error' - title = '失败' + title = t('失败') } else if (value.code == 2) { type = 'warning' - title = '警告' + title = t('警告') } notification.create({ type: type, diff --git a/src/renderer/src/views/LoadingScreen.vue b/src/renderer/src/views/LoadingScreen.vue index 87d86be..ea18d70 100644 --- a/src/renderer/src/views/LoadingScreen.vue +++ b/src/renderer/src/views/LoadingScreen.vue @@ -54,6 +54,7 @@ import { import { useThemeStore, useSoftwareStore } from '@/renderer/src/stores' import { useAuthorization } from '@/renderer/src/hooks/useAuthorization' import { isEmpty } from 'lodash' +import { t } from '@/i18n' const props = defineProps({ isLoading: { @@ -64,17 +65,18 @@ const props = defineProps({ const themeStore = useThemeStore() const softwareStore = useSoftwareStore() -const { validateAuthorization, updateAuthorizationInfo, syncAuthorizationToMain } = useAuthorization() +const { validateAuthorization, updateAuthorizationInfo, syncAuthorizationToMain } = + useAuthorization() const message = useMessage() const loadingProgress = ref(0) -const loadingMessage = ref('正在初始化系统...') +const loadingMessage = ref(t('正在初始化系统...')) const loadingError = ref('') const loadingMessages = [ - '正在初始化系统...', - '加载授权信息配置...', - '连接服务器...', - '准备应用资源...', - '即将完成...' + t('正在初始化系统...'), + t('加载授权信息配置...'), + t('连接服务器...'), + t('准备应用资源...'), + t('即将完成...') ] const emit = defineEmits(['loading-complete']) @@ -116,14 +118,14 @@ const InitSoftware = async () => { await finalizeLoading() loadingProgress.value = 100 - message.success('软件加载完成,即将进入主界面...') + message.success(t('软件加载完成,即将进入主界面...')) // 这边设置队列启动 let res = await window.task.StartTaskQueue(true) if (res.code != 1) { - throw new Error('启动任务队列失败') + throw new Error(t('启动任务队列失败')) } - message.success('任务队列启动成功') + message.success(t('任务队列启动成功')) // 通知父组件加载完成 setTimeout(() => { @@ -131,8 +133,10 @@ const InitSoftware = async () => { }, 500) } catch (error) { console.error('加载过程出错:', error) - loadingMessage.value = '加载失败,请重试...' - loadingError.value = '错误信息: ' + error.message + loadingMessage.value = t('加载失败,请重试...') + loadingError.value = t('错误信息,{error}', { + error: error.message + }) // 处理错误... } } @@ -145,11 +149,10 @@ defineExpose({ * 加载系统外观设置 */ async function initSystem() { - debugger // 实际的系统初始化逻辑 let res = await window.option.GetOptionByKey(OptionKeyName.Software.AppearanceSetting) if (res.code != 1) { - message.error('加载系统外观设置失败') + message.error(t('加载系统外观设置失败')) return } console.log('加载系统外观设置成功', res.data) @@ -179,7 +182,7 @@ async function loadAuthorizationConfig() { let machineIdOption = await window.option.GetOptionByKey(OptionKeyName.Software.MachineId) let machineId = optionSerialization(machineIdOption.data, '') if (isEmpty(machineId)) { - throw new Error('获取机器码失败,请重启软件或者检查对应权限!!') + throw new Error(t('获取机器码失败,请重启软件或者检查对应权限!!')) } softwareStore.authorization.machineId = machineId @@ -196,7 +199,7 @@ async function loadAuthorizationConfig() { }) if (!isAuthorized) { - const errorMsg = '授权验证失败,即将前往授权界面进行授权!' + const errorMsg = t('授权验证失败,即将前往授权界面进行授权!') message.error(errorMsg) setTimeout(() => { emit('loading-complete', false, errorMsg) @@ -207,7 +210,9 @@ async function loadAuthorizationConfig() { console.log('加载授权码配置成功') } catch (error) { console.error('加载授权码配置失败:', error) - const errorMsg = error.message || '加载授权码配置失败' + const errorMsg = t('加载授权码配置失败,{error}', { + error: error.message + }) setTimeout(() => { emit('loading-complete', false, errorMsg) }, 1000) diff --git a/src/renderer/src/views/MediaToVideoHome.vue b/src/renderer/src/views/MediaToVideoHome.vue new file mode 100644 index 0000000..2b80120 --- /dev/null +++ b/src/renderer/src/views/MediaToVideoHome.vue @@ -0,0 +1,526 @@ + + + + + diff --git a/src/renderer/src/views/NotFound.vue b/src/renderer/src/views/NotFound.vue index c80b6a3..73ad58d 100644 --- a/src/renderer/src/views/NotFound.vue +++ b/src/renderer/src/views/NotFound.vue @@ -1,13 +1,15 @@ @@ -26,7 +28,7 @@ .not-found-container h1 { font-size: 72px; margin-bottom: 10px; - color: #409EFF; + color: #409eff; } .not-found-container h3 { @@ -38,4 +40,4 @@ margin-bottom: 30px; color: #909399; } - \ No newline at end of file + diff --git a/src/renderer/src/views/OriginalHome.vue b/src/renderer/src/views/OriginalHome.vue index d247fbf..2f68baf 100644 --- a/src/renderer/src/views/OriginalHome.vue +++ b/src/renderer/src/views/OriginalHome.vue @@ -50,7 +50,7 @@