diff --git a/package.json b/package.json index 29170cc..b9f713d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "laitool-pro", "productName": "LaiToolPro", - "version": "v4.0.0", + "version": "v4.0.1", "description": "来推 Pro - 一款集音频处理、文案生成、图片生成、视频生成等功能于一体的多合一AI工具软件。", "main": "./out/main/index.js", "author": "xiangbei", diff --git a/src/define/Tools/image.ts b/src/define/Tools/image.ts index 660b37c..ee9c828 100644 --- a/src/define/Tools/image.ts +++ b/src/define/Tools/image.ts @@ -352,10 +352,6 @@ export async function ImageSplit( const smallWidth = Math.floor(metadata.width / 2) const smallHeight = Math.floor(metadata.height / 2) - // 计算最后一列和最后一行可能的额外宽度/高度(处理奇数像素) - const rightWidth = metadata.width - smallWidth - const bottomHeight = metadata.height - smallHeight - const timestamp = new Date().getTime() const imgs: string[] = [] @@ -367,21 +363,57 @@ export async function ImageSplit( const xOffset = isRightColumn ? smallWidth : 0 const yOffset = isBottomRow ? smallHeight : 0 - // 使用实际宽高,确保右边和底部区块使用正确尺寸 - const blockWidth = isRightColumn ? rightWidth : smallWidth - const blockHeight = isBottomRow ? bottomHeight : smallHeight + // 计算实际的分块尺寸,确保不超出图片边界 + const blockWidth = isRightColumn ? metadata.width - smallWidth : smallWidth + const blockHeight = isBottomRow ? metadata.height - smallHeight : smallHeight + + // 安全检查:确保分块尺寸为正数且不超出边界 + if (blockWidth <= 0 || blockHeight <= 0) { + throw new Error(t('图片分块尺寸计算错误')) + } + + if (xOffset + blockWidth > metadata.width || yOffset + blockHeight > metadata.height) { + throw new Error(t('图片分块超出边界')) + } const outFile = path.join(outputDir, `${reName}_${timestamp}_${i}.png`) // 提取并保存分块 - await sharp(inputPath) - .extract({ - left: xOffset, - top: yOffset, - width: blockWidth, - height: blockHeight - }) - .toFile(outFile) + try { + // 验证 extract 参数的有效性 + const extractOptions = { + left: Math.max(0, Math.floor(xOffset)), + top: Math.max(0, Math.floor(yOffset)), + width: Math.max(1, Math.floor(blockWidth)), + height: Math.max(1, Math.floor(blockHeight)) + } + + // 再次验证边界 + if (extractOptions.left + extractOptions.width > metadata.width) { + extractOptions.width = metadata.width - extractOptions.left + } + if (extractOptions.top + extractOptions.height > metadata.height) { + extractOptions.height = metadata.height - extractOptions.top + } + + // 使用 buffer 方式更安全,避免 sharp 直接写文件时的崩溃 + const sharpInstance = sharp(inputPath) + const extractedImage = sharpInstance.extract(extractOptions) + + // 先转为 buffer,再写入文件 + const buffer = await extractedImage.png().toBuffer() + + // 确保输出文件的目录存在 + await CheckFolderExistsOrCreate(path.dirname(outFile)) + + // 写入文件 + await fs.promises.writeFile(outFile, buffer) + } catch (extractError) { + throw new Error(t('图片分块 {index} 处理失败: {error}', { + index: i, + error: (extractError as Error).message + })) + } imgs.push(outFile) } diff --git a/src/define/data/apiData.ts b/src/define/data/apiData.ts index 88f10b5..43b07b0 100644 --- a/src/define/data/apiData.ts +++ b/src/define/data/apiData.ts @@ -6,6 +6,7 @@ export const apiDefineData = [ value: 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65', id: 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65', gpt_url: 'https://api.laitool.cc/v1/chat/completions', + base_url: 'https://api.laitool.cc', mj_url: { imagine: 'https://api.laitool.cc/mj/submit/imagine', describe: 'https://api.laitool.cc/mj/submit/describe', @@ -23,6 +24,7 @@ export const apiDefineData = [ value: '2b443f53-ba12-42b3-a57c-e4df92685c73', id: '2b443f53-ba12-42b3-a57c-e4df92685c73', gpt_url: 'https://laitool.net/v1/chat/completions', + base_url: 'https://laitool.net', mj_url: { imagine: 'https://laitool.net/mj/submit/imagine', describe: 'https://laitool.net/mj/submit/describe', @@ -39,6 +41,7 @@ export const apiDefineData = [ label: t('LaiTool生图包'), value: '9c9023bd-871d-4b63-8004-facb3b66c5b3', isPackage: true, + base_url: 'https://lms.laitool.cn', mj_url: { imagine: 'https://lms.laitool.cn/api/mjPackage/mj/submit/imagine', describe: 'https://lms.laitool.cn/api/mjPackage/mj/submit/describe', diff --git a/src/define/data/softwareData.ts b/src/define/data/softwareData.ts index 4479c98..cf9def2 100644 --- a/src/define/data/softwareData.ts +++ b/src/define/data/softwareData.ts @@ -30,8 +30,8 @@ interface ISoftwareData { } export const SoftwareData: ISoftwareData = { - version: 'V3.4.2', - date: '2025-09.09', + version: 'V4.0.1', + date: '2025-09-21', systemInfo: { documentationUrl: 'https://rvgyir5wk1c.feishu.cn/wiki/WdaWwAfDdiLOnjkywIgcaQoKnog', updateUrl: 'https://pvwu1oahp5m.feishu.cn/docx/CAjGdTDlboJ3nVx0cQccOuNHnvd', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 30dd27b..8e766b9 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1801,7 +1801,7 @@ export default { '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}', + '清空失败:{error}': '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!', diff --git a/src/i18n/locales/zh-cn.ts b/src/i18n/locales/zh-cn.ts index a9eb9f4..1bdd08b 100644 --- a/src/i18n/locales/zh-cn.ts +++ b/src/i18n/locales/zh-cn.ts @@ -1801,7 +1801,7 @@ export default { 'AI生成文本': 'AI生成文本', '确定要清空所有的AI生成文本吗?清空后不可恢复,是否继续?': '确定要清空所有的AI生成文本吗?清空后不可恢复,是否继续?', '清空成功': '清空成功', - '清空失败:': '清空失败:{error}', + '清空失败:{error}': '清空失败:{error}', '取消清空': '取消清空', '直接复制会将所有的AI生成后的数据直接进行复制,不会进行格式之类的调整,若有需求可以再下面表格直接修改,或者是再左边的显示生成文本中修改,是否继续复制?': '直接复制会将所有的AI生成后的数据直接进行复制,不会进行格式之类的调整,若有需求可以再下面表格直接修改,或者是再左边的显示生成文本中修改,是否继续复制?', '复制失败:存在未生成的文本,请先生成文本!': '复制失败:存在未生成的文本,请先生成文本!', diff --git a/src/main/service/book/subBookHandle/bookImageHandle.ts b/src/main/service/book/subBookHandle/bookImageHandle.ts index 5d0e0ad..66827fa 100644 --- a/src/main/service/book/subBookHandle/bookImageHandle.ts +++ b/src/main/service/book/subBookHandle/bookImageHandle.ts @@ -527,7 +527,10 @@ export class BookImageHandle extends BookBasicHandle { imageRes = await ImageSplit( imagePath, bookTaskDetail.name as string, - path.join(book.bookFolderPath as string, 'data\\MJOriginalImage') + path.join( + bookTask.imageFolder as string, + `subImage\\${bookTaskDetail.name}` + ) ) if (imageRes && imageRes.length < 4) { throw new Error(t('图片裁剪失败')) @@ -684,7 +687,7 @@ export class BookImageHandle extends BookBasicHandle { bookTaskDetail.name as string, path.join( bookTask.imageFolder as string, - `subImage\\${bookTaskDetail.name}\\${new Date().getTime()}.png` + `subImage\\${bookTaskDetail.name}` ) ) if (imageArray && imageArray.length < 4) { diff --git a/src/main/service/mj/mjApiService.ts b/src/main/service/mj/mjApiService.ts index 35d7b78..9bbb96b 100644 --- a/src/main/service/mj/mjApiService.ts +++ b/src/main/service/mj/mjApiService.ts @@ -279,6 +279,13 @@ export class MJApiService extends MJBasic { let headers = { Authorization: this.token } + + if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.LOCAL_MJ) { + headers['mj-api-secret'] = this.token + } else if (this.mjGeneralSetting?.outputMode == ImageGenerateMode.REMOTE_MJ) { + headers['mj-api-secret'] = this.token + } + // 开始请求 let res = await axios.get(APIDescribeUrl, { headers: headers diff --git a/src/main/service/translate/translateCommon.ts b/src/main/service/translate/translateCommon.ts index 6fe9b4e..e6e23af 100644 --- a/src/main/service/translate/translateCommon.ts +++ b/src/main/service/translate/translateCommon.ts @@ -45,7 +45,7 @@ export class TranslateCommon { let aiSetting = optionSerialization( aiSettingOptionString, - '‘设置-> 推理设置’' + t('设置-> 推理设置') ) let apiProvider = GetApiDefineDataById(aiSetting.apiProvider) diff --git a/src/main/service/write/copyWritingServiceHandle.ts b/src/main/service/write/copyWritingServiceHandle.ts index 1ce2eee..92efc97 100644 --- a/src/main/service/write/copyWritingServiceHandle.ts +++ b/src/main/service/write/copyWritingServiceHandle.ts @@ -10,6 +10,7 @@ import { DEFINE_STRING } from '@/define/ipcDefineString' import axios from 'axios' import { GetOpenAISuccessResponse, GetRixApiErrorResponse } from '@/define/response/openAIResponse' import { t } from '@/i18n' +import { GetApiDefineDataById } from '@/define/data/apiData' export class CopyWritingServiceHandle extends BookBasicHandle { constructor() { @@ -56,6 +57,32 @@ export class CopyWritingServiceHandle extends BookBasicHandle { apiSettingOption, t('文案处理->设置') ) + if (isEmpty(apiSetting.apiKey) || isEmpty(apiSetting.gptUrl) || isEmpty(apiSetting.model)) { + // 这边没有设置数据的话,去文案处理那边获取设置 + let cwApiSetting = this.optionRealmService.GetOptionDataByKey(OptionKeyName.InferenceAI.InferenceSetting, t('设置 -> 推理设置')); + + // 判断是不是有API令牌,没有的话获取推理设置那边的 + if (apiSetting.apiKey == null || isEmpty(apiSetting.apiKey)) { + apiSetting.apiKey = cwApiSetting?.apiToken || '' + } + + // 判断是不是有API地址,没有的话获取推理设置那边的 + if (apiSetting.gptUrl == null || isEmpty(apiSetting.gptUrl)) { + let ApiData = GetApiDefineDataById(cwApiSetting.apiProvider); + if (ApiData.gpt_url == null || isEmpty(ApiData.gpt_url)) { + throw new Error(t('没有找到对应的API的配置,请先检查配置')) + } + // 获取他的基础地址 + apiSetting.gptUrl = ApiData.base_url + } + + // 判断是不是又模型名字,没有的话获取推理设置那边的 + if (apiSetting.model == null || isEmpty(apiSetting.model)) { + apiSetting.model = cwApiSetting?.inferenceModel || '' + } + } + + // 再次检查,没有就报错 if (isEmpty(apiSetting.apiKey) || isEmpty(apiSetting.gptUrl) || isEmpty(apiSetting.model)) { throw new Error(t('文案处理API设置不完整,请检查API地址,密钥和模型是否设置正确')) } @@ -185,6 +212,20 @@ export class CopyWritingServiceHandle extends BookBasicHandle { } } + + /** + * AI文案批量生成主方法 + * + * 根据传入的文案ID数组,自动获取文案处理相关设置,循环调用AI接口(支持流式和非流式), + * 生成新文案并通过 SendReturnMessage 实时返回结果。 + * + * - 支持重试机制,失败自动重试3次 + * - 支持流式和非流式AI接口 + * - 处理完毕后返回所有文案结构(不做持久化保存) + * + * @param ids 需要处理的文案ID数组 + * @returns Promise 处理结果,包含所有文案结构或错误信息 + */ async CopyWritingAIGeneration(ids: string[]) { try { if (ids.length === 0) { diff --git a/src/main/service/write/index.ts b/src/main/service/write/index.ts index 9be2693..5781f2c 100644 --- a/src/main/service/write/index.ts +++ b/src/main/service/write/index.ts @@ -6,5 +6,6 @@ export class WriteHandle { this.copyWritingServiceHandle = new CopyWritingServiceHandle() } + /** 需要处理的文案ID数组 */ CopyWritingAIGeneration = async (ids: string[]) => await this.copyWritingServiceHandle.CopyWritingAIGeneration(ids) } diff --git a/src/renderer/src/components/CopyWriting/CopyWritingContent.vue b/src/renderer/src/components/CopyWriting/CopyWritingContent.vue index 8519535..55b50a3 100644 --- a/src/renderer/src/components/CopyWriting/CopyWritingContent.vue +++ b/src/renderer/src/components/CopyWriting/CopyWritingContent.vue @@ -24,6 +24,7 @@ import { isEmpty } from 'lodash' import { TimeDelay } from '@/define/Tools/time' import TooltipButton from '../common/TooltipButton.vue' import { t } from '@/i18n' +import { OptionKeyName, OptionType } from '@/define/enum/option' let softwareStore = useSoftwareStore() @@ -41,7 +42,6 @@ const simpleSetting = computed(() => props.simpleSetting) const emit = defineEmits(['split-save', 'save-simple-setting']) -let CopyWriting = {} let message = useMessage() let dialog = useDialog() @@ -50,7 +50,7 @@ let resizeObserver = null const columns = [ { - title: t("序号"), + title: t('序号'), key: 'index', width: 80, render: (_, index) => index + 1 @@ -337,10 +337,24 @@ function ClearAIGeneration() { simpleSetting.value.wordStruct.forEach((item) => { item.newWord = '' }) - await CopyWriting.SaveCWAISimpleSetting() + + // 开始保存 + let res = await window.option.ModifyOptionByKey( + OptionKeyName.InferenceAI.CW_SimpleSetting, + JSON.stringify(simpleSetting.value), + OptionType.JSON + ) + if (res.code != 1) { + message.error( + t('清空失败:{error}', { + error: res.message + }) + ) + return + } message.success(t('清空成功')) } catch (error) { - message.error(t('清空失败:', { error: error.message })) + message.error(t('清空失败:{error}', { error: error.message })) } }, onNegativeClick: () => { @@ -511,9 +525,24 @@ const handleDelete = (id) => { return false } simpleSetting.value.wordStruct[index].newWord = '' - // 保存数据 - await CopyWriting.SaveCWAISimpleSetting() - message.success(t('清空成功')) + + console.log('删除后数据:', simpleSetting.value, simpleSetting.value.wordStruct) + + // 开始保存 + let res = await window.option.ModifyOptionByKey( + OptionKeyName.InferenceAI.CW_SimpleSetting, + JSON.stringify(simpleSetting.value), + OptionType.JSON + ) + if (res.code == 1) { + message.success(t('清空成功')) + } else { + message.error( + t('清空失败:{error}', { + error: res.message + }) + ) + } }, onNegativeClick: () => { message.info(t('取消清空')) diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue index 1266d80..a759549 100644 --- a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue @@ -133,7 +133,7 @@ - + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskList.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskList.vue index 4cfa4e1..e3ef8a1 100644 --- a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskList.vue +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskList.vue @@ -211,10 +211,11 @@ const columns = [ h(NImage, { src: row.outImagePath, height: 130, - objectFit: 'cover', + width: 160, + objectFit: 'contain', style: { borderRadius: '4px', - maxWidth: '160px', + width: '160px', height: '130px' }, fallbackSrc: define.zhanwei_image, diff --git a/src/renderer/src/components/Original/Analysis/SceneAnalysis.vue b/src/renderer/src/components/Original/Analysis/SceneAnalysis.vue index 3a9d0cf..91c8909 100644 --- a/src/renderer/src/components/Original/Analysis/SceneAnalysis.vue +++ b/src/renderer/src/components/Original/Analysis/SceneAnalysis.vue @@ -479,7 +479,7 @@ async function handleExport() { message.error(res.message) return } - presetStore.showCharacterPresetArray.unshift(res.data) + presetStore.showScenePresetArray.unshift(res.data) message.success(t('导入 {name} 到场景预设成功', { name: element.name })) } presetStore.presetChangeCount++ diff --git a/src/renderer/src/components/Setting/JianyingKeyFrameSetting.vue b/src/renderer/src/components/Setting/JianyingKeyFrameSetting.vue index c72a097..18210b8 100644 --- a/src/renderer/src/components/Setting/JianyingKeyFrameSetting.vue +++ b/src/renderer/src/components/Setting/JianyingKeyFrameSetting.vue @@ -1,18 +1,20 @@