重构i18n,支持全局多语言切换

This commit is contained in:
YILS
2025-08-23 17:35:20 +08:00
parent 6c2733b8af
commit 8ea9d06efa
28 changed files with 656 additions and 351 deletions

View File

@@ -1,6 +1,12 @@
# Changelog
此项目的所有显著更改都将记录在此文件中。
## [v1.1.0] - 2025-08-22
### Added
- Multi-Language Support
### 添加
- 多语言支持
## [v1.0.1] - 2025-08-12
### Fixed
- 修复混剪片段与语音时长不一致问题

View File

@@ -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
- [ ] 字幕特效,支持多种字幕样式和特效

View File

@@ -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: {

View File

@@ -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

View 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
View 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)
})
}

View File

@@ -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
View File

@@ -0,0 +1 @@
export const isDev = !!process.env['VITE_DEV_SERVER_URL']

View File

@@ -2,8 +2,6 @@ import fs from 'node:fs'
import path from 'node:path'
import { app } from 'electron'
// import packageJson from '~/package.json'
/**
* 生成有序的唯一文件名,用于处理文件已存在的情况
*/

View File

@@ -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()
})

View File

@@ -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
View 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
View 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"
}
}

View File

@@ -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
View File

@@ -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

View File

@@ -1,3 +0,0 @@
export default {
appName: 'AI Short Video Factory - 短视频工厂',
}

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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)
})
})
})

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>()

View File

@@ -24,7 +24,7 @@ export default defineConfig({
position: 'absolute',
top: '0',
right: '0',
width: '120px',
width: '168px',
height: '35px',
'-webkit-app-region': 'no-drag',
},

View File

@@ -42,6 +42,7 @@ export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'~': fileURLToPath(new URL('./', import.meta.url)),
},
},
build: {