mirror of
https://github.com/YILS-LIN/short-video-factory.git
synced 2025-11-25 11:29:34 +08:00
feat: english support
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { app, BrowserWindow, screen } from 'electron'
|
||||
import { app, BrowserWindow, screen, Menu } from 'electron'
|
||||
import type { MenuItemConstructorOptions } from 'electron'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import GlobalSetting from '../setting.global'
|
||||
@@ -69,6 +70,95 @@ function createWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildMenu() {
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
// macOS standard app menu
|
||||
...(process.platform === 'darwin'
|
||||
? [
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' },
|
||||
] as MenuItemConstructorOptions[],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Language',
|
||||
submenu: [
|
||||
{
|
||||
label: 'English',
|
||||
type: 'radio',
|
||||
checked: true,
|
||||
click: () => {
|
||||
BrowserWindow.getAllWindows().forEach((w) => w.webContents.send('set-locale', 'en'))
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '中文',
|
||||
type: 'radio',
|
||||
click: () => {
|
||||
BrowserWindow.getAllWindows().forEach((w) => w.webContents.send('set-locale', 'zh-CN'))
|
||||
},
|
||||
},
|
||||
] as MenuItemConstructorOptions[],
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ role: 'selectAll' },
|
||||
] as MenuItemConstructorOptions[],
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload' },
|
||||
{ role: 'forceReload' },
|
||||
{ role: 'toggleDevTools' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
] as MenuItemConstructorOptions[],
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [{ role: 'minimize' }, { role: 'close' }] as MenuItemConstructorOptions[],
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn More',
|
||||
click: async () => {
|
||||
const { shell } = await import('electron')
|
||||
await shell.openExternal('https://github.com/YILS-LIN/short-video-factory')
|
||||
},
|
||||
},
|
||||
] as MenuItemConstructorOptions[],
|
||||
},
|
||||
]
|
||||
|
||||
const menu = Menu.buildFromTemplate(template)
|
||||
Menu.setApplicationMenu(menu)
|
||||
}
|
||||
|
||||
//关闭所有窗口后退出,macOS除外。在那里,这很常见
|
||||
//让应用程序及其菜单栏保持活动状态,直到用户退出
|
||||
//显式使用Cmd+Q。
|
||||
@@ -101,4 +191,7 @@ app.whenReady().then(() => {
|
||||
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
|
||||
// 允许本地网络请求
|
||||
app.commandLine.appendSwitch('disable-features', 'BlockInsecurePrivateNetworkRequests')
|
||||
|
||||
// Build application menu
|
||||
buildMenu()
|
||||
})
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"music-metadata": "^11.7.3",
|
||||
"subtitle": "4.2.2-alpha.0",
|
||||
"ws": "^8.18.3"
|
||||
"ws": "^8.18.3",
|
||||
"vue-i18n": "^9.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/openai": "^1.3.23",
|
||||
|
||||
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ importers:
|
||||
subtitle:
|
||||
specifier: 4.2.2-alpha.0
|
||||
version: 4.2.2-alpha.0
|
||||
vue-i18n:
|
||||
specifier: ^9.14.0
|
||||
version: 9.14.5(vue@3.5.17(typescript@5.6.2))
|
||||
ws:
|
||||
specifier: ^8.18.3
|
||||
version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
@@ -497,6 +500,18 @@ packages:
|
||||
'@iconify/utils@2.3.0':
|
||||
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
||||
|
||||
'@intlify/core-base@9.14.5':
|
||||
resolution: {integrity: sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@9.14.5':
|
||||
resolution: {integrity: sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@9.14.5':
|
||||
resolution: {integrity: sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -2754,6 +2769,12 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.4.37
|
||||
|
||||
vue-i18n@9.14.5:
|
||||
resolution: {integrity: sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
vue-router@4.5.1:
|
||||
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
|
||||
peerDependencies:
|
||||
@@ -3262,6 +3283,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@intlify/core-base@9.14.5':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 9.14.5
|
||||
'@intlify/shared': 9.14.5
|
||||
|
||||
'@intlify/message-compiler@9.14.5':
|
||||
dependencies:
|
||||
'@intlify/shared': 9.14.5
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@9.14.5': {}
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
@@ -5712,6 +5745,13 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.17(typescript@5.6.2)
|
||||
|
||||
vue-i18n@9.14.5(vue@3.5.17(typescript@5.6.2)):
|
||||
dependencies:
|
||||
'@intlify/core-base': 9.14.5
|
||||
'@intlify/shared': 9.14.5
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.17(typescript@5.6.2)
|
||||
|
||||
vue-router@4.5.1(vue@3.5.17(typescript@5.6.2)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
|
||||
200
src/i18n.ts
Normal file
200
src/i18n.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
app: {
|
||||
name: 'AI Short Video Factory',
|
||||
},
|
||||
prompt: { label: 'Prompt' },
|
||||
actions: { generate: 'Generate', stop: 'Stop', config: 'Configure', refreshAssets: 'Refresh Assets' },
|
||||
llm: {
|
||||
configTitle: 'Configure LLM API',
|
||||
modelName: 'Model Name',
|
||||
apiUrl: 'API URL',
|
||||
apiKey: 'API Key',
|
||||
compatibleNote: 'Compatible with any OpenAI-compatible API',
|
||||
connectSuccess: 'LLM connected successfully',
|
||||
connectFailedPrefix: 'LLM connection failed, please check your configuration',
|
||||
},
|
||||
common: { close: 'Close', test: 'Test', save: 'Save', select: 'Select', noData: 'No data' },
|
||||
output: { label: 'Output Text (editable)' },
|
||||
errors: {
|
||||
promptRequired: 'Prompt cannot be empty',
|
||||
generateFailedPrefix: 'Generation failed, please check LLM configuration',
|
||||
outputFileNameRequired: 'Please set an output file name first',
|
||||
outputPathRequired: 'Please set an export folder first',
|
||||
outputSizeRequired: 'Please set output resolution (width x height) first',
|
||||
bgmListFailed: 'Failed to load BGM list. Please check the folder exists',
|
||||
ttsFailedCorrupt: 'TTS failed: audio file is corrupted',
|
||||
ttsZeroDuration: 'TTS duration is 0s. Check TTS config and network connectivity',
|
||||
renderFailedPrefix: 'Video rendering failed. Please check all configurations',
|
||||
assetsDurationInsufficient: 'Total assets duration is insufficient',
|
||||
edgeTtsListFailed: 'Failed to fetch Edge TTS voice list. Please check your network',
|
||||
ttsConfigInvalid: 'TTS configuration is invalid',
|
||||
ttsSynthesisFailed: 'TTS synthesis failed',
|
||||
},
|
||||
success: { renderSuccess: 'Video rendered successfully' },
|
||||
info: { batchNext: 'Start next render', renderCanceled: 'Video rendering canceled' },
|
||||
empty: {
|
||||
noContent: 'No content yet',
|
||||
hintSelectFolder: 'Please choose a folder above with enough storyboard assets',
|
||||
},
|
||||
videoManage: {
|
||||
assetsFolderLabel: 'Storyboard assets folder',
|
||||
noMp4InFolder: 'No MP4 video files found in the selected folder',
|
||||
emptyFolder: 'The selected folder is empty',
|
||||
readSuccess: 'Assets loaded successfully',
|
||||
readFailed: 'Failed to read assets. Please check if the folder exists',
|
||||
},
|
||||
dialogs: {
|
||||
selectAssetsFolderTitle: 'Select storyboard assets folder',
|
||||
selectOutputFolderTitle: 'Select video export folder',
|
||||
selectBgmFolderTitle: 'Select background music folder',
|
||||
renderConfigTitle: 'Configure render options',
|
||||
},
|
||||
render: {
|
||||
status: {
|
||||
idle: 'Idle, ready to render',
|
||||
generatingText: 'Generating script with AI LLM',
|
||||
synthesizingSpeech: 'Synthesizing speech with TTS',
|
||||
segmentingVideo: 'Processing video segments',
|
||||
rendering: 'Rendering video',
|
||||
success: 'Rendered successfully, you can start the next one',
|
||||
failed: 'Render failed, please try again',
|
||||
},
|
||||
startRender: 'Start Rendering',
|
||||
stopRender: 'Stop Rendering',
|
||||
autoBatch: 'Auto batch render',
|
||||
bgmFolderLabel: 'Background music folder (.mp3, pick randomly)',
|
||||
output: {
|
||||
width: 'Output width',
|
||||
height: 'Output height',
|
||||
fileName: 'Output file name',
|
||||
format: 'Output format',
|
||||
folder: 'Output folder',
|
||||
},
|
||||
},
|
||||
tts: {
|
||||
language: 'Language',
|
||||
gender: 'Gender',
|
||||
voice: 'Voice',
|
||||
speed: 'Speed',
|
||||
tryText: 'Try-listen text',
|
||||
tryListen: 'Try listen',
|
||||
selectLanguageGenderFirst: 'Please select language and gender first',
|
||||
selectVoiceWarning: 'Please select a voice',
|
||||
tryTextEmptyWarning: 'Try-listen text cannot be empty',
|
||||
playTryAudio: 'Playing try-listen audio',
|
||||
trySynthesisFailedNetwork: 'Failed to synthesize try-listen audio. Please check network',
|
||||
genderMale: 'Male',
|
||||
genderFemale: 'Female',
|
||||
speedSlow: 'Slow',
|
||||
speedMedium: 'Medium',
|
||||
speedFast: 'Fast',
|
||||
},
|
||||
footer: { poweredBy: 'Powered by YILS (Blog: https://yils.blog)' },
|
||||
},
|
||||
'zh-CN': {
|
||||
app: { name: 'AI Short Video Factory - 短视频工厂' },
|
||||
prompt: { label: '提示词' },
|
||||
actions: { generate: '生成', stop: '停止', config: '配置', refreshAssets: '刷新素材库' },
|
||||
llm: {
|
||||
configTitle: '配置大语言模型接口',
|
||||
modelName: '模型名称',
|
||||
apiUrl: 'API 地址',
|
||||
apiKey: 'API Key',
|
||||
compatibleNote: '兼容任意 OpenAI 标准接口',
|
||||
connectSuccess: '大模型连接成功',
|
||||
connectFailedPrefix: '大模型连接失败,请检查配置是否正确',
|
||||
},
|
||||
common: { close: '关闭', test: '测试', save: '保存', select: '选择', noData: '无数据' },
|
||||
output: { label: '输出文案(可编辑)' },
|
||||
errors: {
|
||||
promptRequired: '提示词不能为空',
|
||||
generateFailedPrefix: '生成失败,请检查大模型配置是否正确',
|
||||
outputFileNameRequired: '请先配置导出文件名',
|
||||
outputPathRequired: '请先配置导出文件夹',
|
||||
outputSizeRequired: '请先配置导出分辨率(宽高)',
|
||||
bgmListFailed: '获取背景音乐列表失败,请检查文件夹是否存在',
|
||||
ttsFailedCorrupt: '语音合成失败,音频文件损坏',
|
||||
ttsZeroDuration: '语音时长为0秒,检查TTS语音合成配置及网络连接是否正常',
|
||||
renderFailedPrefix: '视频合成失败,请检查各项配置是否正确',
|
||||
assetsDurationInsufficient: '素材总时长不足',
|
||||
edgeTtsListFailed: '获取EdgeTTS语音列表失败,请检查网络',
|
||||
ttsConfigInvalid: 'TTS语音合成配置无效',
|
||||
ttsSynthesisFailed: '语音合成失败',
|
||||
},
|
||||
success: { renderSuccess: '视频合成成功' },
|
||||
info: { batchNext: '开始合成下一个', renderCanceled: '视频合成已终止' },
|
||||
empty: {
|
||||
noContent: '暂无内容',
|
||||
hintSelectFolder: '从上面选择一个包含足够分镜素材的文件夹',
|
||||
},
|
||||
videoManage: {
|
||||
assetsFolderLabel: '分镜视频素材文件夹',
|
||||
noMp4InFolder: '选择的文件夹中不包含MP4视频文件',
|
||||
emptyFolder: '选择的文件夹为空',
|
||||
readSuccess: '素材读取成功',
|
||||
readFailed: '素材读取失败,请检查文件夹是否存在',
|
||||
},
|
||||
dialogs: {
|
||||
selectAssetsFolderTitle: '选择分镜素材文件夹',
|
||||
selectOutputFolderTitle: '选择视频导出文件夹',
|
||||
selectBgmFolderTitle: '选择背景音乐文件夹',
|
||||
renderConfigTitle: '配置合成选项',
|
||||
},
|
||||
render: {
|
||||
status: {
|
||||
idle: '空闲,可以开始合成',
|
||||
generatingText: '正在使用 AI 大模型生成文案',
|
||||
synthesizingSpeech: '正在使用 TTS 合成语音',
|
||||
segmentingVideo: '正在处理分镜素材',
|
||||
rendering: '正在渲染视频',
|
||||
success: '渲染成功,可以开始下一个',
|
||||
failed: '渲染失败,请重新尝试',
|
||||
},
|
||||
startRender: '开始合成',
|
||||
stopRender: '停止合成',
|
||||
autoBatch: '自动批量合成',
|
||||
bgmFolderLabel: '背景音乐文件夹(.mp3格式,从中随机选取)',
|
||||
output: {
|
||||
width: '导出视频宽度',
|
||||
height: '导出视频高度',
|
||||
fileName: '导出文件名',
|
||||
format: '导出格式',
|
||||
folder: '导出文件夹',
|
||||
},
|
||||
},
|
||||
tts: {
|
||||
language: '语言',
|
||||
gender: '性别',
|
||||
voice: '声音',
|
||||
speed: '语速',
|
||||
tryText: '试听文本',
|
||||
tryListen: '试听',
|
||||
selectLanguageGenderFirst: '请先选择语言和性别',
|
||||
selectVoiceWarning: '请选择一个声音',
|
||||
tryTextEmptyWarning: '试听文本不能为空',
|
||||
playTryAudio: '播放试听语音',
|
||||
trySynthesisFailedNetwork: '试听语音合成失败,请检查网络',
|
||||
genderMale: '男性',
|
||||
genderFemale: '女性',
|
||||
speedSlow: '慢',
|
||||
speedMedium: '中',
|
||||
speedFast: '快',
|
||||
},
|
||||
footer: { poweredBy: 'Powered by YILS(博客地址:https://yils.blog)' },
|
||||
},
|
||||
}
|
||||
|
||||
const stored = (typeof localStorage !== 'undefined' && localStorage.getItem('locale')) || undefined
|
||||
const locale = stored || 'en'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale,
|
||||
fallbackLocale: 'en',
|
||||
messages,
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="layout-container">
|
||||
<div class="logo" v-if="!route.meta.hideAppIcon">
|
||||
<img src="/icon.png" alt="" />
|
||||
<span>{{ GlobalSetting.appName }}</span>
|
||||
<span>{{ t('app.name') }}</span>
|
||||
</div>
|
||||
<div class="window-control-bar">
|
||||
<div class="control-btn control-btn-min" @click="handleMin">
|
||||
@@ -21,12 +21,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import GlobalSetting from '../../setting.global'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const route = useRoute()
|
||||
const windowIsMaxed = ref(false)
|
||||
const { t } = useI18n()
|
||||
|
||||
window.addEventListener('resize', async () => {
|
||||
windowIsMaxed.value = await window.electron.isWinMaxed()
|
||||
|
||||
16
src/main.ts
16
src/main.ts
@@ -12,7 +12,7 @@ import 'virtual:uno.css'
|
||||
import './assets/base.scss'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import GlobalSetting from '../setting.global'
|
||||
import i18n from './i18n'
|
||||
import router from './router/index.ts'
|
||||
import store from './store/index.ts'
|
||||
import App from './App.vue'
|
||||
@@ -29,12 +29,14 @@ const vuetify = createVuetify({
|
||||
},
|
||||
})
|
||||
|
||||
document.title = GlobalSetting.appName
|
||||
// Set initial document title from i18n
|
||||
document.title = i18n.global.t('app.name') as string
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(vuetify)
|
||||
app.use(Toast, { position: 'bottom-left', pauseOnFocusLoss: false } as PluginOptions)
|
||||
app.use(i18n)
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
|
||||
@@ -43,4 +45,14 @@ app.mount('#app').$nextTick(() => {
|
||||
window.ipcRenderer.on('main-process-message', (_event, message) => {
|
||||
console.log(message)
|
||||
})
|
||||
|
||||
// Language switching from Electron menu
|
||||
window.ipcRenderer.on('set-locale', (_event, locale: string) => {
|
||||
// @ts-ignore
|
||||
i18n.global.locale.value = locale
|
||||
try {
|
||||
localStorage.setItem('locale', locale)
|
||||
} catch {}
|
||||
document.title = i18n.global.t('app.name') as string
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<v-textarea
|
||||
class="h-full"
|
||||
v-model="appStore.prompt"
|
||||
label="提示词"
|
||||
:label="t('prompt.label')"
|
||||
counter
|
||||
persistent-counter
|
||||
no-resize
|
||||
@@ -19,7 +19,7 @@
|
||||
:disabled="disabled"
|
||||
@click="handleGenerate"
|
||||
>
|
||||
生成
|
||||
{{ t('actions.generate') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
@@ -29,51 +29,51 @@
|
||||
:disabled="disabled"
|
||||
@click="handleStopGenerate"
|
||||
>
|
||||
停止
|
||||
{{ t('actions.stop') }}
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" :disabled="disabled"> 配置 </v-btn>
|
||||
<v-btn v-bind="activatorProps" :disabled="disabled"> {{ t('actions.config') }} </v-btn>
|
||||
</template>
|
||||
|
||||
<v-card prepend-icon="mdi-text-box-edit-outline" title="配置大语言模型接口">
|
||||
<v-card prepend-icon="mdi-text-box-edit-outline" :title="t('llm.configTitle')">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
label="模型名称"
|
||||
:label="t('llm.modelName')"
|
||||
v-model="config.modelName"
|
||||
required
|
||||
clearable
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
label="API 地址"
|
||||
:label="t('llm.apiUrl')"
|
||||
v-model="config.apiUrl"
|
||||
required
|
||||
clearable
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
label="API Key"
|
||||
:label="t('llm.apiKey')"
|
||||
v-model="config.apiKey"
|
||||
type="password"
|
||||
required
|
||||
clearable
|
||||
></v-text-field>
|
||||
<small class="text-caption text-medium-emphasis">兼容任意 OpenAI 标准接口</small>
|
||||
<small class="text-caption text-medium-emphasis">{{ t('llm.compatibleNote') }}</small>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text="关闭" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||
<v-btn :text="t('common.close')" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||
<v-btn
|
||||
color="success"
|
||||
text="测试"
|
||||
:text="t('common.test')"
|
||||
variant="tonal"
|
||||
:loading="testStatus === TestStatusEnum.LOADING"
|
||||
@click="handleTestConfig"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
text="保存"
|
||||
:text="t('common.save')"
|
||||
variant="tonal"
|
||||
@click="handleSaveConfig"
|
||||
></v-btn>
|
||||
@@ -86,7 +86,7 @@
|
||||
<v-textarea
|
||||
class="h-full"
|
||||
v-model="outputText"
|
||||
label="输出文案(可编辑)"
|
||||
:label="t('output.label')"
|
||||
counter
|
||||
persistent-counter
|
||||
no-resize
|
||||
@@ -102,9 +102,11 @@ import { nextTick, ref, toRaw } from 'vue'
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
import { generateText, streamText } from 'ai'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
@@ -116,8 +118,8 @@ const isGenerating = ref(false)
|
||||
const abortController = ref<AbortController | null>(null)
|
||||
const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
||||
if (!appStore.prompt) {
|
||||
!oprions?.noToast && toast.warning('提示词不能为空')
|
||||
throw new Error('提示词不能为空')
|
||||
!oprions?.noToast && toast.warning(t('errors.promptRequired'))
|
||||
throw new Error(t('errors.promptRequired') as string)
|
||||
}
|
||||
|
||||
const openai = createOpenAI({
|
||||
@@ -149,9 +151,7 @@ const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message || error?.error?.message
|
||||
!oprions?.noToast &&
|
||||
toast.error(
|
||||
`生成失败,请检查大模型配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
||||
)
|
||||
toast.error(`${t('errors.generateFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`)
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
@@ -197,15 +197,13 @@ const handleTestConfig = async () => {
|
||||
})
|
||||
console.log(`result`, result)
|
||||
testStatus.value = TestStatusEnum.SUCCESS
|
||||
toast.success('大模型连接成功')
|
||||
toast.success(t('llm.connectSuccess'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
testStatus.value = TestStatusEnum.ERROR
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message
|
||||
toast.error(
|
||||
`大模型连接失败,请检查配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
||||
)
|
||||
toast.error(`${t('llm.connectFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<v-combobox
|
||||
v-model="appStore.language"
|
||||
density="comfortable"
|
||||
label="语言"
|
||||
:label="t('tts.language')"
|
||||
:items="appStore.languageList"
|
||||
no-data-text="无数据"
|
||||
:no-data-text="t('common.noData')"
|
||||
@update:model-value="clearVoice"
|
||||
></v-combobox>
|
||||
<v-select
|
||||
v-model="appStore.gender"
|
||||
density="comfortable"
|
||||
label="性别"
|
||||
:items="appStore.genderList"
|
||||
:label="t('tts.gender')"
|
||||
:items="genderItems"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
@update:model-value="clearVoice"
|
||||
@@ -22,24 +22,24 @@
|
||||
<v-select
|
||||
v-model="appStore.voice"
|
||||
density="comfortable"
|
||||
label="声音"
|
||||
:label="t('tts.voice')"
|
||||
:items="filteredVoicesList"
|
||||
item-title="FriendlyName"
|
||||
return-object
|
||||
no-data-text="请先选择语言和性别"
|
||||
:no-data-text="t('tts.selectLanguageGenderFirst')"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="appStore.speed"
|
||||
density="comfortable"
|
||||
label="语速"
|
||||
:items="appStore.speedList"
|
||||
:label="t('tts.speed')"
|
||||
:items="speedItems"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
<v-text-field
|
||||
v-model="appStore.tryListeningText"
|
||||
density="comfortable"
|
||||
label="试听文本"
|
||||
:label="t('tts.tryText')"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="mb-2"
|
||||
@@ -49,7 +49,7 @@
|
||||
:disabled="disabled"
|
||||
@click="handleTryListening"
|
||||
>
|
||||
试听
|
||||
{{ t('tts.tryListen') }}
|
||||
</v-btn>
|
||||
</v-sheet>
|
||||
</v-form>
|
||||
@@ -58,11 +58,13 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useToast } from 'vue-toastification'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
@@ -70,12 +72,12 @@ defineProps<{
|
||||
|
||||
const configValid = () => {
|
||||
if (!appStore.voice) {
|
||||
toast.warning('请选择一个声音')
|
||||
toast.warning(t('tts.selectVoiceWarning'))
|
||||
return false
|
||||
}
|
||||
|
||||
if (!appStore.tryListeningText) {
|
||||
toast.warning('试听文本不能为空')
|
||||
toast.warning(t('tts.tryTextEmptyWarning'))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -97,10 +99,10 @@ const handleTryListening = async () => {
|
||||
})
|
||||
const audio = new Audio(`data:audio/mp3;base64,${speech}`)
|
||||
audio.play()
|
||||
toast.info('播放试听语音')
|
||||
toast.info(t('tts.playTryAudio'))
|
||||
} catch (error) {
|
||||
console.log('试听语音合成失败', error)
|
||||
toast.error('试听语音合成失败,请检查网络')
|
||||
toast.error(t('tts.trySynthesisFailedNetwork'))
|
||||
} finally {
|
||||
tryListeningLoading.value = false
|
||||
}
|
||||
@@ -116,13 +118,28 @@ const filteredVoicesList = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const genderItems = computed(() => {
|
||||
return [
|
||||
{ label: t('tts.genderMale'), value: 'Male' },
|
||||
{ label: t('tts.genderFemale'), value: 'Female' },
|
||||
]
|
||||
})
|
||||
|
||||
const speedItems = computed(() => {
|
||||
return [
|
||||
{ label: t('tts.speedSlow'), value: -30 },
|
||||
{ label: t('tts.speedMedium'), value: 0 },
|
||||
{ label: t('tts.speedFast'), value: 30 },
|
||||
]
|
||||
})
|
||||
|
||||
const fetchVoices = async () => {
|
||||
try {
|
||||
appStore.originalVoicesList = await window.electron.edgeTtsGetVoiceList()
|
||||
console.log('EdgeTTS语音列表更新:', appStore.originalVoicesList)
|
||||
} catch (error) {
|
||||
console.log('获取EdgeTTS语音列表失败', error)
|
||||
toast.error('获取EdgeTTS语音列表失败,请检查网络')
|
||||
toast.error(t('errors.edgeTtsListFailed'))
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
@@ -133,7 +150,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boolean }) => {
|
||||
if (!configValid()) throw new Error('TTS语音合成配置无效')
|
||||
if (!configValid()) throw new Error(t('errors.ttsConfigInvalid'))
|
||||
|
||||
try {
|
||||
const result = await window.electron.edgeTtsSynthesizeToFile({
|
||||
@@ -147,7 +164,7 @@ const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boo
|
||||
return result
|
||||
} catch (error) {
|
||||
console.log('语音合成失败', error)
|
||||
throw error
|
||||
throw new Error(t('errors.ttsSynthesisFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="flex gap-2 mb-2">
|
||||
<v-text-field
|
||||
v-model="appStore.videoAssetsFolder"
|
||||
label="分镜视频素材文件夹"
|
||||
:label="t('videoManage.assetsFolderLabel')"
|
||||
density="compact"
|
||||
hide-details
|
||||
readonly
|
||||
@@ -17,7 +17,7 @@
|
||||
:disabled="disabled"
|
||||
@click="handleSelectFolder"
|
||||
>
|
||||
选择
|
||||
{{ t('common.select') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
</div>
|
||||
<v-empty-state
|
||||
v-else
|
||||
headline="暂无内容"
|
||||
text="从上面选择一个包含足够分镜素材的文件夹"
|
||||
:headline="t('empty.noContent')"
|
||||
:text="t('empty.hintSelectFolder')"
|
||||
></v-empty-state>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
:loading="refreshAssetsLoading"
|
||||
@click="refreshAssets"
|
||||
>
|
||||
刷新素材库
|
||||
{{ t('actions.refreshAssets') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-sheet>
|
||||
@@ -66,6 +66,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ListFilesFromFolderRecord } from '~/electron/types'
|
||||
@@ -75,6 +76,7 @@ import random from 'random'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
@@ -83,7 +85,7 @@ defineProps<{
|
||||
// 选择文件夹
|
||||
const handleSelectFolder = async () => {
|
||||
const folderPath = await window.electron.selectFolder({
|
||||
title: '选择分镜素材文件夹',
|
||||
title: t('dialogs.selectAssetsFolderTitle'),
|
||||
defaultPath: appStore.videoAssetsFolder,
|
||||
})
|
||||
console.log('用户选择分镜素材文件夹,绝对路径:', folderPath)
|
||||
@@ -109,16 +111,16 @@ const refreshAssets = async () => {
|
||||
videoAssets.value = assets.filter((asset) => asset.name.endsWith('.mp4'))
|
||||
if (!videoAssets.value.length) {
|
||||
if (assets.length) {
|
||||
toast.warning('选择的文件夹中不包含MP4视频文件')
|
||||
toast.warning(t('videoManage.noMp4InFolder'))
|
||||
} else {
|
||||
toast.warning('选择的文件夹为空')
|
||||
toast.warning(t('videoManage.emptyFolder'))
|
||||
}
|
||||
} else {
|
||||
toast.success('素材读取成功')
|
||||
toast.success(t('videoManage.readSuccess'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
toast.error('素材读取失败,请检查文件夹是否存在')
|
||||
toast.error(t('videoManage.readFailed'))
|
||||
} finally {
|
||||
refreshAssetsLoading.value = false
|
||||
}
|
||||
@@ -130,7 +132,7 @@ const videoInfoList = ref<VideoInfo[]>([])
|
||||
const getVideoSegments = (options: { duration: number }) => {
|
||||
// 判断素材库是否满足时长要求
|
||||
if (videoInfoList.value.reduce((pre, cur) => pre + cur.duration, 0) < options.duration) {
|
||||
throw new Error('素材总时长不足')
|
||||
throw new Error(t('errors.assetsDurationInsufficient'))
|
||||
}
|
||||
|
||||
// 搜集随机素材片段
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<div class="h-0 flex-1 relative">
|
||||
<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.None"> {{ t('render.status.idle') }} </v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.GenerateText" variant="elevated">
|
||||
正在使用 AI 大模型生成文案
|
||||
{{ t('render.status.generatingText') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SynthesizedSpeech" variant="elevated">
|
||||
正在使用 TTS 合成语音
|
||||
{{ t('render.status.synthesizingSpeech') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SegmentVideo" variant="elevated">
|
||||
正在处理分镜素材
|
||||
{{ t('render.status.segmentingVideo') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Rendering" variant="elevated">
|
||||
正在渲染视频
|
||||
{{ t('render.status.rendering') }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="appStore.renderStatus === RenderStatus.Completed"
|
||||
variant="elevated"
|
||||
color="success"
|
||||
>
|
||||
渲染成功,可以开始下一个
|
||||
{{ t('render.status.success') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Failed" variant="elevated" color="error">
|
||||
渲染失败,请重新尝试
|
||||
{{ t('render.status.failed') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
prepend-icon="mdi-rocket-launch"
|
||||
@click="emit('renderVideo')"
|
||||
>
|
||||
开始合成
|
||||
{{ t('render.startRender') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
@@ -55,31 +55,31 @@
|
||||
prepend-icon="mdi-stop"
|
||||
@click="emit('cancelRender')"
|
||||
>
|
||||
停止合成
|
||||
{{ t('render.stopRender') }}
|
||||
</v-btn>
|
||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" :disabled="taskInProgress"> 合成配置 </v-btn>
|
||||
<v-btn v-bind="activatorProps" :disabled="taskInProgress"> {{ t('actions.config') }} </v-btn>
|
||||
</template>
|
||||
|
||||
<v-card prepend-icon="mdi-text-box-edit-outline" title="配置合成选项">
|
||||
<v-card prepend-icon="mdi-text-box-edit-outline" :title="t('dialogs.renderConfigTitle')">
|
||||
<v-card-text>
|
||||
<div class="w-full flex gap-2 mb-4 items-center">
|
||||
<v-text-field
|
||||
label="导出视频宽度"
|
||||
:label="t('render.output.width')"
|
||||
v-model="config.outputSize.width"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="config.outputSize.height"
|
||||
label="导出视频高度"
|
||||
:label="t('render.output.height')"
|
||||
hide-details
|
||||
required
|
||||
></v-text-field>
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mb-4 items-center">
|
||||
<v-text-field
|
||||
label="导出文件名"
|
||||
:label="t('render.output.fileName')"
|
||||
v-model="config.outputFileName"
|
||||
hide-details
|
||||
required
|
||||
@@ -88,7 +88,7 @@
|
||||
<v-text-field
|
||||
class="w-[120px] flex-none"
|
||||
v-model="config.outputFileExt"
|
||||
label="导出格式"
|
||||
:label="t('render.output.format')"
|
||||
hide-details
|
||||
readonly
|
||||
required
|
||||
@@ -96,7 +96,7 @@
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mb-4 items-center">
|
||||
<v-text-field
|
||||
label="导出文件夹"
|
||||
:label="t('render.output.folder')"
|
||||
v-model="config.outputPath"
|
||||
hide-details
|
||||
readonly
|
||||
@@ -107,12 +107,12 @@
|
||||
prepend-icon="mdi-folder-open"
|
||||
@click="handleSelectOutputFolder"
|
||||
>
|
||||
选择
|
||||
{{ t('common.select') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mb-2 items-center">
|
||||
<v-text-field
|
||||
label="背景音乐文件夹(.mp3格式,从中随机选取)"
|
||||
:label="t('render.bgmFolderLabel')"
|
||||
v-model="config.bgmPath"
|
||||
hide-details
|
||||
readonly
|
||||
@@ -124,17 +124,17 @@
|
||||
prepend-icon="mdi-folder-open"
|
||||
@click="handleSelectBgmFolder"
|
||||
>
|
||||
选择
|
||||
{{ t('common.select') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text="关闭" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||
<v-btn :text="t('common.close')" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
text="保存"
|
||||
:text="t('common.save')"
|
||||
variant="tonal"
|
||||
@click="handleSaveConfig"
|
||||
></v-btn>
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="w-full flex justify-center">
|
||||
<v-switch
|
||||
v-model="appStore.autoBatch"
|
||||
label="自动批量合成"
|
||||
:label="t('render.autoBatch')"
|
||||
color="indigo"
|
||||
density="compact"
|
||||
hide-details
|
||||
@@ -158,7 +158,7 @@
|
||||
|
||||
<div class="absolute bottom-2 w-full flex justify-center text-sm">
|
||||
<span class="text-indigo cursor-pointer select-none" @click="handleOpenHomePage">
|
||||
Powered by YILS(博客地址:https://yils.blog)
|
||||
{{ t('footer.poweredBy') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,9 +166,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRaw, nextTick, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RenderStatus, useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'renderVideo'): void
|
||||
@@ -206,7 +208,7 @@ const handleSaveConfig = () => {
|
||||
// 选择文件夹
|
||||
const handleSelectOutputFolder = async () => {
|
||||
const folderPath = await window.electron.selectFolder({
|
||||
title: '选择视频导出文件夹',
|
||||
title: t('dialogs.selectOutputFolderTitle'),
|
||||
defaultPath: config.value.outputPath,
|
||||
})
|
||||
console.log('用户选择视频导出文件夹,绝对路径:', folderPath)
|
||||
@@ -216,7 +218,7 @@ const handleSelectOutputFolder = async () => {
|
||||
}
|
||||
const handleSelectBgmFolder = async () => {
|
||||
const folderPath = await window.electron.selectFolder({
|
||||
title: '选择背景音乐文件夹',
|
||||
title: t('dialogs.selectBgmFolderTitle'),
|
||||
defaultPath: config.value.bgmPath,
|
||||
})
|
||||
console.log('用户选择背景音乐文件夹,绝对路径:', folderPath)
|
||||
|
||||
@@ -35,6 +35,7 @@ import TtsControl from './components/tts-control.vue'
|
||||
import VideoRender from './components/video-render.vue'
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RenderStatus, useAppStore } from '@/store'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ListFilesFromFolderRecord } from '~/electron/types'
|
||||
@@ -42,6 +43,7 @@ import random from 'random'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 渲染合成视频
|
||||
const TextGenerateInstance = ref<InstanceType<typeof TextGenerate> | null>()
|
||||
@@ -49,15 +51,15 @@ const VideoManageInstance = ref<InstanceType<typeof VideoManage> | null>()
|
||||
const TtsControlInstance = ref<InstanceType<typeof TtsControl> | null>()
|
||||
const handleRenderVideo = async () => {
|
||||
if (!appStore.renderConfig.outputFileName) {
|
||||
toast.warning('请先配置导出文件名')
|
||||
toast.warning(t('errors.outputFileNameRequired'))
|
||||
return
|
||||
}
|
||||
if (!appStore.renderConfig.outputPath) {
|
||||
toast.warning('请先配置导出文件夹')
|
||||
toast.warning(t('errors.outputPathRequired'))
|
||||
return
|
||||
}
|
||||
if (!appStore.renderConfig.outputSize?.width || !appStore.renderConfig.outputSize?.height) {
|
||||
toast.warning('请先配置导出分辨率(宽高)')
|
||||
toast.warning(t('errors.outputSizeRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,7 +73,7 @@ const handleRenderVideo = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('获取背景音乐列表失败', error)
|
||||
toast.error('获取背景音乐列表失败,请检查文件夹是否存在')
|
||||
toast.error(t('errors.bgmListFailed'))
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -92,10 +94,10 @@ const handleRenderVideo = async () => {
|
||||
withCaption: true,
|
||||
})
|
||||
if (ttsResult?.duration === undefined) {
|
||||
throw new Error('语音合成失败,音频文件损坏')
|
||||
throw new Error(t('errors.ttsFailedCorrupt'))
|
||||
}
|
||||
if (ttsResult?.duration === 0) {
|
||||
throw new Error('语音时长为0秒,检查TTS语音合成配置及网络连接是否正常')
|
||||
throw new Error(t('errors.ttsZeroDuration'))
|
||||
}
|
||||
|
||||
// 获取视频片段
|
||||
@@ -132,11 +134,11 @@ const handleRenderVideo = async () => {
|
||||
appStore.renderConfig.outputFileExt,
|
||||
})
|
||||
|
||||
toast.success('视频合成成功')
|
||||
toast.success(t('success.renderSuccess'))
|
||||
appStore.updateRenderStatus(RenderStatus.Completed)
|
||||
|
||||
if (appStore.autoBatch) {
|
||||
toast.info('开始合成下一个')
|
||||
toast.info(t('info.batchNext'))
|
||||
TextGenerateInstance.value?.clearOutputText()
|
||||
handleRenderVideo()
|
||||
}
|
||||
@@ -146,9 +148,7 @@ const handleRenderVideo = async () => {
|
||||
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message || error?.error?.message
|
||||
toast.error(
|
||||
`视频合成失败,请检查各项配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
||||
)
|
||||
toast.error(`${t('errors.renderFailedPrefix')}${errorMessage ? '\n' + errorMessage : ''}`)
|
||||
appStore.updateRenderStatus(RenderStatus.Failed)
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ const handleCancelRender = () => {
|
||||
break
|
||||
}
|
||||
appStore.updateRenderStatus(RenderStatus.None)
|
||||
toast.info('视频合成已终止')
|
||||
toast.info(t('info.renderCanceled'))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user