V 4.0.1(2025.09.21)

1. 修改场景推理,导入到场景预设时原创界面的自动更新分组错误
2. 文案处理,可单独设置API、密钥、推理设置,没有设置就默认使用推理设置
3. 修改MJ出图的代理模式(添加账号,修改账号,出图)
4. 优化剪映关键帧设置UI界面
5. 修复文案处理的单个清空和批量清空
6. 删除 MJ Video Extend 的尾帧链接
This commit is contained in:
lq1405 2025-09-21 13:05:02 +08:00
parent aa16a494bd
commit 7a16f02673
17 changed files with 219 additions and 97 deletions

View File

@ -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",

View File

@ -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)
}

View File

@ -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',

View File

@ -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',

View File

@ -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!',

View File

@ -1801,7 +1801,7 @@ export default {
'AI生成文本': 'AI生成文本',
'确定要清空所有的AI生成文本吗清空后不可恢复是否继续': '确定要清空所有的AI生成文本吗清空后不可恢复是否继续',
'清空成功': '清空成功',
'清空失败:': '清空失败:{error}',
'清空失败:{error}': '清空失败:{error}',
'取消清空': '取消清空',
'直接复制会将所有的AI生成后的数据直接进行复制不会进行格式之类的调整若有需求可以再下面表格直接修改或者是再左边的显示生成文本中修改是否继续复制': '直接复制会将所有的AI生成后的数据直接进行复制不会进行格式之类的调整若有需求可以再下面表格直接修改或者是再左边的显示生成文本中修改是否继续复制',
'复制失败:存在未生成的文本,请先生成文本!': '复制失败:存在未生成的文本,请先生成文本!',

View File

@ -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) {

View File

@ -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

View File

@ -45,7 +45,7 @@ export class TranslateCommon {
let aiSetting = optionSerialization<SettingModal.InferenceAISettings>(
aiSettingOptionString,
'设置-> 推理设置'
t('设置-> 推理设置')
)
let apiProvider = GetApiDefineDataById(aiSetting.apiProvider)

View File

@ -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<SettingModal.InferenceAISettings>(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<SuccessItem | ErrorItem>
*/
async CopyWritingAIGeneration(ids: string[]) {
try {
if (ids.length === 0) {

View File

@ -6,5 +6,6 @@ export class WriteHandle {
this.copyWritingServiceHandle = new CopyWritingServiceHandle()
}
/** 需要处理的文案ID数组 */
CopyWritingAIGeneration = async (ids: string[]) => await this.copyWritingServiceHandle.CopyWritingAIGeneration(ids)
}

View File

@ -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('取消清空'))

View File

@ -133,7 +133,7 @@
</n-form-item>
<!-- 尾帧图片链接 -->
<n-form-item :label="t('尾帧图片链接(可选)')" :show-feedback="false">
<!-- <n-form-item :label="t('尾帧图片链接(可选)')" :show-feedback="false">
<div class="input-with-preview">
<n-input
v-model:value="videoMessage.mjVideoOptionsObject.extendEndImageUrl"
@ -199,7 +199,7 @@
<n-text depth="3" class="placeholder-text">{{ t('图片预览') }}</n-text>
</div>
</div>
</n-form-item>
</n-form-item> -->
<!-- 提示词 -->
<n-form-item :label="t('提示词(可选)')" :show-feedback="false">

View File

@ -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,

View File

@ -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++

View File

@ -1,18 +1,20 @@
<template>
<n-card :title="t('剪映关键帧设置')">
<div style="display: flex; height: 40px; line-height: 40px">
<div style="margin-bottom: 10px; width: 100px">{{ t('打帧方式') }}</div>
<n-select
v-model:value="keyFrameData.keyFrame"
:options="getJianyingKeyFrameOptions()"
style="width: 140px"
:placeholder="
t('请选择 {data}', {
data: t('打帧方式')
})
"
/>
<div style="width: 170px">
<n-space align="center" :size="12">
<n-space align="center">
<div>{{ t('打帧方式') }}</div>
<n-select
v-model:value="keyFrameData.keyFrame"
:options="getJianyingKeyFrameOptions()"
style="width: 140px"
:placeholder="
t('请选择 {data}', {
data: t('打帧方式')
})
"
/>
</n-space>
<div>
<disabled-wrapper
:un-use="!softwareStore.authorization.isPro"
one-cell="true"
@ -32,88 +34,91 @@
</n-tooltip>
</disabled-wrapper>
</div>
<span>{{ t('匀速关键帧时间') }}</span>
<div>
<n-input-number
:show-button="false"
:step="0.1"
:min="0"
:max="10"
v-model:value="keyFrameData.keyFrameTime"
style="width: 100px"
/>
</div>
<div>
<n-button style="margin-left: 30px" type="info" @click="SaveKeyFrameSetting">保存</n-button>
</div>
</div>
<n-input-number
:show-button="false"
:step="0.1"
:min="0"
:max="10"
v-model:value="keyFrameData.keyFrameTime"
style="width: 100px"
/>
<n-button type="info" @click="SaveKeyFrameSetting">保存</n-button>
</n-space>
<n-card style="margin-top: 10px" :title="t('上下关键帧设置')">
<div style="display: flex; height: 40px; line-height: 40px">
<div>{{ t('上关键帧位置') }}</div>
<n-space align="center" :size="12">
<span>{{ t('上关键帧位置') }}</span>
<n-input-number
:show-button="false"
v-model:value="keyFrameData.upDownKeyFrame.startPosition"
style="width: 100px; margin-right: 10px"
style="width: 100px"
/>
<div>{{ t('下关键帧位置') }}</div>
<span>{{ t('下关键帧位置') }}</span>
<n-input-number
:show-button="false"
v-model:value="keyFrameData.upDownKeyFrame.endPosition"
style="width: 100px; margin-right: 10px"
></n-input-number>
<div>{{ t('缩放大小') }}</div>
style="width: 100px"
/>
<span>{{ t('缩放大小') }}</span>
<n-input-number
:show-button="false"
v-model:value="keyFrameData.upDownKeyFrame.defaultScale"
style="width: 100px; margin-right: 10px"
></n-input-number>
</div>
style="width: 100px"
/>
</n-space>
</n-card>
<n-card :title="t('左右关键帧设置')" style="margin-top: 10px">
<div style="display: flex; height: 40px; line-height: 40px">
<div>{{ t('左关键帧位置') }}</div>
<n-space align="center" :size="12">
<span>{{ t('左关键帧位置') }}</span>
<n-input-number
:show-button="false"
v-model:value="keyFrameData.leftRightKeyFrame.startPosition"
style="width: 100px; margin-right: 10px"
style="width: 100px"
/>
<div>{{ t('右关键帧位置') }}</div>
<span>{{ t('右关键帧位置') }}</span>
<n-input-number
:show-button="false"
v-model:value="keyFrameData.leftRightKeyFrame.endPosition"
style="width: 100px; margin-right: 10px"
></n-input-number>
<div>{{ t('缩放大小') }}</div>
style="width: 100px"
/>
<span>{{ t('缩放大小') }}</span>
<n-input-number
:show-button="false"
v-model:value="keyFrameData.leftRightKeyFrame.defaultScale"
style="width: 100px; margin-right: 10px"
></n-input-number>
</div>
style="width: 100px"
/>
</n-space>
</n-card>
<n-card :title="t('缩放关键帧设置')" style="margin-top: 10px">
<div style="display: flex; height: 40px; line-height: 40px">
<div>{{ t('开始缩放大小') }}</div>
<n-space align="center" :size="12">
<span>{{ t('开始缩放大小') }}</span>
<n-input-number
:show-button="false"
v-model:value="keyFrameData.scaleKeyFrame.startPosition"
style="width: 100px; margin-right: 10px"
style="width: 100px"
/>
<div>{{ t('结束缩放大小') }}</div>
<span>{{ t('结束缩放大小') }}</span>
<n-input-number
:show-button="false"
v-model:value="keyFrameData.scaleKeyFrame.endPosition"
></n-input-number>
</div>
style="width: 100px"
/>
</n-space>
</n-card>
</n-card>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useMessage, NSelect, NCheckbox, NInputNumber, NCard, NButton, NTooltip } from 'naive-ui'
import {
useMessage,
NSelect,
NCheckbox,
NInputNumber,
NCard,
NButton,
NTooltip,
NSpace
} from 'naive-ui'
import { getJianyingKeyFrameOptions, JianyingKeyFrameEnum } from '@/define/enum/jianyingEnum'
import { defaultJianyingKeyFrameSetting } from '@/renderer/src/common/initialData'
import { cloneDeep, isEmpty } from 'lodash'

View File

@ -421,7 +421,7 @@ const handleAccountAddSuccess = async (accountData) => {
mjBotChannelId: null,
nijiBotChannelId: null,
queueSize: 5,
remark: global.machineId,
remark: softwareStore.authorization.machineId,
remixAutoSubmit: false,
timeoutMinutes: 6,
userAgent:
@ -429,11 +429,11 @@ const handleAccountAddSuccess = async (accountData) => {
}
//
accountData = Object.assign(defaultSetting, accountData)
accountData.id = uuidv4()
accountData.id = crypto.randomUUID()
accountData.createTime = new Date()
accountData.updateTime = new Date()
accountData.version = version
accountData.remark = global.machineId
accountData.version = softwareStore.versionInfo.currentVersion
accountData.remark = softwareStore.authorization.machineId
if (accountData.hasOwnProperty('enable') == false) {
accountData.enable = true
@ -474,7 +474,7 @@ const handleAccountUpdateSuccess = async (accountData) => {
}
accountData.updateTime = new Date()
accountData.version = version
accountData.version = softwareStore.versionInfo.currentVersion
accountData.remark = softwareStore.authorization.machineId
let findIndex = remoteSettings.value.accountList.findIndex((item) => item.id === accountData.id)