mirror of
https://github.com/YILS-LIN/short-video-factory.git
synced 2025-11-25 03:15:03 +08:00
重构i18n,支持全局多语言切换
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
# Changelog
|
||||
此项目的所有显著更改都将记录在此文件中。
|
||||
|
||||
## [v1.1.0] - 2025-08-22
|
||||
### Added
|
||||
- Multi-Language Support
|
||||
### 添加
|
||||
- 多语言支持
|
||||
|
||||
## [v1.0.1] - 2025-08-12
|
||||
### Fixed
|
||||
- 修复混剪片段与语音时长不一致问题
|
||||
|
||||
@@ -48,11 +48,13 @@
|
||||
- 🎥 **自动剪辑**:支持多种视频格式,自动化批量处理视频剪辑任务
|
||||
- 🎙️ **语音合成**:将生成的文案转换为自然流畅的语音
|
||||
- 🎬 **字幕特效**:自动添加字幕和特效,提升视频质量
|
||||
- 📦 **批量处理**:支持批量任务,按预设自动持续合成视频
|
||||
- 🌐 **多语言支持**:支持中文、英文等多种语言,满足不同用户需求
|
||||
- 📦 **开箱即用**:无需复杂配置,用户可以快速上手
|
||||
- 📈 **持续更新**:定期发布新版本,修复bug并添加新功能
|
||||
- 🔒 **安全可靠**:完全本地本地化运行,确保用户数据安全
|
||||
- 🎨 **用户友好**:简洁直观的用户界面,易于操作
|
||||
- 🌐 **多平台支持**:支持Windows、macOS和Linux等多个操作系统
|
||||
- 💻 **多平台支持**:支持Windows、macOS和Linux等多个操作系统
|
||||
|
||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||
|
||||
@@ -66,6 +68,9 @@
|
||||
- [x] 语音合成,支持EdgeTTS
|
||||
- [x] 视频剪辑,文案、视频、音频、字幕合成,自动混剪
|
||||
- [x] 批量处理,支持一个批量任务,按预设自动持续合成视频
|
||||
- [x] 多语言支持,能够支持中文、英文等多种语言
|
||||
- [ ] 更全面的参数调整
|
||||
- [ ] 更多的语音合成API
|
||||
- [ ] 字幕特效,支持多种字幕样式和特效
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
directories: {
|
||||
output: 'release/${version}',
|
||||
},
|
||||
files: ['dist', 'dist-electron', 'dist-native'],
|
||||
files: ['dist', 'dist-electron', 'dist-native', 'locales'],
|
||||
npmRebuild: false, // disable rebuild node_modules 使用包内自带预构建二进制,而不重新构建
|
||||
beforePack: './scripts/before-pack.js',
|
||||
mac: {
|
||||
|
||||
5
electron/electron-env.d.ts
vendored
5
electron/electron-env.d.ts
vendored
@@ -24,6 +24,11 @@ declare namespace NodeJS {
|
||||
// 在渲染器进程中使用,在 `preload.ts` 中暴露方法
|
||||
interface Window {
|
||||
ipcRenderer: Pick<import('electron').IpcRenderer, 'on' | 'once' | 'off' | 'send' | 'invoke'>
|
||||
i18n: {
|
||||
getLocalesPath: () => Promise<string>
|
||||
getLanguage: () => Promise<string>
|
||||
changeLanguage: (lng: string) => Promise<string>
|
||||
}
|
||||
electron: {
|
||||
isWinMaxed: () => Promise<boolean>
|
||||
winMin: () => void
|
||||
|
||||
15
electron/i18n/common-options.ts
Normal file
15
electron/i18n/common-options.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { InitOptions } from 'i18next'
|
||||
|
||||
export const i18nLanguages = [
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'zh-CN', name: '简体中文' },
|
||||
]
|
||||
|
||||
export const i18nCommonOptions: InitOptions = {
|
||||
fallbackLng: i18nLanguages[0].code,
|
||||
supportedLngs: i18nLanguages.map((l) => l.code),
|
||||
load: 'currentOnly',
|
||||
ns: ['common'],
|
||||
defaultNS: 'common',
|
||||
interpolation: { escapeValue: false },
|
||||
}
|
||||
42
electron/i18n/index.ts
Normal file
42
electron/i18n/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import i18next from 'i18next'
|
||||
import Backend from 'i18next-fs-backend'
|
||||
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import { i18nCommonOptions } from './common-options'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
process.env.APP_ROOT = path.join(__dirname, '..')
|
||||
|
||||
const localesPath = path.join(process.env.APP_ROOT, 'locales/{{lng}}/{{ns}}.json')
|
||||
|
||||
export const initI18n = async () => {
|
||||
await i18next.use(Backend).init({
|
||||
// initAsync: false,
|
||||
// debug: true,
|
||||
...i18nCommonOptions,
|
||||
lng: app.getLocale(), // 获取系统语言
|
||||
backend: {
|
||||
loadPath: localesPath,
|
||||
},
|
||||
})
|
||||
|
||||
// 获取多语言文件路径
|
||||
ipcMain.handle('i18n-getLocalesPath', () => localesPath)
|
||||
|
||||
// 读取当前语言
|
||||
ipcMain.handle('i18n-getLanguage', () => i18next.language)
|
||||
|
||||
// 渲染进程切换语言
|
||||
ipcMain.handle('i18n-changeLanguage', async (_, lng: string) => {
|
||||
await changeAppLanguage(lng)
|
||||
return lng
|
||||
})
|
||||
}
|
||||
|
||||
export const changeAppLanguage = async (lng: string) => {
|
||||
await i18next.changeLanguage(lng)
|
||||
BrowserWindow.getAllWindows().forEach((win) => {
|
||||
win.webContents.send('i18n-changeLanguage', lng)
|
||||
})
|
||||
}
|
||||
@@ -20,7 +20,7 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
||||
? path.join(process.env.APP_ROOT, 'public')
|
||||
: RENDERER_DIST
|
||||
|
||||
export default function initIPC(win: BrowserWindow) {
|
||||
export default function initIPC() {
|
||||
// sqlite 查询
|
||||
ipcMain.handle('sqlite-query', (_event, params) => sqQuery(params))
|
||||
// sqlite 插入
|
||||
@@ -33,15 +33,18 @@ export default function initIPC(win: BrowserWindow) {
|
||||
ipcMain.handle('sqlite-bulk-insert-or-update', (_event, params) => sqBulkInsertOrUpdate(params))
|
||||
|
||||
// 是否最大化
|
||||
ipcMain.handle('is-win-maxed', () => {
|
||||
ipcMain.handle('is-win-maxed', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
return win?.isMaximized()
|
||||
})
|
||||
//最小化
|
||||
ipcMain.on('win-min', () => {
|
||||
ipcMain.on('win-min', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.minimize()
|
||||
})
|
||||
//最大化
|
||||
ipcMain.on('win-max', () => {
|
||||
ipcMain.on('win-max', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (win?.isMaximized()) {
|
||||
win?.restore()
|
||||
} else {
|
||||
@@ -49,7 +52,8 @@ export default function initIPC(win: BrowserWindow) {
|
||||
}
|
||||
})
|
||||
//关闭程序
|
||||
ipcMain.on('win-close', () => {
|
||||
ipcMain.on('win-close', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.close()
|
||||
})
|
||||
|
||||
@@ -59,7 +63,12 @@ export default function initIPC(win: BrowserWindow) {
|
||||
})
|
||||
|
||||
// 选择文件夹
|
||||
ipcMain.handle('select-folder', async (_event, params?: SelectFolderParams) => {
|
||||
ipcMain.handle('select-folder', async (event, params?: SelectFolderParams) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win) {
|
||||
throw new Error('无法获取窗口')
|
||||
}
|
||||
|
||||
const result = await dialog.showOpenDialog(win, {
|
||||
properties: ['openDirectory'],
|
||||
title: params?.title || '选择文件夹',
|
||||
|
||||
1
electron/lib/is-dev.ts
Normal file
1
electron/lib/is-dev.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const isDev = !!process.env['VITE_DEV_SERVER_URL']
|
||||
@@ -2,8 +2,6 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { app } from 'electron'
|
||||
|
||||
// import packageJson from '~/package.json'
|
||||
|
||||
/**
|
||||
* 生成有序的唯一文件名,用于处理文件已存在的情况
|
||||
*/
|
||||
|
||||
103
electron/main.ts
103
electron/main.ts
@@ -1,10 +1,13 @@
|
||||
import { app, BrowserWindow, screen, Menu } from 'electron'
|
||||
import type { MenuItemConstructorOptions } from 'electron'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { isDev } from './lib/is-dev'
|
||||
import path from 'node:path'
|
||||
import GlobalSetting from '../setting.global'
|
||||
import initIPC from './ipc'
|
||||
import { initSqlite } from './sqlite'
|
||||
import i18next from 'i18next'
|
||||
import { changeAppLanguage, initI18n } from './i18n'
|
||||
import { i18nLanguages } from './i18n/common-options'
|
||||
import useCookieAllowCrossSite from './lib/cookie-allow-cross-site'
|
||||
|
||||
// 用于引入 CommonJS 模块的方法
|
||||
@@ -29,9 +32,7 @@ export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
|
||||
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
|
||||
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
|
||||
|
||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
||||
? path.join(process.env.APP_ROOT, 'public')
|
||||
: RENDERER_DIST
|
||||
process.env.VITE_PUBLIC = isDev ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST
|
||||
|
||||
let win: BrowserWindow | null
|
||||
|
||||
@@ -39,7 +40,6 @@ function createWindow() {
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||
win = new BrowserWindow({
|
||||
icon: path.join(process.env.VITE_PUBLIC, 'icon.png'),
|
||||
title: GlobalSetting.appName,
|
||||
width: Math.ceil(width * 0.8),
|
||||
height: Math.ceil(height * 0.8),
|
||||
minWidth: 800,
|
||||
@@ -76,76 +76,63 @@ function buildMenu() {
|
||||
...(process.platform === 'darwin'
|
||||
? [
|
||||
{
|
||||
label: app.name,
|
||||
label: i18next.t('app.name'),
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
{
|
||||
label: i18next.t('menu.app.about'),
|
||||
click: async () => {
|
||||
const { shell } = await import('electron')
|
||||
await shell.openExternal('https://github.com/YILS-LIN/short-video-factory')
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ label: i18next.t('menu.app.services'), role: 'services' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ label: i18next.t('menu.app.hide'), role: 'hide' },
|
||||
{ label: i18next.t('menu.app.hideOthers'), role: 'hideOthers' },
|
||||
{ label: i18next.t('menu.app.unhide'), role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' },
|
||||
{ label: i18next.t('menu.app.quit'), 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: i18next.t('menu.language'),
|
||||
submenu: i18nLanguages.map((lng) => ({
|
||||
label: lng.name,
|
||||
type: 'radio',
|
||||
checked: i18next.language === lng.code,
|
||||
click: () => {
|
||||
changeAppLanguage(lng.code)
|
||||
},
|
||||
{
|
||||
label: '中文',
|
||||
type: 'radio',
|
||||
click: () => {
|
||||
BrowserWindow.getAllWindows().forEach((w) => w.webContents.send('set-locale', 'zh-CN'))
|
||||
},
|
||||
},
|
||||
] as MenuItemConstructorOptions[],
|
||||
})) as MenuItemConstructorOptions[],
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
label: i18next.t('menu.view.root'),
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ role: 'toggleDevTools', visible: false },
|
||||
{ label: i18next.t('menu.view.resetZoom'), role: 'resetZoom' },
|
||||
{ label: i18next.t('menu.view.zoomIn'), role: 'zoomIn' },
|
||||
{ label: i18next.t('menu.view.zoomOut'), role: 'zoomOut' },
|
||||
{ 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' },
|
||||
{ label: i18next.t('menu.view.toggleFullscreen'), role: 'togglefullscreen' },
|
||||
] as MenuItemConstructorOptions[],
|
||||
},
|
||||
{
|
||||
label: i18next.t('menu.window.root'),
|
||||
role: 'window',
|
||||
submenu: [{ role: 'minimize' }, { role: 'close' }] as MenuItemConstructorOptions[],
|
||||
submenu: [
|
||||
{ label: i18next.t('menu.window.minimize'), role: 'minimize' },
|
||||
{ label: i18next.t('menu.window.close'), role: 'close' },
|
||||
] as MenuItemConstructorOptions[],
|
||||
},
|
||||
{
|
||||
label: i18next.t('menu.help.root'),
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn More',
|
||||
label: i18next.t('menu.help.learnMore'),
|
||||
click: async () => {
|
||||
const { shell } = await import('electron')
|
||||
await shell.openExternal('https://github.com/YILS-LIN/short-video-factory')
|
||||
@@ -181,9 +168,14 @@ app.on('activate', () => {
|
||||
// app.disableHardwareAcceleration();
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
initSqlite()
|
||||
initIPC(win as BrowserWindow)
|
||||
initI18n()
|
||||
initIPC()
|
||||
createWindow()
|
||||
|
||||
i18next.on('languageChanged', () => {
|
||||
buildMenu()
|
||||
})
|
||||
|
||||
// 允许跨站请求携带cookie
|
||||
useCookieAllowCrossSite()
|
||||
@@ -191,7 +183,4 @@ app.whenReady().then(() => {
|
||||
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
|
||||
// 允许本地网络请求
|
||||
app.commandLine.appendSwitch('disable-features', 'BlockInsecurePrivateNetworkRequests')
|
||||
|
||||
// Build application menu
|
||||
buildMenu()
|
||||
})
|
||||
|
||||
@@ -35,6 +35,12 @@ contextBridge.exposeInMainWorld('ipcRenderer', {
|
||||
},
|
||||
})
|
||||
|
||||
contextBridge.exposeInMainWorld('i18n', {
|
||||
getLocalesPath: () => ipcRenderer.invoke('i18n-getLocalesPath'),
|
||||
getLanguage: () => ipcRenderer.invoke('i18n-getLanguage'),
|
||||
changeLanguage: (lng: string) => ipcRenderer.invoke('i18n-changeLanguage', lng),
|
||||
})
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
isWinMaxed: () => ipcRenderer.invoke('is-win-maxed'),
|
||||
winMin: () => ipcRenderer.send('win-min'),
|
||||
|
||||
142
locales/en/common.json
Normal file
142
locales/en/common.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "AI Short Video Factory"
|
||||
},
|
||||
"menu": {
|
||||
"app": {
|
||||
"about": "About",
|
||||
"services": "Services",
|
||||
"hide": "Hide",
|
||||
"hideOthers": "Hide Others",
|
||||
"unhide": "Unhide",
|
||||
"quit": "Quit"
|
||||
},
|
||||
"language": "Language",
|
||||
"view": {
|
||||
"root": "View",
|
||||
"resetZoom": "Reset Zoom",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"toggleFullscreen": "Toggle Full Screen"
|
||||
},
|
||||
"window": {
|
||||
"root": "Window",
|
||||
"minimize": "Minimize",
|
||||
"close": "Close"
|
||||
},
|
||||
"help": {
|
||||
"root": "Help",
|
||||
"learnMore": "Learn More"
|
||||
}
|
||||
},
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
142
locales/zh-CN/common.json
Normal file
142
locales/zh-CN/common.json
Normal file
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "AI Short Video Factory - 短视频工厂"
|
||||
},
|
||||
"menu": {
|
||||
"app": {
|
||||
"about": "关于",
|
||||
"services": "服务",
|
||||
"hide": "隐藏",
|
||||
"hideOthers": "隐藏其他",
|
||||
"unhide": "取消隐藏",
|
||||
"quit": "退出"
|
||||
},
|
||||
"language": "语言",
|
||||
"view": {
|
||||
"root": "视图",
|
||||
"resetZoom": "重置缩放",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小",
|
||||
"toggleFullscreen": "切换全屏"
|
||||
},
|
||||
"window": {
|
||||
"root": "窗口",
|
||||
"minimize": "最小化",
|
||||
"close": "关闭"
|
||||
},
|
||||
"help": {
|
||||
"root": "帮助",
|
||||
"learnMore": "了解更多"
|
||||
}
|
||||
},
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "short-video-factory",
|
||||
"description": "短视频工厂,一键生成产品营销与泛内容短视频,AI批量自动剪辑",
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.0",
|
||||
"author": {
|
||||
"name": "YILS",
|
||||
"developer": "YILS",
|
||||
@@ -22,10 +22,11 @@
|
||||
"axios": "^1.11.0",
|
||||
"better-sqlite3": "9.6.0",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"i18next": "^25.4.0",
|
||||
"i18next-fs-backend": "^2.3.2",
|
||||
"music-metadata": "^11.7.3",
|
||||
"subtitle": "4.2.2-alpha.0",
|
||||
"ws": "^8.18.3",
|
||||
"vue-i18n": "^9.14.0"
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/openai": "^1.3.23",
|
||||
@@ -40,6 +41,8 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^22.3.27",
|
||||
"electron-builder": "^24.13.3",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"i18next-vue": "^5.3.0",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
|
||||
137
pnpm-lock.yaml
generated
137
pnpm-lock.yaml
generated
@@ -17,15 +17,18 @@ importers:
|
||||
ffmpeg-static:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
i18next:
|
||||
specifier: ^25.4.0
|
||||
version: 25.4.0(typescript@5.6.2)
|
||||
i18next-fs-backend:
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
music-metadata:
|
||||
specifier: ^11.7.3
|
||||
version: 11.7.3
|
||||
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)
|
||||
@@ -66,6 +69,12 @@ importers:
|
||||
electron-builder:
|
||||
specifier: ^24.13.3
|
||||
version: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
||||
i18next-http-backend:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
i18next-vue:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0(i18next@25.4.0(typescript@5.6.2))(vue@3.5.17(typescript@5.6.2))
|
||||
mitt:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
@@ -296,6 +305,10 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime@7.28.3':
|
||||
resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -500,18 +513,6 @@ 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}
|
||||
@@ -1380,6 +1381,9 @@ packages:
|
||||
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
|
||||
hasBin: true
|
||||
|
||||
cross-fetch@4.0.0:
|
||||
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1824,6 +1828,26 @@ packages:
|
||||
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
i18next-fs-backend@2.3.2:
|
||||
resolution: {integrity: sha512-LIwUlkqDZnUI8lnUxBnEj8K/FrHQTT/Sc+1rvDm9E8YvvY5YxzoEAASNx+W5M9DfD5s77lI5vSAFWeTp26B/3Q==}
|
||||
|
||||
i18next-http-backend@3.0.2:
|
||||
resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==}
|
||||
|
||||
i18next-vue@5.3.0:
|
||||
resolution: {integrity: sha512-X5gYF1R9FadUdRyIze6p+mU4+kztIRWb1SYeoegB0eFBt/lDA++i0A235enWq5qdrRpWZIHlLV8gd/D5xakOsw==}
|
||||
peerDependencies:
|
||||
i18next: '>=23'
|
||||
vue: ^3.4.38
|
||||
|
||||
i18next@25.4.0:
|
||||
resolution: {integrity: sha512-UH5aiamXsO3cfrZFurCHiB6YSs3C+s+XY9UaJllMMSbmaoXILxFgqDEZu4NbfzJFjmUo3BNMa++Rjkr3ofjfLw==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
iconv-corefoundation@1.1.7:
|
||||
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
||||
engines: {node: ^8.11.2 || >=10}
|
||||
@@ -2152,6 +2176,15 @@ packages:
|
||||
node-fetch-native@1.6.6:
|
||||
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-gyp-build@4.8.4:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
@@ -2597,6 +2630,9 @@ packages:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
truncate-utf8-bytes@1.0.2:
|
||||
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
|
||||
|
||||
@@ -2769,12 +2805,6 @@ 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:
|
||||
@@ -2815,6 +2845,12 @@ packages:
|
||||
webpack-plugin-vuetify:
|
||||
optional: true
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -3104,6 +3140,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/runtime@7.28.3': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
@@ -3283,18 +3321,6 @@ 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':
|
||||
@@ -4289,6 +4315,12 @@ snapshots:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
cross-fetch@4.0.0:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -4822,6 +4854,25 @@ snapshots:
|
||||
|
||||
human-signals@8.0.1: {}
|
||||
|
||||
i18next-fs-backend@2.3.2: {}
|
||||
|
||||
i18next-http-backend@3.0.2:
|
||||
dependencies:
|
||||
cross-fetch: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
i18next-vue@5.3.0(i18next@25.4.0(typescript@5.6.2))(vue@3.5.17(typescript@5.6.2)):
|
||||
dependencies:
|
||||
i18next: 25.4.0(typescript@5.6.2)
|
||||
vue: 3.5.17(typescript@5.6.2)
|
||||
|
||||
i18next@25.4.0(typescript@5.6.2):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.3
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
|
||||
iconv-corefoundation@1.1.7:
|
||||
dependencies:
|
||||
cli-truncate: 2.1.0
|
||||
@@ -5100,6 +5151,10 @@ snapshots:
|
||||
|
||||
node-fetch-native@1.6.6: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-gyp-build@4.8.4:
|
||||
optional: true
|
||||
|
||||
@@ -5571,6 +5626,8 @@ snapshots:
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
truncate-utf8-bytes@1.0.2:
|
||||
dependencies:
|
||||
utf8-byte-length: 1.0.5
|
||||
@@ -5745,13 +5802,6 @@ 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
|
||||
@@ -5783,6 +5833,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
appName: 'AI Short Video Factory - 短视频工厂',
|
||||
}
|
||||
200
src/i18n.ts
200
src/i18n.ts
@@ -1,200 +0,0 @@
|
||||
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
|
||||
@@ -5,6 +5,32 @@
|
||||
<span>{{ t('app.name') }}</span>
|
||||
</div>
|
||||
<div class="window-control-bar">
|
||||
<div class="window-no-drag">
|
||||
<v-menu location="bottom right">
|
||||
<template v-slot:activator="{ props }">
|
||||
<div class="control-btn control-btn-translate" v-bind="props">
|
||||
<v-icon icon="mdi-translate" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
<v-list
|
||||
class="p-2 space-y-1"
|
||||
activatable
|
||||
:activated="i18next.language"
|
||||
@update:activated="handleChangeLanguage"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, index) in i18nLanguages"
|
||||
:key="index"
|
||||
:value="item.code"
|
||||
color="primary"
|
||||
density="compact"
|
||||
rounded
|
||||
>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
<div class="control-btn control-btn-min" @click="handleMin">
|
||||
<v-icon icon="mdi-window-minimize" size="small" />
|
||||
</div>
|
||||
@@ -23,11 +49,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTranslation } from 'i18next-vue'
|
||||
import { i18nLanguages } from '~/electron/i18n/common-options'
|
||||
|
||||
const { i18next, t } = useTranslation()
|
||||
// const lang = ref(i18next.language)
|
||||
// console.log('i18next.language', i18next.language)
|
||||
|
||||
document.title = t('app.name')
|
||||
|
||||
const route = useRoute()
|
||||
const windowIsMaxed = ref(false)
|
||||
const { t } = useI18n()
|
||||
|
||||
const handleChangeLanguage = (lng: unknown) => {
|
||||
console.log('handleChangeLanguage', lng)
|
||||
if ((lng as string[])[0]) {
|
||||
window.i18n.changeLanguage((lng as string[])[0])
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', async () => {
|
||||
windowIsMaxed.value = await window.electron.isWinMaxed()
|
||||
|
||||
27
src/lib/i18n.ts
Normal file
27
src/lib/i18n.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useAppStore } from '@/store'
|
||||
import i18next from 'i18next'
|
||||
import Backend from 'i18next-http-backend'
|
||||
import { toRaw } from 'vue'
|
||||
import { i18nCommonOptions } from '~/electron/i18n/common-options'
|
||||
|
||||
const i18nInitialized = async () => {
|
||||
const appStore = useAppStore()
|
||||
if (appStore.locale) {
|
||||
await window.i18n.changeLanguage(toRaw(appStore.locale))
|
||||
} else {
|
||||
const systemLocale = await window.i18n.getLanguage()
|
||||
appStore.updateLocale(systemLocale)
|
||||
}
|
||||
return i18next.use(Backend).init({
|
||||
// debug: true,
|
||||
...i18nCommonOptions,
|
||||
lng: appStore.locale,
|
||||
backend: {
|
||||
loadPath: 'file:///' + (await window.i18n.getLocalesPath()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const i18n = i18next
|
||||
|
||||
export default i18nInitialized
|
||||
37
src/main.ts
37
src/main.ts
@@ -12,11 +12,14 @@ import 'virtual:uno.css'
|
||||
import './assets/base.scss'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import i18n from './i18n'
|
||||
import router from './router/index.ts'
|
||||
import store from './store/index.ts'
|
||||
import store, { useAppStore } from './store/index.ts'
|
||||
import App from './App.vue'
|
||||
|
||||
import i18next from 'i18next'
|
||||
import I18NextVue from 'i18next-vue'
|
||||
import i18nInitialized from './lib/i18n.ts'
|
||||
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
@@ -29,30 +32,26 @@ const vuetify = createVuetify({
|
||||
},
|
||||
})
|
||||
|
||||
// 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)
|
||||
|
||||
app.mount('#app').$nextTick(() => {
|
||||
// Use contextBridge
|
||||
window.ipcRenderer.on('main-process-message', (_event, message) => {
|
||||
console.log(message)
|
||||
})
|
||||
// 初始化并应用国际化
|
||||
i18nInitialized().then(() => {
|
||||
app.use(I18NextVue, { i18next })
|
||||
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
|
||||
// 监听主进程切换语言
|
||||
window.ipcRenderer.on('i18n-changeLanguage', (_event, lng) => {
|
||||
i18next.changeLanguage(lng)
|
||||
useAppStore().updateLocale(lng)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,12 @@ export enum RenderStatus {
|
||||
export const useAppStore = defineStore(
|
||||
'app',
|
||||
() => {
|
||||
// 国际化区域设置
|
||||
const locale = ref('')
|
||||
const updateLocale = (newLocale: string) => {
|
||||
locale.value = newLocale
|
||||
}
|
||||
|
||||
// 大模型文案生成
|
||||
const prompt = ref('')
|
||||
const llmConfig = ref({
|
||||
@@ -72,6 +78,9 @@ export const useAppStore = defineStore(
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
updateLocale,
|
||||
|
||||
prompt,
|
||||
llmConfig,
|
||||
updateLLMConfig,
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
|
||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" :disabled="disabled"> {{ t('actions.config') }} </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="t('llm.configTitle')">
|
||||
@@ -58,7 +60,9 @@
|
||||
required
|
||||
clearable
|
||||
></v-text-field>
|
||||
<small class="text-caption text-medium-emphasis">{{ t('llm.compatibleNote') }}</small>
|
||||
<small class="text-caption text-medium-emphasis">{{
|
||||
t('llm.compatibleNote')
|
||||
}}</small>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
@@ -102,11 +106,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'
|
||||
import { useTranslation } from 'i18next-vue'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
const { t } = useTranslation()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
@@ -151,7 +155,9 @@ const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message || error?.error?.message
|
||||
!oprions?.noToast &&
|
||||
toast.error(`${t('errors.generateFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`)
|
||||
toast.error(
|
||||
`${t('errors.generateFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -58,13 +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'
|
||||
import { useTranslation } from 'i18next-vue'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
const { t } = useTranslation()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTranslation } from 'i18next-vue'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ListFilesFromFolderRecord } from '~/electron/types'
|
||||
@@ -76,7 +76,7 @@ import random from 'random'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
const { t } = useTranslation()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<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"> {{ t('render.status.idle') }} </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">
|
||||
{{ t('render.status.generatingText') }}
|
||||
</v-chip>
|
||||
@@ -59,10 +61,15 @@
|
||||
</v-btn>
|
||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" :disabled="taskInProgress"> {{ t('actions.config') }} </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="t('dialogs.renderConfigTitle')">
|
||||
<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
|
||||
@@ -166,11 +173,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRaw, nextTick, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTranslation } from 'i18next-vue'
|
||||
import { RenderStatus, useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'renderVideo'): void
|
||||
|
||||
@@ -35,15 +35,15 @@ 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 { useTranslation } from 'i18next-vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ListFilesFromFolderRecord } from '~/electron/types'
|
||||
import random from 'random'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useI18n()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 渲染合成视频
|
||||
const TextGenerateInstance = ref<InstanceType<typeof TextGenerate> | null>()
|
||||
|
||||
@@ -24,7 +24,7 @@ export default defineConfig({
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
right: '0',
|
||||
width: '120px',
|
||||
width: '168px',
|
||||
height: '35px',
|
||||
'-webkit-app-region': 'no-drag',
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'~': fileURLToPath(new URL('./', import.meta.url)),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user