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 { fileURLToPath } from 'node:url'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import GlobalSetting from '../setting.global'
|
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除外。在那里,这很常见
|
//关闭所有窗口后退出,macOS除外。在那里,这很常见
|
||||||
//让应用程序及其菜单栏保持活动状态,直到用户退出
|
//让应用程序及其菜单栏保持活动状态,直到用户退出
|
||||||
//显式使用Cmd+Q。
|
//显式使用Cmd+Q。
|
||||||
@@ -101,4 +191,7 @@ app.whenReady().then(() => {
|
|||||||
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
|
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
|
||||||
// 允许本地网络请求
|
// 允许本地网络请求
|
||||||
app.commandLine.appendSwitch('disable-features', 'BlockInsecurePrivateNetworkRequests')
|
app.commandLine.appendSwitch('disable-features', 'BlockInsecurePrivateNetworkRequests')
|
||||||
|
|
||||||
|
// Build application menu
|
||||||
|
buildMenu()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
"music-metadata": "^11.7.3",
|
"music-metadata": "^11.7.3",
|
||||||
"subtitle": "4.2.2-alpha.0",
|
"subtitle": "4.2.2-alpha.0",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3",
|
||||||
|
"vue-i18n": "^9.14.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ai-sdk/openai": "^1.3.23",
|
"@ai-sdk/openai": "^1.3.23",
|
||||||
|
|||||||
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ importers:
|
|||||||
subtitle:
|
subtitle:
|
||||||
specifier: 4.2.2-alpha.0
|
specifier: 4.2.2-alpha.0
|
||||||
version: 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:
|
ws:
|
||||||
specifier: ^8.18.3
|
specifier: ^8.18.3
|
||||||
version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
version: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||||
@@ -497,6 +500,18 @@ packages:
|
|||||||
'@iconify/utils@2.3.0':
|
'@iconify/utils@2.3.0':
|
||||||
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
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':
|
'@isaacs/balanced-match@4.0.1':
|
||||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -2754,6 +2769,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.4.37
|
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:
|
vue-router@4.5.1:
|
||||||
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
|
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3262,6 +3283,18 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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/balanced-match@4.0.1': {}
|
||||||
|
|
||||||
'@isaacs/brace-expansion@5.0.0':
|
'@isaacs/brace-expansion@5.0.0':
|
||||||
@@ -5712,6 +5745,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.17(typescript@5.6.2)
|
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)):
|
vue-router@4.5.1(vue@3.5.17(typescript@5.6.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 6.6.4
|
'@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="layout-container">
|
||||||
<div class="logo" v-if="!route.meta.hideAppIcon">
|
<div class="logo" v-if="!route.meta.hideAppIcon">
|
||||||
<img src="/icon.png" alt="" />
|
<img src="/icon.png" alt="" />
|
||||||
<span>{{ GlobalSetting.appName }}</span>
|
<span>{{ t('app.name') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-control-bar">
|
<div class="window-control-bar">
|
||||||
<div class="control-btn control-btn-min" @click="handleMin">
|
<div class="control-btn control-btn-min" @click="handleMin">
|
||||||
@@ -21,12 +21,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import GlobalSetting from '../../setting.global'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const windowIsMaxed = ref(false)
|
const windowIsMaxed = ref(false)
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
window.addEventListener('resize', async () => {
|
window.addEventListener('resize', async () => {
|
||||||
windowIsMaxed.value = await window.electron.isWinMaxed()
|
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 './assets/base.scss'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import GlobalSetting from '../setting.global'
|
import i18n from './i18n'
|
||||||
import router from './router/index.ts'
|
import router from './router/index.ts'
|
||||||
import store from './store/index.ts'
|
import store from './store/index.ts'
|
||||||
import App from './App.vue'
|
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)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(vuetify)
|
app.use(vuetify)
|
||||||
app.use(Toast, { position: 'bottom-left', pauseOnFocusLoss: false } as PluginOptions)
|
app.use(Toast, { position: 'bottom-left', pauseOnFocusLoss: false } as PluginOptions)
|
||||||
|
app.use(i18n)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(store)
|
app.use(store)
|
||||||
|
|
||||||
@@ -43,4 +45,14 @@ app.mount('#app').$nextTick(() => {
|
|||||||
window.ipcRenderer.on('main-process-message', (_event, message) => {
|
window.ipcRenderer.on('main-process-message', (_event, message) => {
|
||||||
console.log(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
|
<v-textarea
|
||||||
class="h-full"
|
class="h-full"
|
||||||
v-model="appStore.prompt"
|
v-model="appStore.prompt"
|
||||||
label="提示词"
|
:label="t('prompt.label')"
|
||||||
counter
|
counter
|
||||||
persistent-counter
|
persistent-counter
|
||||||
no-resize
|
no-resize
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="handleGenerate"
|
@click="handleGenerate"
|
||||||
>
|
>
|
||||||
生成
|
{{ t('actions.generate') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-else
|
v-else
|
||||||
@@ -29,51 +29,51 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="handleStopGenerate"
|
@click="handleStopGenerate"
|
||||||
>
|
>
|
||||||
停止
|
{{ t('actions.stop') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
<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>
|
</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-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="模型名称"
|
:label="t('llm.modelName')"
|
||||||
v-model="config.modelName"
|
v-model="config.modelName"
|
||||||
required
|
required
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="API 地址"
|
:label="t('llm.apiUrl')"
|
||||||
v-model="config.apiUrl"
|
v-model="config.apiUrl"
|
||||||
required
|
required
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="API Key"
|
:label="t('llm.apiKey')"
|
||||||
v-model="config.apiKey"
|
v-model="config.apiKey"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></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-card-text>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<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
|
<v-btn
|
||||||
color="success"
|
color="success"
|
||||||
text="测试"
|
:text="t('common.test')"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
:loading="testStatus === TestStatusEnum.LOADING"
|
:loading="testStatus === TestStatusEnum.LOADING"
|
||||||
@click="handleTestConfig"
|
@click="handleTestConfig"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
text="保存"
|
:text="t('common.save')"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
@click="handleSaveConfig"
|
@click="handleSaveConfig"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
<v-textarea
|
<v-textarea
|
||||||
class="h-full"
|
class="h-full"
|
||||||
v-model="outputText"
|
v-model="outputText"
|
||||||
label="输出文案(可编辑)"
|
:label="t('output.label')"
|
||||||
counter
|
counter
|
||||||
persistent-counter
|
persistent-counter
|
||||||
no-resize
|
no-resize
|
||||||
@@ -102,9 +102,11 @@ import { nextTick, ref, toRaw } from 'vue'
|
|||||||
import { createOpenAI } from '@ai-sdk/openai'
|
import { createOpenAI } from '@ai-sdk/openai'
|
||||||
import { generateText, streamText } from 'ai'
|
import { generateText, streamText } from 'ai'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -116,8 +118,8 @@ const isGenerating = ref(false)
|
|||||||
const abortController = ref<AbortController | null>(null)
|
const abortController = ref<AbortController | null>(null)
|
||||||
const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
||||||
if (!appStore.prompt) {
|
if (!appStore.prompt) {
|
||||||
!oprions?.noToast && toast.warning('提示词不能为空')
|
!oprions?.noToast && toast.warning(t('errors.promptRequired'))
|
||||||
throw new Error('提示词不能为空')
|
throw new Error(t('errors.promptRequired') as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openai = createOpenAI({
|
const openai = createOpenAI({
|
||||||
@@ -149,9 +151,7 @@ const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const errorMessage = error?.message || error?.error?.message
|
const errorMessage = error?.message || error?.error?.message
|
||||||
!oprions?.noToast &&
|
!oprions?.noToast &&
|
||||||
toast.error(
|
toast.error(`${t('errors.generateFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`)
|
||||||
`生成失败,请检查大模型配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
|
||||||
)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -197,15 +197,13 @@ const handleTestConfig = async () => {
|
|||||||
})
|
})
|
||||||
console.log(`result`, result)
|
console.log(`result`, result)
|
||||||
testStatus.value = TestStatusEnum.SUCCESS
|
testStatus.value = TestStatusEnum.SUCCESS
|
||||||
toast.success('大模型连接成功')
|
toast.success(t('llm.connectSuccess'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
testStatus.value = TestStatusEnum.ERROR
|
testStatus.value = TestStatusEnum.ERROR
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const errorMessage = error?.message
|
const errorMessage = error?.message
|
||||||
toast.error(
|
toast.error(`${t('llm.connectFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`)
|
||||||
`大模型连接失败,请检查配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
<v-combobox
|
<v-combobox
|
||||||
v-model="appStore.language"
|
v-model="appStore.language"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="语言"
|
:label="t('tts.language')"
|
||||||
:items="appStore.languageList"
|
:items="appStore.languageList"
|
||||||
no-data-text="无数据"
|
:no-data-text="t('common.noData')"
|
||||||
@update:model-value="clearVoice"
|
@update:model-value="clearVoice"
|
||||||
></v-combobox>
|
></v-combobox>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="appStore.gender"
|
v-model="appStore.gender"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="性别"
|
:label="t('tts.gender')"
|
||||||
:items="appStore.genderList"
|
:items="genderItems"
|
||||||
item-title="label"
|
item-title="label"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
@update:model-value="clearVoice"
|
@update:model-value="clearVoice"
|
||||||
@@ -22,24 +22,24 @@
|
|||||||
<v-select
|
<v-select
|
||||||
v-model="appStore.voice"
|
v-model="appStore.voice"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="声音"
|
:label="t('tts.voice')"
|
||||||
:items="filteredVoicesList"
|
:items="filteredVoicesList"
|
||||||
item-title="FriendlyName"
|
item-title="FriendlyName"
|
||||||
return-object
|
return-object
|
||||||
no-data-text="请先选择语言和性别"
|
:no-data-text="t('tts.selectLanguageGenderFirst')"
|
||||||
></v-select>
|
></v-select>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="appStore.speed"
|
v-model="appStore.speed"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="语速"
|
:label="t('tts.speed')"
|
||||||
:items="appStore.speedList"
|
:items="speedItems"
|
||||||
item-title="label"
|
item-title="label"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
></v-select>
|
></v-select>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="appStore.tryListeningText"
|
v-model="appStore.tryListeningText"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="试听文本"
|
:label="t('tts.tryText')"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="handleTryListening"
|
@click="handleTryListening"
|
||||||
>
|
>
|
||||||
试听
|
{{ t('tts.tryListen') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</v-form>
|
</v-form>
|
||||||
@@ -58,11 +58,13 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -70,12 +72,12 @@ defineProps<{
|
|||||||
|
|
||||||
const configValid = () => {
|
const configValid = () => {
|
||||||
if (!appStore.voice) {
|
if (!appStore.voice) {
|
||||||
toast.warning('请选择一个声音')
|
toast.warning(t('tts.selectVoiceWarning'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appStore.tryListeningText) {
|
if (!appStore.tryListeningText) {
|
||||||
toast.warning('试听文本不能为空')
|
toast.warning(t('tts.tryTextEmptyWarning'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +99,10 @@ const handleTryListening = async () => {
|
|||||||
})
|
})
|
||||||
const audio = new Audio(`data:audio/mp3;base64,${speech}`)
|
const audio = new Audio(`data:audio/mp3;base64,${speech}`)
|
||||||
audio.play()
|
audio.play()
|
||||||
toast.info('播放试听语音')
|
toast.info(t('tts.playTryAudio'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('试听语音合成失败', error)
|
console.log('试听语音合成失败', error)
|
||||||
toast.error('试听语音合成失败,请检查网络')
|
toast.error(t('tts.trySynthesisFailedNetwork'))
|
||||||
} finally {
|
} finally {
|
||||||
tryListeningLoading.value = false
|
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 () => {
|
const fetchVoices = async () => {
|
||||||
try {
|
try {
|
||||||
appStore.originalVoicesList = await window.electron.edgeTtsGetVoiceList()
|
appStore.originalVoicesList = await window.electron.edgeTtsGetVoiceList()
|
||||||
console.log('EdgeTTS语音列表更新:', appStore.originalVoicesList)
|
console.log('EdgeTTS语音列表更新:', appStore.originalVoicesList)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('获取EdgeTTS语音列表失败', error)
|
console.log('获取EdgeTTS语音列表失败', error)
|
||||||
toast.error('获取EdgeTTS语音列表失败,请检查网络')
|
toast.error(t('errors.edgeTtsListFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -133,7 +150,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boolean }) => {
|
const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boolean }) => {
|
||||||
if (!configValid()) throw new Error('TTS语音合成配置无效')
|
if (!configValid()) throw new Error(t('errors.ttsConfigInvalid'))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electron.edgeTtsSynthesizeToFile({
|
const result = await window.electron.edgeTtsSynthesizeToFile({
|
||||||
@@ -147,7 +164,7 @@ const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boo
|
|||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('语音合成失败', error)
|
console.log('语音合成失败', error)
|
||||||
throw error
|
throw new Error(t('errors.ttsSynthesisFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex gap-2 mb-2">
|
<div class="flex gap-2 mb-2">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="appStore.videoAssetsFolder"
|
v-model="appStore.videoAssetsFolder"
|
||||||
label="分镜视频素材文件夹"
|
:label="t('videoManage.assetsFolderLabel')"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
readonly
|
readonly
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="handleSelectFolder"
|
@click="handleSelectFolder"
|
||||||
>
|
>
|
||||||
选择
|
{{ t('common.select') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<v-empty-state
|
<v-empty-state
|
||||||
v-else
|
v-else
|
||||||
headline="暂无内容"
|
:headline="t('empty.noContent')"
|
||||||
text="从上面选择一个包含足够分镜素材的文件夹"
|
:text="t('empty.hintSelectFolder')"
|
||||||
></v-empty-state>
|
></v-empty-state>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
:loading="refreshAssetsLoading"
|
:loading="refreshAssetsLoading"
|
||||||
@click="refreshAssets"
|
@click="refreshAssets"
|
||||||
>
|
>
|
||||||
刷新素材库
|
{{ t('actions.refreshAssets') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRaw } from 'vue'
|
import { ref, toRaw } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { ListFilesFromFolderRecord } from '~/electron/types'
|
import { ListFilesFromFolderRecord } from '~/electron/types'
|
||||||
@@ -75,6 +76,7 @@ import random from 'random'
|
|||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -83,7 +85,7 @@ defineProps<{
|
|||||||
// 选择文件夹
|
// 选择文件夹
|
||||||
const handleSelectFolder = async () => {
|
const handleSelectFolder = async () => {
|
||||||
const folderPath = await window.electron.selectFolder({
|
const folderPath = await window.electron.selectFolder({
|
||||||
title: '选择分镜素材文件夹',
|
title: t('dialogs.selectAssetsFolderTitle'),
|
||||||
defaultPath: appStore.videoAssetsFolder,
|
defaultPath: appStore.videoAssetsFolder,
|
||||||
})
|
})
|
||||||
console.log('用户选择分镜素材文件夹,绝对路径:', folderPath)
|
console.log('用户选择分镜素材文件夹,绝对路径:', folderPath)
|
||||||
@@ -109,16 +111,16 @@ const refreshAssets = async () => {
|
|||||||
videoAssets.value = assets.filter((asset) => asset.name.endsWith('.mp4'))
|
videoAssets.value = assets.filter((asset) => asset.name.endsWith('.mp4'))
|
||||||
if (!videoAssets.value.length) {
|
if (!videoAssets.value.length) {
|
||||||
if (assets.length) {
|
if (assets.length) {
|
||||||
toast.warning('选择的文件夹中不包含MP4视频文件')
|
toast.warning(t('videoManage.noMp4InFolder'))
|
||||||
} else {
|
} else {
|
||||||
toast.warning('选择的文件夹为空')
|
toast.warning(t('videoManage.emptyFolder'))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.success('素材读取成功')
|
toast.success(t('videoManage.readSuccess'))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
toast.error('素材读取失败,请检查文件夹是否存在')
|
toast.error(t('videoManage.readFailed'))
|
||||||
} finally {
|
} finally {
|
||||||
refreshAssetsLoading.value = false
|
refreshAssetsLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -130,7 +132,7 @@ const videoInfoList = ref<VideoInfo[]>([])
|
|||||||
const getVideoSegments = (options: { duration: number }) => {
|
const getVideoSegments = (options: { duration: number }) => {
|
||||||
// 判断素材库是否满足时长要求
|
// 判断素材库是否满足时长要求
|
||||||
if (videoInfoList.value.reduce((pre, cur) => pre + cur.duration, 0) < options.duration) {
|
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>
|
<template>
|
||||||
<div class="h-0 flex-1 relative">
|
<div class="h-0 flex-1 relative">
|
||||||
<div class="absolute top-1/12 w-full flex justify-center cursor-default select-none">
|
<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">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.GenerateText" variant="elevated">
|
||||||
正在使用 AI 大模型生成文案
|
{{ t('render.status.generatingText') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SynthesizedSpeech" variant="elevated">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.SynthesizedSpeech" variant="elevated">
|
||||||
正在使用 TTS 合成语音
|
{{ t('render.status.synthesizingSpeech') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SegmentVideo" variant="elevated">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.SegmentVideo" variant="elevated">
|
||||||
正在处理分镜素材
|
{{ t('render.status.segmentingVideo') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Rendering" variant="elevated">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.Rendering" variant="elevated">
|
||||||
正在渲染视频
|
{{ t('render.status.rendering') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip
|
<v-chip
|
||||||
v-if="appStore.renderStatus === RenderStatus.Completed"
|
v-if="appStore.renderStatus === RenderStatus.Completed"
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
color="success"
|
color="success"
|
||||||
>
|
>
|
||||||
渲染成功,可以开始下一个
|
{{ t('render.status.success') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Failed" variant="elevated" color="error">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.Failed" variant="elevated" color="error">
|
||||||
渲染失败,请重新尝试
|
{{ t('render.status.failed') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
prepend-icon="mdi-rocket-launch"
|
prepend-icon="mdi-rocket-launch"
|
||||||
@click="emit('renderVideo')"
|
@click="emit('renderVideo')"
|
||||||
>
|
>
|
||||||
开始合成
|
{{ t('render.startRender') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-else
|
v-else
|
||||||
@@ -55,31 +55,31 @@
|
|||||||
prepend-icon="mdi-stop"
|
prepend-icon="mdi-stop"
|
||||||
@click="emit('cancelRender')"
|
@click="emit('cancelRender')"
|
||||||
>
|
>
|
||||||
停止合成
|
{{ t('render.stopRender') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
<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>
|
</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>
|
<v-card-text>
|
||||||
<div class="w-full flex gap-2 mb-4 items-center">
|
<div class="w-full flex gap-2 mb-4 items-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="导出视频宽度"
|
:label="t('render.output.width')"
|
||||||
v-model="config.outputSize.width"
|
v-model="config.outputSize.width"
|
||||||
hide-details
|
hide-details
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="config.outputSize.height"
|
v-model="config.outputSize.height"
|
||||||
label="导出视频高度"
|
:label="t('render.output.height')"
|
||||||
hide-details
|
hide-details
|
||||||
required
|
required
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex gap-2 mb-4 items-center">
|
<div class="w-full flex gap-2 mb-4 items-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="导出文件名"
|
:label="t('render.output.fileName')"
|
||||||
v-model="config.outputFileName"
|
v-model="config.outputFileName"
|
||||||
hide-details
|
hide-details
|
||||||
required
|
required
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
<v-text-field
|
<v-text-field
|
||||||
class="w-[120px] flex-none"
|
class="w-[120px] flex-none"
|
||||||
v-model="config.outputFileExt"
|
v-model="config.outputFileExt"
|
||||||
label="导出格式"
|
:label="t('render.output.format')"
|
||||||
hide-details
|
hide-details
|
||||||
readonly
|
readonly
|
||||||
required
|
required
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full flex gap-2 mb-4 items-center">
|
<div class="w-full flex gap-2 mb-4 items-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="导出文件夹"
|
:label="t('render.output.folder')"
|
||||||
v-model="config.outputPath"
|
v-model="config.outputPath"
|
||||||
hide-details
|
hide-details
|
||||||
readonly
|
readonly
|
||||||
@@ -107,12 +107,12 @@
|
|||||||
prepend-icon="mdi-folder-open"
|
prepend-icon="mdi-folder-open"
|
||||||
@click="handleSelectOutputFolder"
|
@click="handleSelectOutputFolder"
|
||||||
>
|
>
|
||||||
选择
|
{{ t('common.select') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex gap-2 mb-2 items-center">
|
<div class="w-full flex gap-2 mb-2 items-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="背景音乐文件夹(.mp3格式,从中随机选取)"
|
:label="t('render.bgmFolderLabel')"
|
||||||
v-model="config.bgmPath"
|
v-model="config.bgmPath"
|
||||||
hide-details
|
hide-details
|
||||||
readonly
|
readonly
|
||||||
@@ -124,17 +124,17 @@
|
|||||||
prepend-icon="mdi-folder-open"
|
prepend-icon="mdi-folder-open"
|
||||||
@click="handleSelectBgmFolder"
|
@click="handleSelectBgmFolder"
|
||||||
>
|
>
|
||||||
选择
|
{{ t('common.select') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<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
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
text="保存"
|
:text="t('common.save')"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
@click="handleSaveConfig"
|
@click="handleSaveConfig"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="appStore.autoBatch"
|
v-model="appStore.autoBatch"
|
||||||
label="自动批量合成"
|
:label="t('render.autoBatch')"
|
||||||
color="indigo"
|
color="indigo"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
|
|
||||||
<div class="absolute bottom-2 w-full flex justify-center text-sm">
|
<div class="absolute bottom-2 w-full flex justify-center text-sm">
|
||||||
<span class="text-indigo cursor-pointer select-none" @click="handleOpenHomePage">
|
<span class="text-indigo cursor-pointer select-none" @click="handleOpenHomePage">
|
||||||
Powered by YILS(博客地址:https://yils.blog)
|
{{ t('footer.poweredBy') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,9 +166,11 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRaw, nextTick, computed } from 'vue'
|
import { ref, toRaw, nextTick, computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { RenderStatus, useAppStore } from '@/store'
|
import { RenderStatus, useAppStore } from '@/store'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'renderVideo'): void
|
(e: 'renderVideo'): void
|
||||||
@@ -206,7 +208,7 @@ const handleSaveConfig = () => {
|
|||||||
// 选择文件夹
|
// 选择文件夹
|
||||||
const handleSelectOutputFolder = async () => {
|
const handleSelectOutputFolder = async () => {
|
||||||
const folderPath = await window.electron.selectFolder({
|
const folderPath = await window.electron.selectFolder({
|
||||||
title: '选择视频导出文件夹',
|
title: t('dialogs.selectOutputFolderTitle'),
|
||||||
defaultPath: config.value.outputPath,
|
defaultPath: config.value.outputPath,
|
||||||
})
|
})
|
||||||
console.log('用户选择视频导出文件夹,绝对路径:', folderPath)
|
console.log('用户选择视频导出文件夹,绝对路径:', folderPath)
|
||||||
@@ -216,7 +218,7 @@ const handleSelectOutputFolder = async () => {
|
|||||||
}
|
}
|
||||||
const handleSelectBgmFolder = async () => {
|
const handleSelectBgmFolder = async () => {
|
||||||
const folderPath = await window.electron.selectFolder({
|
const folderPath = await window.electron.selectFolder({
|
||||||
title: '选择背景音乐文件夹',
|
title: t('dialogs.selectBgmFolderTitle'),
|
||||||
defaultPath: config.value.bgmPath,
|
defaultPath: config.value.bgmPath,
|
||||||
})
|
})
|
||||||
console.log('用户选择背景音乐文件夹,绝对路径:', folderPath)
|
console.log('用户选择背景音乐文件夹,绝对路径:', folderPath)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import TtsControl from './components/tts-control.vue'
|
|||||||
import VideoRender from './components/video-render.vue'
|
import VideoRender from './components/video-render.vue'
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { RenderStatus, useAppStore } from '@/store'
|
import { RenderStatus, useAppStore } from '@/store'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { ListFilesFromFolderRecord } from '~/electron/types'
|
import { ListFilesFromFolderRecord } from '~/electron/types'
|
||||||
@@ -42,6 +43,7 @@ import random from 'random'
|
|||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
// 渲染合成视频
|
// 渲染合成视频
|
||||||
const TextGenerateInstance = ref<InstanceType<typeof TextGenerate> | null>()
|
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 TtsControlInstance = ref<InstanceType<typeof TtsControl> | null>()
|
||||||
const handleRenderVideo = async () => {
|
const handleRenderVideo = async () => {
|
||||||
if (!appStore.renderConfig.outputFileName) {
|
if (!appStore.renderConfig.outputFileName) {
|
||||||
toast.warning('请先配置导出文件名')
|
toast.warning(t('errors.outputFileNameRequired'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!appStore.renderConfig.outputPath) {
|
if (!appStore.renderConfig.outputPath) {
|
||||||
toast.warning('请先配置导出文件夹')
|
toast.warning(t('errors.outputPathRequired'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!appStore.renderConfig.outputSize?.width || !appStore.renderConfig.outputSize?.height) {
|
if (!appStore.renderConfig.outputSize?.width || !appStore.renderConfig.outputSize?.height) {
|
||||||
toast.warning('请先配置导出分辨率(宽高)')
|
toast.warning(t('errors.outputSizeRequired'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +73,7 @@ const handleRenderVideo = async () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('获取背景音乐列表失败', error)
|
console.log('获取背景音乐列表失败', error)
|
||||||
toast.error('获取背景音乐列表失败,请检查文件夹是否存在')
|
toast.error(t('errors.bgmListFailed'))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -92,10 +94,10 @@ const handleRenderVideo = async () => {
|
|||||||
withCaption: true,
|
withCaption: true,
|
||||||
})
|
})
|
||||||
if (ttsResult?.duration === undefined) {
|
if (ttsResult?.duration === undefined) {
|
||||||
throw new Error('语音合成失败,音频文件损坏')
|
throw new Error(t('errors.ttsFailedCorrupt'))
|
||||||
}
|
}
|
||||||
if (ttsResult?.duration === 0) {
|
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,
|
appStore.renderConfig.outputFileExt,
|
||||||
})
|
})
|
||||||
|
|
||||||
toast.success('视频合成成功')
|
toast.success(t('success.renderSuccess'))
|
||||||
appStore.updateRenderStatus(RenderStatus.Completed)
|
appStore.updateRenderStatus(RenderStatus.Completed)
|
||||||
|
|
||||||
if (appStore.autoBatch) {
|
if (appStore.autoBatch) {
|
||||||
toast.info('开始合成下一个')
|
toast.info(t('info.batchNext'))
|
||||||
TextGenerateInstance.value?.clearOutputText()
|
TextGenerateInstance.value?.clearOutputText()
|
||||||
handleRenderVideo()
|
handleRenderVideo()
|
||||||
}
|
}
|
||||||
@@ -146,9 +148,7 @@ const handleRenderVideo = async () => {
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const errorMessage = error?.message || error?.error?.message
|
const errorMessage = error?.message || error?.error?.message
|
||||||
toast.error(
|
toast.error(`${t('errors.renderFailedPrefix')}${errorMessage ? '\n' + errorMessage : ''}`)
|
||||||
`视频合成失败,请检查各项配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
|
||||||
)
|
|
||||||
appStore.updateRenderStatus(RenderStatus.Failed)
|
appStore.updateRenderStatus(RenderStatus.Failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ const handleCancelRender = () => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
appStore.updateRenderStatus(RenderStatus.None)
|
appStore.updateRenderStatus(RenderStatus.None)
|
||||||
toast.info('视频合成已终止')
|
toast.info(t('info.renderCanceled'))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user