修改剪映草稿生成,修改字幕分组报错,优化加载标签信息,修改预设样式

This commit is contained in:
lq1405 2025-09-21 10:20:33 +08:00
parent 3db9352a42
commit aa16a494bd
19 changed files with 179 additions and 166 deletions

View File

@ -1,7 +1,7 @@
{
"name": "laitool-pro",
"productName": "LaiToolPro",
"version": "v3.4.9",
"version": "v4.0.0",
"description": "来推 Pro - 一款集音频处理、文案生成、图片生成、视频生成等功能于一体的多合一AI工具软件。",
"main": "./out/main/index.js",
"author": "xiangbei",
@ -84,6 +84,7 @@
"resources/image/zhanwei.png",
"resources/scripts/model/**",
"resources/scripts/Lai.exe",
"resources/scripts/xiangbei_jianying_main.exe",
"resources/scripts/discordScript.js",
"resources/tmp/**",
"resources/icon.ico"

View File

@ -31,7 +31,7 @@ interface ISoftwareData {
export const SoftwareData: ISoftwareData = {
version: 'V3.4.2',
date: '2025-08-08',
date: '2025-09.09',
systemInfo: {
documentationUrl: 'https://rvgyir5wk1c.feishu.cn/wiki/WdaWwAfDdiLOnjkywIgcaQoKnog',
updateUrl: 'https://pvwu1oahp5m.feishu.cn/docx/CAjGdTDlboJ3nVx0cQccOuNHnvd',

View File

@ -462,6 +462,14 @@ export class BookTaskService extends RealmBaseService {
modifyBookTask.subImageFolder = []
modifyBookTask.srtPath = book.srtPath ?? undefined
modifyBookTask.audioPath = book.audioPath ?? undefined
} else {
// 不重置基础数据的话,做一下数据的继承
if (isEmpty(modifyBookTask.srtPath)) {
modifyBookTask.srtPath = book.srtPath ?? undefined
}
if (isEmpty(modifyBookTask.audioPath)) {
modifyBookTask.audioPath = book.audioPath ?? undefined
}
}
// 继承小说的srt和配音文件

View File

@ -1411,7 +1411,7 @@ export default {
'智能合并失败,{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.',
'处理过程中遇到错误,已停止智能匹配:\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}"',

View File

@ -1411,7 +1411,7 @@ export default {
'智能合并失败,{error}': '智能合并失败,{error}',
'智能批量合并完成,所有字幕都已正确匹配': '智能批量合并完成,所有字幕都已正确匹配',
'智能合并完成,{count} 个剩余字幕已写入后续行': '智能合并完成,{count} 个剩余字幕已写入后续行',
'处理过程中遇到错误,已停止智能匹配:\n\n{message}\n\n已处理字幕${totalProcessed} 个\n剩余字幕${totalRemaining} 个\n\n剩余字幕已按顺序写入后续行中请手动调整文案匹配。': '处理过程中遇到错误,已停止智能匹配:\n\n{message}\n\n已处理字幕${totalProcessed} 个\n剩余字幕${totalRemaining} 个\n\n剩余字幕已按顺序写入后续行中请手动调整文案匹配。',
'处理过程中遇到错误,已停止智能匹配:\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}” 不匹配',

View File

@ -18,7 +18,6 @@ import { isEmpty } from 'lodash'
import { define } from '@/define/define'
import util from 'util'
import { exec } from 'child_process'
import { BookTaskDetail } from '@/define/model/book/bookTaskDetail'
import { ValidateJson } from '@/define/Tools/validate'
const execAsync = util.promisify(exec)
import JianyingService from '../../jianying/jianyingService'
@ -184,18 +183,6 @@ export class BookExportHandle extends BookBasicHandle {
}
}
// 时间单位转换:从毫秒转换为秒
element.startTime = (element.startTime as number) / 1000
element.endTime = (element.endTime as number) / 1000
// 处理子字幕数组,同样进行时间单位转换
element.subValue = Array.isArray(element.subValue)
? (element.subValue as BookTaskDetail.CopywritingSubValue[]).map((sub) => {
sub.start_time = sub.start_time / 1000
sub.end_time = sub.end_time / 1000
return sub
})
: []
// 构建单帧数据对象
let frameData = {

View File

@ -1,96 +1,9 @@
import { PresetCategory } from '@/define/data/presetData'
import { Book } from '@/define/model/book/book'
import { ErrorItem, SuccessItem } from '@/define/model/generalResponse'
import { PresetModel } from '@/define/model/preset'
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()
/**
*
*
*
*
* - file://前缀)
* -
* -
* -
*
* @param {PresetModel.QueryPresetCondition} [condition] - 使
*
* @returns {Promise<PresetModel.PresetCategoryCollection>}
* - character: 角色类预设数组
* - style: 风格类预设数组
* - scene: 场景类预设数组
*
* @throws {Error}
*
* @example
* // 使用默认条件获取预设
* const presets = await getShowTagsData();
*
* // 使用自定义条件获取预设
* const presets = await getShowTagsData({
* keyword: '自然',
* category: PresetCategory.Scene
* });
*/
export async function getShowTagsData(
condition?: PresetModel.QueryPresetCondition
): Promise<PresetModel.PresetCategoryCollection> {
if (!condition) {
condition = {
...presetStore.queryPresetCondition
}
}
let res = await window.preset.GetPresetByCondition(condition)
if (res.code != 1) {
throw new Error(res.message)
}
let characterShowPreset: PresetModel.Preset[] = []
let styleShowPreset: PresetModel.Preset[] = []
let sceneShowPreset: PresetModel.Preset[] = []
for (let i = 0; res.data.presetArray && i < res.data.presetArray.length; i++) {
const element = res.data.presetArray[i] as PresetModel.Preset
if (element.showImage && element.showImage.length > 0) {
// 处理路径中的特殊字符和中文
let imagePath = element.showImage[0]
// 确保本地文件路径有file://前缀
if (
!imagePath.startsWith('file://') &&
(imagePath.startsWith('/') || imagePath.includes(':\\') || imagePath.startsWith('..'))
) {
imagePath = `file://${imagePath}`.replaceAll(/\\/g, '/')
}
element.coverImage = imagePath + `?${Date.now()}` // 添加时间戳以避免缓存问题
element.checked = false // 初始化checked属性
} else {
element.coverImage = ''
element.checked = false // 初始化checked属性
}
if (element.type == PresetCategory.Character) {
characterShowPreset.push(element)
} else if (element.type == PresetCategory.Style) {
styleShowPreset.push(element)
} else if (element.type == PresetCategory.Scene) {
sceneShowPreset.push(element)
}
}
return {
character: characterShowPreset,
style: styleShowPreset,
scene: sceneShowPreset
}
}
/**
*

View File

@ -1,42 +1,50 @@
<template>
<div class="all-image-preview-container">
<div v-for="(image, index) in images" :key="index" :style="{ margin: '5px' }">
<div class="image-item" :class="{ placeholder: !isValidImage(image.url) }">
<!-- 有效图片 -->
<div v-if="isValidImage(image.url)" class="image-wrapper">
<n-image
:src="image.url"
:alt="t('图片 {index}', { index: index + 1 })"
object-fit="cover"
:width="150"
/>
</div>
<div>
<div v-if="!comLoading" class="all-image-preview-container">
<div v-for="(image, index) in images" :key="index" :style="{ margin: '5px' }">
<div class="image-item" :class="{ placeholder: !isValidImage(image.url) }">
<!-- 有效图片 -->
<div v-if="isValidImage(image.url)" class="image-wrapper">
<n-image
:src="image.url"
:alt="t('图片 {index}', { index: index + 1 })"
object-fit="cover"
:width="150"
lazy="true"
/>
</div>
<!-- 占位符 - 修改为与图片结构一致 -->
<div
v-else
class="image-wrapper image-placeholder"
:style="{
width: `150px`,
height: `150px`,
backgroundColor: themeStore.menuPrimaryColor
}"
>
<n-icon size="24" color="#fff">
<image-outline />
</n-icon>
</div>
<!-- 占位符 - 修改为与图片结构一致 -->
<div
v-else
class="image-wrapper image-placeholder"
:style="{
width: `150px`,
height: `150px`,
backgroundColor: themeStore.menuPrimaryColor
}"
>
<n-icon size="24" color="#fff">
<image-outline />
</n-icon>
</div>
<!-- 左上角文本标识 -->
<div class="image-label">{{ image.name }}</div>
<!-- 右下角定位图标 -->
<div class="image-locate" @click.stop="locateTableRow(image.id)" :title="t('定位到对应行')">
<n-icon size="20" color="#fff">
<locate-outline />
</n-icon>
<!-- 左上角文本标识 -->
<div class="image-label">{{ image.name }}</div>
<!-- 右下角定位图标 -->
<div
class="image-locate"
@click.stop="locateTableRow(image.id)"
:title="t('定位到对应行')"
>
<n-icon size="20" color="#fff">
<locate-outline />
</n-icon>
</div>
</div>
</div>
</div>
<LoadingComponent v-else :description="t('加载中...')" size="large" />
</div>
</template>
@ -48,16 +56,24 @@ import { LocateOutline } from '@vicons/ionicons5' // 添加定位图标
import { useBookStore, useThemeStore } from '@/renderer/src/stores'
import { isEmpty } from 'lodash'
import { t } from '@/i18n'
import { checkImageExists } from '@/renderer/src/common/image'
const bookStore = useBookStore()
const themeStore = useThemeStore()
const message = useMessage()
const images = ref([])
const comLoading = ref(true)
onMounted(() => {
bookStore.selectBookTaskDetail.forEach((item) => {
if (!isEmpty(item.outImagePath)) {
onMounted(async () => {
await waitForImagesToLoad()
window.addEventListener('locate-table-row', handleLocateTableRow)
})
async function waitForImagesToLoad() {
for (let i = 0; i < bookStore.selectBookTaskDetail.length; i++) {
const item = bookStore.selectBookTaskDetail[i]
if (!isEmpty(item.outImagePath) && (await checkImageExists(item.outImagePath))) {
images.value.push({
url: item.outImagePath.split('?t')[0] + '?t=' + Date.now(), //
alt: t('分镜_{name}', {
@ -76,9 +92,9 @@ onMounted(() => {
id: item.id // ID
})
}
})
window.addEventListener('locate-table-row', handleLocateTableRow)
})
}
comLoading.value = false
}
onUnmounted(() => {
window.removeEventListener('locate-table-row', handleLocateTableRow)
@ -151,7 +167,7 @@ const labelBackgroundColor = computed(() => {
}
.image-item:hover .image-wrapper {
transform: scale(1.2);
transform: scale(1.05);
}
/* 添加阴影效果到父元素 */

View File

@ -177,6 +177,7 @@ watch(
watch(
() => presetStore.presetChangeCount,
(newValue, oldValue) => {
console.log('presetChangeCount变化了重新获取标签数据', newValue, oldValue)
if (newValue !== oldValue) {
fecthTagsData()
}

View File

@ -36,6 +36,7 @@
:width="dialogWidth"
:height="dialogHeight"
:content-component="dialogComponent"
@close="handleOnclose"
/>
</div>
</template>
@ -127,4 +128,13 @@ async function SelectConfirm(selectedStyles) {
function OpenPresetLibraryHome() {
dialogVisible.value = true
}
//
async function handleOnclose() {
//
await presetStore.LoadPresetArray({
isShow: true
})
presetStore.presetChangeCount++
}
</script>

View File

@ -221,7 +221,7 @@ onMounted(() => {
}, 15000)
})
function ErrorPosition(type) {
async function ErrorPosition(type) {
let id = ''
softwareStore.skipRowIndex = 0
if (type == 'gptPrompt') {
@ -249,7 +249,12 @@ function ErrorPosition(type) {
}
} else if (type == 'image') {
for (let i = 0; i < bookStore.selectBookTaskDetail.length; i++) {
if (isEmpty(bookStore.selectBookTaskDetail[i].outImagePath)) {
let item = bookStore.selectBookTaskDetail[i]
if (
!item.outImagePath ||
isEmpty(item.outImagePath) ||
!(await checkImageExists(item.outImagePath))
) {
id = bookStore.selectBookTaskDetail[i].id
break
}

View File

@ -100,7 +100,6 @@ async function handleSave() {
//
onMounted(() => {
console.log('初始化数据:', props.initData)
debugger
if (props.initData.value && props.initData.value.length > 0) {
const lines = props.initData.value
.map((item) => {

View File

@ -253,7 +253,7 @@ const columns = [
function computedInputContent() {
inputContent.value = tableData.value
.map((item) => {
let w = item.afterGpt || ''
let w = item.afterGpt || item.subValue?.map(v => v.srt_value).join('') || ''
if (!w.endsWith('。')) {
w = w + '。'
}

View File

@ -132,9 +132,6 @@ export function useWordGroupBase(initData) {
// 从后往前遍历,避免删除时索引变化的问题
for (let i = data.value.length - 1; i >= 0; i--) {
const currentRow = data.value[i]
if (i == 64) {
debugger
}
// 检查当前行是否为空
if (isRowEmpty(currentRow)) {
@ -381,7 +378,7 @@ export function useWordGroupBase(initData) {
initData: data,
onSaveWord: (newWords) => {
da?.destroy()
debugger
for (let i = 0; i < data.value.length; i++) {
const element = data.value[i]
element.afterGpt = ''
@ -444,11 +441,32 @@ export function useWordGroupBase(initData) {
onPositiveClick: async () => {
try {
da?.destroy()
debugger
// 深度清理数据,移除不可序列化的属性
const cleanData = toRaw(data.value).map(item => ({
no: item.no,
id: item.id,
lastId: item.lastId,
word: item.word || '',
afterGpt: item.afterGpt || '',
startTime: item.startTime || 0,
endTime: item.endTime || 0,
timeLimit: item.timeLimit || '',
subValue: (item.subValue || []).map(subItem => ({
id: subItem.id,
srt_value: subItem.srt_value,
start_time: subItem.start_time,
end_time: subItem.end_time
}))
}))
console.log("保存的数据", cleanData)
softwareStore.spin.spinning = true
softwareStore.spin.tip = t('正在保存文案信息...')
let res = await window.book.SaveCopywritingInfo(
bookStore.selectBookTask.id,
toRaw(data.value),
cleanData,
OperateBookType.BOOKTASK
)
if (res.code != 1) {
@ -514,7 +532,7 @@ export function useWordGroupBase(initData) {
title: t('智能批量合并'),
showIcon: true,
content: t('确定要进行智能批量合并吗?将从第 {startIndex} 行开始处理。此操作会根据分镜文案内容自动将字幕合并到对应的行中。', {
startFromIndex: startFromIndex
startIndex: startFromIndex
}),
style: 'width: 500px',
maskClosable: false,
@ -853,7 +871,7 @@ export function useWordGroupBase(initData) {
dialog.create({
type: 'warning',
title: t('智能合并部分完成'),
content: t('处理过程中遇到错误,已停止智能匹配:\n\n{message}\n\n已处理字幕${totalProcessed} 个\n剩余字幕${totalRemaining} 个\n\n剩余字幕已按顺序写入后续行中请手动调整文案匹配。', {
content: t('处理过程中遇到错误,已停止智能匹配:\n\n{message}\n\n已处理字幕{totalProcessed} 个\n剩余字幕{totalRemaining} 个\n\n剩余字幕已按顺序写入后续行中请手动调整文案匹配。', {
message: processingError.message,
totalProcessed: totalProcessed,
totalRemaining: totalRemaining

View File

@ -210,7 +210,7 @@ import {
useBookStore
} from '@/renderer/src/stores'
import { checBookTaskDetailImageExist, getShowTagsData } from '@/renderer/src/common/book'
import { checBookTaskDetailImageExist } from '@/renderer/src/common/book'
import { useRouter } from 'vue-router'
import { useBookTaskCardOption } from '@/renderer/src/hooks/useBookTaskCardOption'
import TooltipDropdown from '../../common/TooltipDropdown.vue'
@ -368,14 +368,10 @@ async function handleOpenTask() {
bookStore.selectBookTaskDetail = res.data
bookStore.selectBookTask = { ...props.bookTask }
//
let tagRes = await getShowTagsData({
//
await presetStore.LoadPresetArray({
isShow: true
})
//
presetStore.showCharacterPresetArray = tagRes.character
presetStore.showScenePresetArray = tagRes.scene
presetStore.showStylePresetArray = tagRes.style
router.push('/original-book-detail/' + props.bookTask.id)
} catch (error) {

View File

@ -179,6 +179,7 @@ let contentBackgroundColor = computed(() => {
.preset-card {
transition: all 0.3s;
height: 280px; /* 固定卡片高度 */
width: 200px; /* 固定卡片宽度 */
display: flex;
flex-direction: column;
position: relative;

View File

@ -68,10 +68,14 @@ const he = ref(props.height) // 使用const而不是let
// visibletrue
watch(
visible,
(newVal) => {
(newVal, oldVal) => {
if (newVal === true) {
calculateDimensions()
}
// modal close
if (oldVal === true && newVal === false) {
emit('close')
}
emit('update:show', newVal)
}
)
@ -107,7 +111,6 @@ const actualHeight = computed(() => he.value)
const handleClose = () => {
visible.value = false
emit('close')
}
const handleConfirm = () => {

View File

@ -45,6 +45,62 @@ export const usePresetStore = defineStore('preset', {
}),
getters: {},
actions: {
async LoadPresetArray(condition?: PresetModel.QueryPresetCondition) {
try {
if (!condition) {
condition = {
...this.queryPresetCondition
}
}
let res = await window.preset.GetPresetByCondition(condition)
if (res.code != 1) {
throw new Error(res.message)
}
let characterShowPreset: PresetModel.Preset[] = []
let styleShowPreset: PresetModel.Preset[] = []
let sceneShowPreset: PresetModel.Preset[] = []
for (let i = 0; res.data.presetArray && i < res.data.presetArray.length; i++) {
const element = res.data.presetArray[i] as PresetModel.Preset
if (element.showImage && element.showImage.length > 0) {
// 处理路径中的特殊字符和中文
let imagePath = element.showImage[0]
// 确保本地文件路径有file://前缀
if (
!imagePath.startsWith('file://') &&
(imagePath.startsWith('/') || imagePath.includes(':\\') || imagePath.startsWith('..'))
) {
imagePath = `file://${imagePath}`.replaceAll(/\\/g, '/')
}
element.coverImage = imagePath + `?${Date.now()}` // 添加时间戳以避免缓存问题
element.checked = false // 初始化checked属性
} else {
element.coverImage = ''
element.checked = false // 初始化checked属性
}
if (element.type == PresetCategory.Character) {
characterShowPreset.push(element)
} else if (element.type == PresetCategory.Style) {
styleShowPreset.push(element)
} else if (element.type == PresetCategory.Scene) {
sceneShowPreset.push(element)
}
}
// 更新数据
this.showCharacterPresetArray = characterShowPreset
this.showScenePresetArray = sceneShowPreset
this.showStylePresetArray = styleShowPreset
} catch (error) {
console.error('加载预设数据失败:', error)
throw error
}
},
ResetSelectPreset() {
const now = new Date()
const formattedDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`

View File

@ -195,7 +195,6 @@ let selectBorderColor = computed(() => {
}
.preset-flex-item {
width: 200px; /* 固定卡片宽度 */
flex-grow: 0;
flex-shrink: 0;
}