mirror of
https://github.com/YILS-LIN/short-video-factory.git
synced 2025-11-25 03:15:03 +08:00
完善视频处理进度交互,增加批量任务能力,优化大量细节
This commit is contained in:
@@ -58,14 +58,14 @@
|
||||
|
||||
## 🗺️ 路线图
|
||||
|
||||
**当前正在积极开发中,即将发布第一个版本,喜欢可以点个 Star 支持一下!**
|
||||
**喜欢可以点个 Star 支持一下哦!**
|
||||
|
||||
下面是已实现和计划中的功能:
|
||||
|
||||
- [x] 文案生成,兼容通用的 OpenAI 接口格式
|
||||
- [x] 语音合成,支持EdgeTTS
|
||||
- [x] 视频剪辑,文案、视频、音频、字幕合成,自动混剪
|
||||
- [ ] 批量处理,支持一个批量任务,按预设自动持续合成视频
|
||||
- [x] 批量处理,支持一个批量任务,按预设自动持续合成视频
|
||||
- [ ] 字幕特效,支持多种字幕样式和特效
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { app } from 'electron'
|
||||
|
||||
// import packageJson from '~/package.json'
|
||||
|
||||
/**
|
||||
* 生成有序的唯一文件名,用于处理文件已存在的情况
|
||||
*/
|
||||
export function generateUniqueFileName(filePath: string): string {
|
||||
const dir = path.dirname(filePath)
|
||||
const ext = path.extname(filePath)
|
||||
@@ -14,3 +20,10 @@ export function generateUniqueFileName(filePath: string): string {
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取软件的临时文件存储路径
|
||||
*/
|
||||
export function getAppTempPath() {
|
||||
return path.join(app.getPath('temp'), app.name).replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { app } from 'electron'
|
||||
import { EdgeTTS } from '../lib/edge-tts'
|
||||
import { parseBuffer } from 'music-metadata'
|
||||
import {
|
||||
@@ -8,14 +7,31 @@ import {
|
||||
EdgeTtsSynthesizeToFileParams,
|
||||
EdgeTtsSynthesizeToFileResult,
|
||||
} from './types'
|
||||
import { getAppTempPath } from '../lib/tools'
|
||||
import { app } from 'electron'
|
||||
|
||||
const edgeTts = new EdgeTTS()
|
||||
const setupTime = new Date().getTime()
|
||||
|
||||
export function getTempTtsVoiceFilePath() {
|
||||
return path.join(app.getPath('temp'), `temp-tts-voice-${setupTime}.mp3`).replace(/\\/g, '/')
|
||||
return path.join(getAppTempPath(), `temp-tts-voice-${setupTime}.mp3`).replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
export function clearCurrentTtsFiles() {
|
||||
const voicePath = getTempTtsVoiceFilePath()
|
||||
if (fs.existsSync(voicePath)) {
|
||||
fs.unlinkSync(voicePath)
|
||||
}
|
||||
const srtPath = path.join(path.dirname(voicePath), path.basename(voicePath, '.mp3') + '.srt')
|
||||
if (fs.existsSync(srtPath)) {
|
||||
fs.unlinkSync(srtPath)
|
||||
}
|
||||
}
|
||||
|
||||
app.on('before-quit', () => {
|
||||
clearCurrentTtsFiles()
|
||||
})
|
||||
|
||||
export function edgeTtsGetVoiceList() {
|
||||
return edgeTts.getVoices()
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 618 KiB After Width: | Height: | Size: 381 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "short-video-factory",
|
||||
"description": "短视频工厂,一键生成产品营销与泛内容短视频,AI批量自动剪辑",
|
||||
"version": "0.6.6",
|
||||
"version": "0.7.0",
|
||||
"author": {
|
||||
"name": "YILS",
|
||||
"developer": "YILS",
|
||||
|
||||
@@ -62,6 +62,7 @@ export const useAppStore = defineStore(
|
||||
outputFileName: '',
|
||||
outputFileExt: '.mp4',
|
||||
})
|
||||
const autoBatch = ref(false)
|
||||
const renderStatus = ref(RenderStatus.None)
|
||||
const updateRenderConfig = (newConfig: typeof renderConfig.value) => {
|
||||
renderConfig.value = newConfig
|
||||
@@ -89,6 +90,7 @@ export const useAppStore = defineStore(
|
||||
tryListeningText,
|
||||
|
||||
renderConfig,
|
||||
autoBatch,
|
||||
renderStatus,
|
||||
updateRenderConfig,
|
||||
updateRenderStatus,
|
||||
@@ -96,7 +98,7 @@ export const useAppStore = defineStore(
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
omit: ['genderList', 'speedList'],
|
||||
omit: ['genderList', 'speedList', 'autoBatch', 'renderStatus'],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -21,7 +21,14 @@
|
||||
>
|
||||
生成
|
||||
</v-btn>
|
||||
<v-btn v-else prepend-icon="mdi-stop" color="red" stacked @click="handleStopGenerate">
|
||||
<v-btn
|
||||
v-else
|
||||
prepend-icon="mdi-stop"
|
||||
color="red"
|
||||
stacked
|
||||
:disabled="disabled"
|
||||
@click="handleStopGenerate"
|
||||
>
|
||||
停止
|
||||
</v-btn>
|
||||
|
||||
@@ -107,9 +114,9 @@ defineProps<{
|
||||
const outputText = ref('')
|
||||
const isGenerating = ref(false)
|
||||
const abortController = ref<AbortController | null>(null)
|
||||
const handleGenerate = async () => {
|
||||
const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
||||
if (!appStore.prompt) {
|
||||
toast.warning('提示词不能为空')
|
||||
!oprions?.noToast && toast.warning('提示词不能为空')
|
||||
throw new Error('提示词不能为空')
|
||||
}
|
||||
|
||||
@@ -134,17 +141,19 @@ const handleGenerate = async () => {
|
||||
for await (const textPart of result.textStream) {
|
||||
outputText.value += textPart
|
||||
}
|
||||
return outputText.value
|
||||
} catch (error) {
|
||||
console.log(`error`, error)
|
||||
// @ts-ignore
|
||||
if (error?.name !== 'AbortError' && error?.error?.name !== 'AbortError') {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message || error?.error?.message
|
||||
toast.error(
|
||||
`生成失败,请检查大模型配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
||||
)
|
||||
!oprions?.noToast &&
|
||||
toast.error(
|
||||
`生成失败,请检查大模型配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
abortController.value = null
|
||||
isGenerating.value = false
|
||||
@@ -204,8 +213,12 @@ const handleTestConfig = async () => {
|
||||
const getCurrentOutputText = () => {
|
||||
return outputText.value
|
||||
}
|
||||
// 清空文案
|
||||
const clearOutputText = () => {
|
||||
outputText.value = ''
|
||||
}
|
||||
|
||||
defineExpose({ handleGenerate, getCurrentOutputText })
|
||||
defineExpose({ handleGenerate, handleStopGenerate, getCurrentOutputText, clearOutputText })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,55 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-sheet class="h-fit p-2" border rounded>
|
||||
<v-combobox
|
||||
v-model="appStore.language"
|
||||
density="comfortable"
|
||||
label="语言"
|
||||
:items="appStore.languageList"
|
||||
no-data-text="无数据"
|
||||
@update:model-value="clearVoice"
|
||||
></v-combobox>
|
||||
<v-select
|
||||
v-model="appStore.gender"
|
||||
density="comfortable"
|
||||
label="性别"
|
||||
:items="appStore.genderList"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
@update:model-value="clearVoice"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="appStore.voice"
|
||||
density="comfortable"
|
||||
label="声音"
|
||||
:items="filteredVoicesList"
|
||||
item-title="FriendlyName"
|
||||
return-object
|
||||
no-data-text="请先选择语言和性别"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="appStore.speed"
|
||||
density="comfortable"
|
||||
label="语速"
|
||||
:items="appStore.speedList"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
<v-text-field
|
||||
v-model="appStore.tryListeningText"
|
||||
density="comfortable"
|
||||
label="试听文本"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="mb-2"
|
||||
prepend-icon="mdi-volume-high"
|
||||
block
|
||||
:loading="tryListeningLoading"
|
||||
@click="handleTryListening"
|
||||
>
|
||||
试听
|
||||
</v-btn>
|
||||
</v-sheet>
|
||||
<v-form :disabled="disabled">
|
||||
<v-sheet class="h-fit p-2" border rounded>
|
||||
<v-combobox
|
||||
v-model="appStore.language"
|
||||
density="comfortable"
|
||||
label="语言"
|
||||
:items="appStore.languageList"
|
||||
no-data-text="无数据"
|
||||
@update:model-value="clearVoice"
|
||||
></v-combobox>
|
||||
<v-select
|
||||
v-model="appStore.gender"
|
||||
density="comfortable"
|
||||
label="性别"
|
||||
:items="appStore.genderList"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
@update:model-value="clearVoice"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="appStore.voice"
|
||||
density="comfortable"
|
||||
label="声音"
|
||||
:items="filteredVoicesList"
|
||||
item-title="FriendlyName"
|
||||
return-object
|
||||
no-data-text="请先选择语言和性别"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="appStore.speed"
|
||||
density="comfortable"
|
||||
label="语速"
|
||||
:items="appStore.speedList"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
<v-text-field
|
||||
v-model="appStore.tryListeningText"
|
||||
density="comfortable"
|
||||
label="试听文本"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="mb-2"
|
||||
prepend-icon="mdi-volume-high"
|
||||
block
|
||||
:loading="tryListeningLoading"
|
||||
:disabled="disabled"
|
||||
@click="handleTryListening"
|
||||
>
|
||||
试听
|
||||
</v-btn>
|
||||
</v-sheet>
|
||||
</v-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -61,6 +64,10 @@ import { useToast } from 'vue-toastification'
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const configValid = () => {
|
||||
if (!appStore.voice) {
|
||||
toast.warning('请选择一个声音')
|
||||
@@ -140,7 +147,6 @@ const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boo
|
||||
return result
|
||||
} catch (error) {
|
||||
console.log('语音合成失败', error)
|
||||
toast.error('语音合成失败,请检查网络')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +1,66 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<v-sheet class="h-full p-2 flex flex-col" border rounded>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<v-text-field
|
||||
v-model="appStore.videoAssetsFolder"
|
||||
label="分镜视频素材文件夹"
|
||||
density="compact"
|
||||
hide-details
|
||||
readonly
|
||||
>
|
||||
</v-text-field>
|
||||
<v-btn class="mt-[2px]" prepend-icon="mdi-folder-open" @click="handleSelectFolder">
|
||||
选择
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 h-0 w-full border">
|
||||
<div
|
||||
v-if="videoAssets.length"
|
||||
class="w-full max-h-full overflow-y-auto grid grid-cols-3 gap-2 p-2"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in videoAssets"
|
||||
:key="index"
|
||||
class="w-full h-full max-h-[200px]"
|
||||
<v-form class="w-full h-full" :disabled="disabled">
|
||||
<v-sheet class="h-full p-2 flex flex-col" border rounded>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<v-text-field
|
||||
v-model="appStore.videoAssetsFolder"
|
||||
label="分镜视频素材文件夹"
|
||||
density="compact"
|
||||
hide-details
|
||||
readonly
|
||||
>
|
||||
<VideoAutoPreview
|
||||
:asset="item"
|
||||
@loaded="
|
||||
(info) => {
|
||||
videoInfoList[index] = info
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</v-text-field>
|
||||
<v-btn
|
||||
class="mt-[2px]"
|
||||
prepend-icon="mdi-folder-open"
|
||||
:disabled="disabled"
|
||||
@click="handleSelectFolder"
|
||||
>
|
||||
选择
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-empty-state
|
||||
v-else
|
||||
headline="暂无内容"
|
||||
text="从上面选择一个包含足够分镜素材的文件夹"
|
||||
></v-empty-state>
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
<v-btn
|
||||
block
|
||||
prepend-icon="mdi-refresh"
|
||||
:disabled="!appStore.videoAssetsFolder"
|
||||
:loading="refreshAssetsLoading"
|
||||
@click="refreshAssets"
|
||||
>
|
||||
刷新素材库
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-sheet>
|
||||
<div class="flex-1 h-0 w-full border">
|
||||
<div
|
||||
v-if="videoAssets.length"
|
||||
class="w-full max-h-full overflow-y-auto grid grid-cols-3 gap-2 p-2"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full max-h-[200px]"
|
||||
v-for="(item, index) in videoAssets"
|
||||
:key="index"
|
||||
>
|
||||
<VideoAutoPreview
|
||||
:asset="item"
|
||||
@loaded="
|
||||
(info) => {
|
||||
videoInfoList[index] = info
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<v-empty-state
|
||||
v-else
|
||||
headline="暂无内容"
|
||||
text="从上面选择一个包含足够分镜素材的文件夹"
|
||||
></v-empty-state>
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
<v-btn
|
||||
block
|
||||
prepend-icon="mdi-refresh"
|
||||
:disabled="disabled || !appStore.videoAssetsFolder"
|
||||
:loading="refreshAssetsLoading"
|
||||
@click="refreshAssets"
|
||||
>
|
||||
刷新素材库
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -69,6 +76,10 @@ import random from 'random'
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
// 选择文件夹
|
||||
const handleSelectFolder = async () => {
|
||||
const folderPath = await window.electron.selectFolder({
|
||||
|
||||
@@ -1,30 +1,46 @@
|
||||
<template>
|
||||
<div class="h-0 flex-1 relative">
|
||||
<div class="absolute top-2/12 w-full flex justify-center">
|
||||
<v-chip> 空闲,可以开始合成 </v-chip>
|
||||
<!-- <v-chip variant="elevated"> 正在使用 AI 大模型生成文案 </v-chip> -->
|
||||
<!-- <v-chip variant="elevated"> 正在使用 TTS 合成语音 </v-chip> -->
|
||||
<!-- <v-chip variant="elevated"> 正在处理分镜素材 </v-chip> -->
|
||||
<!-- <v-chip variant="elevated"> 正在渲染视频 </v-chip> -->
|
||||
<!-- <v-chip variant="elevated" color="success"> 渲染成功,可以开始下一个 </v-chip> -->
|
||||
<!-- <v-chip variant="elevated" color="error"> 渲染失败,请重新尝试 </v-chip> -->
|
||||
<div class="absolute top-1/12 w-full flex justify-center cursor-default select-none">
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.None"> 空闲,可以开始合成 </v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.GenerateText" variant="elevated">
|
||||
正在使用 AI 大模型生成文案
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SynthesizedSpeech" variant="elevated">
|
||||
正在使用 TTS 合成语音
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SegmentVideo" variant="elevated">
|
||||
正在处理分镜素材
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Rendering" variant="elevated">
|
||||
正在渲染视频
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="appStore.renderStatus === RenderStatus.Completed"
|
||||
variant="elevated"
|
||||
color="success"
|
||||
>
|
||||
渲染成功,可以开始下一个
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Failed" variant="elevated" color="error">
|
||||
渲染失败,请重新尝试
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-sheet
|
||||
class="h-full p-2 pt-4 flex flex-col gap-6 items-center justify-between"
|
||||
border
|
||||
rounded
|
||||
>
|
||||
<div class="w-full h-0 flex-1 flex gap-10 items-center justify-center">
|
||||
<v-progress-circular
|
||||
color="primary"
|
||||
v-model="renderProgress"
|
||||
:size="96"
|
||||
:width="8"
|
||||
></v-progress-circular>
|
||||
<v-sheet class="h-full p-2 pt-4 flex flex-col gap-6 items-center justify-center" border rounded>
|
||||
<div class="w-full h-[120px] flex gap-10 items-center justify-center">
|
||||
<div class="flex flex-col gap-4">
|
||||
<v-progress-circular
|
||||
color="indigo"
|
||||
v-model="renderProgress"
|
||||
:indeterminate="taskInProgress && appStore.renderStatus !== RenderStatus.Rendering"
|
||||
:size="96"
|
||||
:width="8"
|
||||
>
|
||||
</v-progress-circular>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<v-btn
|
||||
class=""
|
||||
v-if="!taskInProgress"
|
||||
size="x-large"
|
||||
color="deep-purple-accent-3"
|
||||
prepend-icon="mdi-rocket-launch"
|
||||
@@ -32,9 +48,18 @@
|
||||
>
|
||||
开始合成
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
size="x-large"
|
||||
color="red"
|
||||
prepend-icon="mdi-stop"
|
||||
@click="emit('cancelRender')"
|
||||
>
|
||||
停止合成
|
||||
</v-btn>
|
||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps"> 合成配置 </v-btn>
|
||||
<v-btn v-bind="activatorProps" :disabled="taskInProgress"> 合成配置 </v-btn>
|
||||
</template>
|
||||
|
||||
<v-card prepend-icon="mdi-text-box-edit-outline" title="配置合成选项">
|
||||
@@ -118,6 +143,17 @@
|
||||
</v-dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-center">
|
||||
<v-switch
|
||||
v-model="appStore.autoBatch"
|
||||
label="自动批量合成"
|
||||
color="indigo"
|
||||
density="compact"
|
||||
hide-details
|
||||
:disabled="taskInProgress"
|
||||
></v-switch>
|
||||
</div>
|
||||
</v-sheet>
|
||||
|
||||
<div class="absolute bottom-2 w-full flex justify-center text-sm">
|
||||
@@ -129,15 +165,24 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRaw, nextTick } from 'vue'
|
||||
import { useAppStore } from '@/store'
|
||||
import { ref, toRaw, nextTick, computed } from 'vue'
|
||||
import { RenderStatus, useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'renderVideo'): void
|
||||
(e: 'cancelRender'): void
|
||||
}>()
|
||||
|
||||
const taskInProgress = computed(() => {
|
||||
return (
|
||||
appStore.renderStatus !== RenderStatus.None &&
|
||||
appStore.renderStatus !== RenderStatus.Completed &&
|
||||
appStore.renderStatus !== RenderStatus.Failed
|
||||
)
|
||||
})
|
||||
|
||||
const renderProgress = ref(0)
|
||||
window.ipcRenderer.on('render-video-progress', (_, progress: number) => {
|
||||
renderProgress.value = progress
|
||||
|
||||
@@ -12,11 +12,17 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="w-1/3 h-full">
|
||||
<VideoManage ref="VideoManageInstance" />
|
||||
<VideoManage
|
||||
ref="VideoManageInstance"
|
||||
:disabled="appStore.renderStatus === RenderStatus.SegmentVideo"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-1/3 h-full flex flex-col gap-3">
|
||||
<TtsControl ref="TtsControlInstance" />
|
||||
<VideoRender @render-video="handleRenderVideo" />
|
||||
<TtsControl
|
||||
ref="TtsControlInstance"
|
||||
:disabled="appStore.renderStatus === RenderStatus.SynthesizedSpeech"
|
||||
/>
|
||||
<VideoRender @render-video="handleRenderVideo" @cancel-render="handleCancelRender" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,33 +77,43 @@ const handleRenderVideo = async () => {
|
||||
try {
|
||||
// 获取文案
|
||||
appStore.updateRenderStatus(RenderStatus.GenerateText)
|
||||
const text = TextGenerateInstance.value?.getCurrentOutputText()
|
||||
if (!text) {
|
||||
toast.warning('请先生成文案')
|
||||
return
|
||||
}
|
||||
const text =
|
||||
TextGenerateInstance.value?.getCurrentOutputText() ||
|
||||
(await TextGenerateInstance.value?.handleGenerate())!
|
||||
|
||||
// TTS合成语音
|
||||
// @ts-ignore
|
||||
if (appStore.renderStatus !== RenderStatus.GenerateText) {
|
||||
return
|
||||
}
|
||||
appStore.updateRenderStatus(RenderStatus.SynthesizedSpeech)
|
||||
const ttsResult = await TtsControlInstance.value?.synthesizedSpeechToFile({
|
||||
text,
|
||||
withCaption: true,
|
||||
})
|
||||
if (ttsResult?.duration === undefined) {
|
||||
toast.warning('语音合成失败,音频文件损坏')
|
||||
return
|
||||
throw new Error('语音合成失败,音频文件损坏')
|
||||
}
|
||||
if (ttsResult?.duration === 0) {
|
||||
toast.warning('语音时长为0秒,可能文案为空')
|
||||
return
|
||||
throw new Error('语音时长为0秒,检查TTS语音合成配置及网络连接是否正常')
|
||||
}
|
||||
|
||||
// 获取视频片段
|
||||
// @ts-ignore
|
||||
if (appStore.renderStatus !== RenderStatus.SynthesizedSpeech) {
|
||||
return
|
||||
}
|
||||
appStore.updateRenderStatus(RenderStatus.SegmentVideo)
|
||||
const videoSegments = VideoManageInstance.value?.getVideoSegments({
|
||||
duration: ttsResult.duration,
|
||||
})!
|
||||
await new Promise((resolve) => setTimeout(resolve, random.integer(1000, 3000)))
|
||||
|
||||
// 合成视频
|
||||
// @ts-ignore
|
||||
if (appStore.renderStatus !== RenderStatus.SegmentVideo) {
|
||||
return
|
||||
}
|
||||
appStore.updateRenderStatus(RenderStatus.Rendering)
|
||||
await window.electron.renderVideo({
|
||||
...videoSegments,
|
||||
@@ -118,8 +134,16 @@ const handleRenderVideo = async () => {
|
||||
|
||||
toast.success('视频合成成功')
|
||||
appStore.updateRenderStatus(RenderStatus.Completed)
|
||||
|
||||
if (appStore.autoBatch) {
|
||||
toast.info('开始合成下一个')
|
||||
TextGenerateInstance.value?.clearOutputText()
|
||||
handleRenderVideo()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('视频合成失败:', error)
|
||||
if (appStore.renderStatus === RenderStatus.None) return
|
||||
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message || error?.error?.message
|
||||
toast.error(
|
||||
@@ -128,6 +152,29 @@ const handleRenderVideo = async () => {
|
||||
appStore.updateRenderStatus(RenderStatus.Failed)
|
||||
}
|
||||
}
|
||||
const handleCancelRender = () => {
|
||||
console.log('视频合成终止')
|
||||
switch (appStore.renderStatus) {
|
||||
case RenderStatus.GenerateText:
|
||||
TextGenerateInstance.value?.handleStopGenerate()
|
||||
break
|
||||
|
||||
case RenderStatus.SynthesizedSpeech:
|
||||
break
|
||||
|
||||
case RenderStatus.SegmentVideo:
|
||||
break
|
||||
|
||||
case RenderStatus.Rendering:
|
||||
window.ipcRenderer.send('cancel-render-video')
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
appStore.updateRenderStatus(RenderStatus.None)
|
||||
toast.info('视频合成已终止')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Reference in New Issue
Block a user