mirror of
https://github.com/YILS-LIN/short-video-factory.git
synced 2025-11-25 19:37:50 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb198c1b00 | ||
|
|
8ea9d06efa | ||
|
|
6c2733b8af | ||
|
|
e3939ae743 | ||
|
|
13a45a817b | ||
|
|
b66714cb0e | ||
|
|
7f600c9dbf | ||
|
|
3696f20277 | ||
|
|
9be840e88e | ||
|
|
8608c401c9 | ||
|
|
a997766543 | ||
|
|
00a21aef46 | ||
|
|
50e3c403d0 | ||
|
|
29adaa3989 | ||
|
|
31780f4703 | ||
|
|
2cead83ddc | ||
|
|
95b1aa359f | ||
|
|
b9b656da9e | ||
|
|
a328a5aea3 | ||
|
|
204468935b | ||
|
|
46c612946b | ||
|
|
8c77530f58 |
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -71,22 +71,18 @@ jobs:
|
|||||||
echo "EOF" >> $GITHUB_ENV
|
echo "EOF" >> $GITHUB_ENV
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
# Upload artifacts
|
# Publish Release
|
||||||
- name: Upload artifacts
|
- name: Publish Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
release/${env.VERSION}/*.dmg
|
release/${{ env.VERSION }}/*.dmg
|
||||||
release/${env.VERSION}/*.exe
|
release/${{ env.VERSION }}/*.exe
|
||||||
release/${env.VERSION}/*.deb
|
release/${{ env.VERSION }}/*.deb
|
||||||
release/${env.VERSION}/*.rpm
|
release/${{ env.VERSION }}/*.rpm
|
||||||
release/${env.VERSION}/*.AppImage
|
release/${{ env.VERSION }}/*.AppImage
|
||||||
release/${env.VERSION}/latest*.yml
|
|
||||||
release/${env.VERSION}/*.blockmap
|
|
||||||
body: ${{ env.NOTES }}
|
body: ${{ env.NOTES }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
overwrite_files: false
|
|
||||||
append_body: true
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"scss.validate": false,
|
|
||||||
"css.validate": false,
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.quickSuggestions": {
|
"editor.quickSuggestions": {
|
||||||
"strings": "on"
|
"strings": "on"
|
||||||
}
|
},
|
||||||
|
"css.customData": [
|
||||||
|
".vscode/unocss.json"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
11
.vscode/unocss.json
vendored
Normal file
11
.vscode/unocss.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": 1.1,
|
||||||
|
"atDirectives": [
|
||||||
|
{
|
||||||
|
"name": "@apply"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "@screen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,7 +1,24 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
此项目的所有显著更改都将记录在此文件中。
|
此项目的所有显著更改都将记录在此文件中。
|
||||||
|
|
||||||
## [v1.0.0] - 2025-08-07
|
## [v1.1.1] - 2025-08-26
|
||||||
|
### Fixed
|
||||||
|
- Fix MAC copy and paste shortcut key failure issue
|
||||||
|
### 修复
|
||||||
|
- 修复MAC复制粘贴快捷键失效问题
|
||||||
|
|
||||||
|
## [v1.1.0] - 2025-08-22
|
||||||
|
### Added
|
||||||
|
- Multi-Language Support
|
||||||
|
### 添加
|
||||||
|
- 多语言支持
|
||||||
|
|
||||||
|
## [v1.0.1] - 2025-08-12
|
||||||
|
### Fixed
|
||||||
|
- 修复混剪片段与语音时长不一致问题
|
||||||
|
- 修复渲染卡帧问题
|
||||||
|
|
||||||
|
## [v1.0.0] - 2025-08-08
|
||||||
### Added
|
### Added
|
||||||
- 发布第一个正式版本
|
- 发布第一个正式版本
|
||||||
- 支持使用大语言模型生成文案(推荐免费的GLM-4.5-Flash)
|
- 支持使用大语言模型生成文案(推荐免费的GLM-4.5-Flash)
|
||||||
@@ -11,7 +28,7 @@
|
|||||||
- 支持自动化批量任务
|
- 支持自动化批量任务
|
||||||
- 美观的UI界面
|
- 美观的UI界面
|
||||||
|
|
||||||
## [v0.7.5] - 2025-08-07
|
## [v0.7.12] - 2025-08-08
|
||||||
### Added
|
### Added
|
||||||
- 构建测试
|
- 构建测试
|
||||||
- 跨平台: macOS dmg, Windows exe, Linux AppImage.
|
- 跨平台: macOS dmg, Windows exe, Linux AppImage.
|
||||||
46
README.md
46
README.md
@@ -25,8 +25,8 @@
|
|||||||
[![分支][forks-shield]][forks-url]
|
[![分支][forks-shield]][forks-url]
|
||||||
[![星标][stars-shield]][stars-url]
|
[![星标][stars-shield]][stars-url]
|
||||||
[![问题][issues-shield]][issues-url]
|
[![问题][issues-shield]][issues-url]
|
||||||
<!-- [![最新版本][release-shield]][release-url]
|
[![最新版本][release-shield]][release-url]
|
||||||
![发布日期][release-date-shield] -->
|
<!-- ![发布日期][release-date-shield] -->
|
||||||
[![许可证][license-shield]][license-url]
|
[![许可证][license-shield]][license-url]
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -48,11 +48,13 @@
|
|||||||
- 🎥 **自动剪辑**:支持多种视频格式,自动化批量处理视频剪辑任务
|
- 🎥 **自动剪辑**:支持多种视频格式,自动化批量处理视频剪辑任务
|
||||||
- 🎙️ **语音合成**:将生成的文案转换为自然流畅的语音
|
- 🎙️ **语音合成**:将生成的文案转换为自然流畅的语音
|
||||||
- 🎬 **字幕特效**:自动添加字幕和特效,提升视频质量
|
- 🎬 **字幕特效**:自动添加字幕和特效,提升视频质量
|
||||||
|
- 📦 **批量处理**:支持批量任务,按预设自动持续合成视频
|
||||||
|
- 🌐 **多语言支持**:支持中文、英文等多种语言,满足不同用户需求
|
||||||
- 📦 **开箱即用**:无需复杂配置,用户可以快速上手
|
- 📦 **开箱即用**:无需复杂配置,用户可以快速上手
|
||||||
- 📈 **持续更新**:定期发布新版本,修复bug并添加新功能
|
- 📈 **持续更新**:定期发布新版本,修复bug并添加新功能
|
||||||
- 🔒 **安全可靠**:完全本地本地化运行,确保用户数据安全
|
- 🔒 **安全可靠**:完全本地本地化运行,确保用户数据安全
|
||||||
- 🎨 **用户友好**:简洁直观的用户界面,易于操作
|
- 🎨 **用户友好**:简洁直观的用户界面,易于操作
|
||||||
- 🌐 **多平台支持**:支持Windows、macOS和Linux等多个操作系统
|
- 💻 **多平台支持**:支持Windows、macOS和Linux等多个操作系统
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||||
|
|
||||||
@@ -66,6 +68,9 @@
|
|||||||
- [x] 语音合成,支持EdgeTTS
|
- [x] 语音合成,支持EdgeTTS
|
||||||
- [x] 视频剪辑,文案、视频、音频、字幕合成,自动混剪
|
- [x] 视频剪辑,文案、视频、音频、字幕合成,自动混剪
|
||||||
- [x] 批量处理,支持一个批量任务,按预设自动持续合成视频
|
- [x] 批量处理,支持一个批量任务,按预设自动持续合成视频
|
||||||
|
- [x] 多语言支持,能够支持中文、英文等多种语言
|
||||||
|
- [ ] 更全面的参数调整
|
||||||
|
- [ ] 更多的语音合成API
|
||||||
- [ ] 字幕特效,支持多种字幕样式和特效
|
- [ ] 字幕特效,支持多种字幕样式和特效
|
||||||
|
|
||||||
|
|
||||||
@@ -73,6 +78,27 @@
|
|||||||
|
|
||||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||||
|
|
||||||
|
## 🎞️ 示例视频
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《产品营销短视频》</th>
|
||||||
|
<th align="center"><g-emoji class="g-emoji" alias="arrow_forward">▶️</g-emoji> 《暖心治愈系语录》</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><video src="https://github.com/user-attachments/assets/165a8f96-861b-4cf3-946c-444b9692cef8"></video></td>
|
||||||
|
<td align="center"><video src="https://github.com/user-attachments/assets/12694618-e0fe-4848-8a7e-98b3f3a7aece"></video></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
注:素材来源于网络,仅用于展示剪辑效果
|
||||||
|
|
||||||
|
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||||
|
|
||||||
<!-- 贡献 -->
|
<!-- 贡献 -->
|
||||||
## 🤝 贡献
|
## 🤝 贡献
|
||||||
|
|
||||||
@@ -111,6 +137,18 @@ Copyright © 2025 YILS.
|
|||||||
|
|
||||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||||
|
|
||||||
|
## 🐱 赞赏
|
||||||
|
|
||||||
|
如果这个项目对你有帮助,欢迎请作者喝杯咖啡 🍺
|
||||||
|
|
||||||
|
你的 Star ⭐ 和赞助是我持续更新的最大动力!
|
||||||
|
|
||||||
|
> 赞助者未来也许会享有额外的特殊功能或新功能优先使用等福利
|
||||||
|
|
||||||
|
<div align="left">
|
||||||
|
<img src="https://github.com/user-attachments/assets/6b832dd3-38ea-4927-9c3b-97549c77a1f0" alt="YILS的微信赞赏码" width="400">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 星标历史 -->
|
<!-- 星标历史 -->
|
||||||
## ⭐ 星标历史
|
## ⭐ 星标历史
|
||||||
|
|
||||||
@@ -133,4 +171,4 @@ Copyright © 2025 YILS.
|
|||||||
[release-url]: https://github.com/YILS-LIN/short-video-factory/releases
|
[release-url]: https://github.com/YILS-LIN/short-video-factory/releases
|
||||||
[release-date-shield]: https://img.shields.io/github/release-date/YILS-LIN/short-video-factory?color=9cf&style=flat-round
|
[release-date-shield]: https://img.shields.io/github/release-date/YILS-LIN/short-video-factory?color=9cf&style=flat-round
|
||||||
[license-shield]: https://img.shields.io/github/license/YILS-LIN/short-video-factory.svg?labelColor=black&style=flat-square
|
[license-shield]: https://img.shields.io/github/license/YILS-LIN/short-video-factory.svg?labelColor=black&style=flat-square
|
||||||
[license-url]: https://github.com/YILS-LIN/short-video-factory/blob/master/LICENSE.txt
|
[license-url]: https://github.com/YILS-LIN/short-video-factory/blob/main/LICENSE
|
||||||
|
|||||||
@@ -7,12 +7,17 @@
|
|||||||
directories: {
|
directories: {
|
||||||
output: 'release/${version}',
|
output: 'release/${version}',
|
||||||
},
|
},
|
||||||
files: ['dist', 'dist-electron', 'dist-native'],
|
files: ['dist', 'dist-electron', 'dist-native', 'locales'],
|
||||||
npmRebuild: false, // disable rebuild node_modules 使用包内自带预构建二进制,而不重新构建
|
npmRebuild: false, // disable rebuild node_modules 使用包内自带预构建二进制,而不重新构建
|
||||||
beforePack: './scripts/before-pack.js',
|
beforePack: './scripts/before-pack.js',
|
||||||
mac: {
|
mac: {
|
||||||
target: ['dmg'],
|
target: [
|
||||||
artifactName: '${productName}-Mac-${arch}-${version}-Installer.${ext}',
|
{
|
||||||
|
target: 'dmg',
|
||||||
|
arch: ['universal'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
artifactName: '${name}-${version}-mac-${arch}-installer.${ext}',
|
||||||
icon: './public/icon.png',
|
icon: './public/icon.png',
|
||||||
},
|
},
|
||||||
win: {
|
win: {
|
||||||
@@ -21,7 +26,7 @@
|
|||||||
target: 'nsis',
|
target: 'nsis',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
artifactName: '${productName}-Windows-${arch}-${version}-Setup.${ext}',
|
artifactName: '${name}-${version}-win-${arch}-setup.${ext}',
|
||||||
icon: './public/icon.png',
|
icon: './public/icon.png',
|
||||||
},
|
},
|
||||||
nsis: {
|
nsis: {
|
||||||
@@ -33,7 +38,8 @@
|
|||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
target: ['AppImage'],
|
target: ['AppImage'],
|
||||||
artifactName: '${productName}-Linux-${arch}-${version}.${ext}',
|
artifactName: '${name}-${version}-linux-${arch}.${ext}',
|
||||||
icon: './public/icon.png',
|
icon: './public/icon.png',
|
||||||
},
|
},
|
||||||
|
publish: null, // disable auto publish 防止重复发版
|
||||||
}
|
}
|
||||||
|
|||||||
5
electron/electron-env.d.ts
vendored
5
electron/electron-env.d.ts
vendored
@@ -24,6 +24,11 @@ declare namespace NodeJS {
|
|||||||
// 在渲染器进程中使用,在 `preload.ts` 中暴露方法
|
// 在渲染器进程中使用,在 `preload.ts` 中暴露方法
|
||||||
interface Window {
|
interface Window {
|
||||||
ipcRenderer: Pick<import('electron').IpcRenderer, 'on' | 'once' | 'off' | 'send' | 'invoke'>
|
ipcRenderer: Pick<import('electron').IpcRenderer, 'on' | 'once' | 'off' | 'send' | 'invoke'>
|
||||||
|
i18n: {
|
||||||
|
getLocalesPath: () => Promise<string>
|
||||||
|
getLanguage: () => Promise<string>
|
||||||
|
changeLanguage: (lng: string) => Promise<string>
|
||||||
|
}
|
||||||
electron: {
|
electron: {
|
||||||
isWinMaxed: () => Promise<boolean>
|
isWinMaxed: () => Promise<boolean>
|
||||||
winMin: () => void
|
winMin: () => void
|
||||||
|
|||||||
@@ -86,7 +86,10 @@ export async function renderVideo(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 拼接视频
|
// 拼接视频
|
||||||
filters.push(`[${videoStreams.join('][')}]concat=n=${videoFiles.length}:v=1:a=0[vout]`)
|
filters.push(`[${videoStreams.join('][')}]concat=n=${videoFiles.length}:v=1:a=0[vconcat]`)
|
||||||
|
|
||||||
|
// 重置时间基、帧率、色彩空间
|
||||||
|
filters.push(`[vconcat]fps=30,format=yuv420p,setpts=PTS-STARTPTS[vout]`)
|
||||||
|
|
||||||
// 在视频拼接后添加字幕
|
// 在视频拼接后添加字幕
|
||||||
filters.push(`[vout]subtitles=${subtitleFile.replace(/\:/g, '\\\\:')}[with_subs]`)
|
filters.push(`[vout]subtitles=${subtitleFile.replace(/\:/g, '\\\\:')}[with_subs]`)
|
||||||
@@ -122,6 +125,8 @@ export async function renderVideo(
|
|||||||
'aac',
|
'aac',
|
||||||
'-b:a',
|
'-b:a',
|
||||||
'128k',
|
'128k',
|
||||||
|
'-fps_mode',
|
||||||
|
'cfr',
|
||||||
'-s',
|
'-s',
|
||||||
`${outputSize.width}x${outputSize.height}`,
|
`${outputSize.width}x${outputSize.height}`,
|
||||||
'-progress',
|
'-progress',
|
||||||
@@ -132,6 +137,7 @@ export async function renderVideo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 打印命令
|
// 打印命令
|
||||||
|
// console.log('传入参数:', params)
|
||||||
// console.log('执行命令:', args.join(' '))
|
// console.log('执行命令:', args.join(' '))
|
||||||
|
|
||||||
// 执行命令
|
// 执行命令
|
||||||
|
|||||||
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')
|
? path.join(process.env.APP_ROOT, 'public')
|
||||||
: RENDERER_DIST
|
: RENDERER_DIST
|
||||||
|
|
||||||
export default function initIPC(win: BrowserWindow) {
|
export default function initIPC() {
|
||||||
// sqlite 查询
|
// sqlite 查询
|
||||||
ipcMain.handle('sqlite-query', (_event, params) => sqQuery(params))
|
ipcMain.handle('sqlite-query', (_event, params) => sqQuery(params))
|
||||||
// sqlite 插入
|
// sqlite 插入
|
||||||
@@ -33,15 +33,18 @@ export default function initIPC(win: BrowserWindow) {
|
|||||||
ipcMain.handle('sqlite-bulk-insert-or-update', (_event, params) => sqBulkInsertOrUpdate(params))
|
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()
|
return win?.isMaximized()
|
||||||
})
|
})
|
||||||
//最小化
|
//最小化
|
||||||
ipcMain.on('win-min', () => {
|
ipcMain.on('win-min', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
win?.minimize()
|
win?.minimize()
|
||||||
})
|
})
|
||||||
//最大化
|
//最大化
|
||||||
ipcMain.on('win-max', () => {
|
ipcMain.on('win-max', (event) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
if (win?.isMaximized()) {
|
if (win?.isMaximized()) {
|
||||||
win?.restore()
|
win?.restore()
|
||||||
} else {
|
} 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()
|
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, {
|
const result = await dialog.showOpenDialog(win, {
|
||||||
properties: ['openDirectory'],
|
properties: ['openDirectory'],
|
||||||
title: params?.title || '选择文件夹',
|
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 path from 'node:path'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
|
|
||||||
// import packageJson from '~/package.json'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成有序的唯一文件名,用于处理文件已存在的情况
|
* 生成有序的唯一文件名,用于处理文件已存在的情况
|
||||||
*/
|
*/
|
||||||
|
|||||||
110
electron/main.ts
110
electron/main.ts
@@ -1,9 +1,13 @@
|
|||||||
import { app, BrowserWindow, screen } from 'electron'
|
import { app, BrowserWindow, screen, Menu } from 'electron'
|
||||||
|
import type { MenuItemConstructorOptions } from 'electron'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { isDev } from './lib/is-dev'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import GlobalSetting from '../setting.global'
|
|
||||||
import initIPC from './ipc'
|
import initIPC from './ipc'
|
||||||
import { initSqlite } from './sqlite'
|
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'
|
import useCookieAllowCrossSite from './lib/cookie-allow-cross-site'
|
||||||
|
|
||||||
// 用于引入 CommonJS 模块的方法
|
// 用于引入 CommonJS 模块的方法
|
||||||
@@ -28,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 MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
|
||||||
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
|
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
|
||||||
|
|
||||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
process.env.VITE_PUBLIC = isDev ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST
|
||||||
? path.join(process.env.APP_ROOT, 'public')
|
|
||||||
: RENDERER_DIST
|
|
||||||
|
|
||||||
let win: BrowserWindow | null
|
let win: BrowserWindow | null
|
||||||
|
|
||||||
@@ -38,7 +40,6 @@ function createWindow() {
|
|||||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||||
win = new BrowserWindow({
|
win = new BrowserWindow({
|
||||||
icon: path.join(process.env.VITE_PUBLIC, 'icon.png'),
|
icon: path.join(process.env.VITE_PUBLIC, 'icon.png'),
|
||||||
title: GlobalSetting.appName,
|
|
||||||
width: Math.ceil(width * 0.8),
|
width: Math.ceil(width * 0.8),
|
||||||
height: Math.ceil(height * 0.8),
|
height: Math.ceil(height * 0.8),
|
||||||
minWidth: 800,
|
minWidth: 800,
|
||||||
@@ -69,6 +70,94 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMenu() {
|
||||||
|
const template: MenuItemConstructorOptions[] = [
|
||||||
|
// macOS standard app menu
|
||||||
|
...(process.platform === 'darwin'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: i18next.t('app.name'),
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
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' },
|
||||||
|
{ label: i18next.t('menu.app.services'), role: 'services' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ 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' },
|
||||||
|
{ label: i18next.t('menu.app.quit'), role: 'quit' },
|
||||||
|
] as MenuItemConstructorOptions[],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
label: i18next.t('menu.language'),
|
||||||
|
submenu: i18nLanguages.map((lng) => ({
|
||||||
|
label: lng.name,
|
||||||
|
type: 'radio',
|
||||||
|
checked: i18next.language === lng.code,
|
||||||
|
click: () => {
|
||||||
|
changeAppLanguage(lng.code)
|
||||||
|
},
|
||||||
|
})) as MenuItemConstructorOptions[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18next.t('menu.edit.root'),
|
||||||
|
submenu: [
|
||||||
|
{ label: i18next.t('menu.edit.undo'), role: 'undo' },
|
||||||
|
{ label: i18next.t('menu.edit.redo'), role: 'redo' },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: i18next.t('menu.edit.cut'), role: 'cut' },
|
||||||
|
{ label: i18next.t('menu.edit.copy'), role: 'copy' },
|
||||||
|
{ label: i18next.t('menu.edit.paste'), role: 'paste' },
|
||||||
|
{ label: i18next.t('menu.edit.selectAll'), role: 'selectAll' },
|
||||||
|
] as MenuItemConstructorOptions[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18next.t('menu.view.root'),
|
||||||
|
submenu: [
|
||||||
|
{ 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' },
|
||||||
|
{ label: i18next.t('menu.view.toggleFullscreen'), role: 'togglefullscreen' },
|
||||||
|
] as MenuItemConstructorOptions[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: i18next.t('menu.window.root'),
|
||||||
|
role: 'window',
|
||||||
|
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: i18next.t('menu.help.learnMore'),
|
||||||
|
click: async () => {
|
||||||
|
const { shell } = await import('electron')
|
||||||
|
await shell.openExternal('https://github.com/YILS-LIN/short-video-factory')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as MenuItemConstructorOptions[],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate(template)
|
||||||
|
Menu.setApplicationMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
//关闭所有窗口后退出,macOS除外。在那里,这很常见
|
//关闭所有窗口后退出,macOS除外。在那里,这很常见
|
||||||
//让应用程序及其菜单栏保持活动状态,直到用户退出
|
//让应用程序及其菜单栏保持活动状态,直到用户退出
|
||||||
//显式使用Cmd+Q。
|
//显式使用Cmd+Q。
|
||||||
@@ -91,9 +180,14 @@ app.on('activate', () => {
|
|||||||
// app.disableHardwareAcceleration();
|
// app.disableHardwareAcceleration();
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow()
|
|
||||||
initSqlite()
|
initSqlite()
|
||||||
initIPC(win as BrowserWindow)
|
initI18n()
|
||||||
|
initIPC()
|
||||||
|
createWindow()
|
||||||
|
|
||||||
|
i18next.on('languageChanged', () => {
|
||||||
|
buildMenu()
|
||||||
|
})
|
||||||
|
|
||||||
// 允许跨站请求携带cookie
|
// 允许跨站请求携带cookie
|
||||||
useCookieAllowCrossSite()
|
useCookieAllowCrossSite()
|
||||||
|
|||||||
@@ -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', {
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
isWinMaxed: () => ipcRenderer.invoke('is-win-maxed'),
|
isWinMaxed: () => ipcRenderer.invoke('is-win-maxed'),
|
||||||
winMin: () => ipcRenderer.send('win-min'),
|
winMin: () => ipcRenderer.send('win-min'),
|
||||||
|
|||||||
151
locales/en/common.json
Normal file
151
locales/en/common.json
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "AI Short Video Factory"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"app": {
|
||||||
|
"about": "About",
|
||||||
|
"services": "Services",
|
||||||
|
"hide": "Hide",
|
||||||
|
"hideOthers": "Hide Others",
|
||||||
|
"unhide": "Unhide",
|
||||||
|
"quit": "Quit"
|
||||||
|
},
|
||||||
|
"language": "Language",
|
||||||
|
"edit": {
|
||||||
|
"root": "Edit",
|
||||||
|
"undo": "Undo",
|
||||||
|
"redo": "Redo",
|
||||||
|
"cut": "Cut",
|
||||||
|
"copy": "Copy",
|
||||||
|
"paste": "Paste",
|
||||||
|
"selectAll": "Select All"
|
||||||
|
},
|
||||||
|
"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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
151
locales/zh-CN/common.json
Normal file
151
locales/zh-CN/common.json
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "AI Short Video Factory - 短视频工厂"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"app": {
|
||||||
|
"about": "关于",
|
||||||
|
"services": "服务",
|
||||||
|
"hide": "隐藏",
|
||||||
|
"hideOthers": "隐藏其他",
|
||||||
|
"unhide": "取消隐藏",
|
||||||
|
"quit": "退出"
|
||||||
|
},
|
||||||
|
"language": "语言",
|
||||||
|
"edit": {
|
||||||
|
"root": "编辑",
|
||||||
|
"undo": "撤销",
|
||||||
|
"redo": "重做",
|
||||||
|
"cut": "剪切",
|
||||||
|
"copy": "复制",
|
||||||
|
"paste": "粘贴",
|
||||||
|
"selectAll": "全选"
|
||||||
|
},
|
||||||
|
"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,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "short-video-factory",
|
"name": "short-video-factory",
|
||||||
"description": "短视频工厂,一键生成产品营销与泛内容短视频,AI批量自动剪辑",
|
"description": "短视频工厂,一键生成产品营销与泛内容短视频,AI批量自动剪辑",
|
||||||
"version": "0.7.5",
|
"version": "1.1.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "YILS",
|
"name": "YILS",
|
||||||
"developer": "YILS",
|
"developer": "YILS",
|
||||||
"email": "yils_lin@163.com",
|
"email": "yils_lin@163.com",
|
||||||
"url": "https://yils.blog/"
|
"url": "https://yils.blog/"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/YILS-LIN/short-video-factory",
|
"homepage": "https://github.com/YILS-LIN/short-video-factory",
|
||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env VITE_CJS_IGNORE_WARNING=true vite",
|
"dev": "cross-env VITE_CJS_IGNORE_WARNING=true vite",
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"better-sqlite3": "9.6.0",
|
"better-sqlite3": "9.6.0",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
|
"i18next": "^25.4.0",
|
||||||
|
"i18next-fs-backend": "^2.3.2",
|
||||||
"music-metadata": "^11.7.3",
|
"music-metadata": "^11.7.3",
|
||||||
"subtitle": "4.2.2-alpha.0",
|
"subtitle": "4.2.2-alpha.0",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3"
|
||||||
@@ -39,6 +41,8 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"electron": "^22.3.27",
|
"electron": "^22.3.27",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
|
"i18next-http-backend": "^3.0.2",
|
||||||
|
"i18next-vue": "^5.3.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.4.1",
|
||||||
|
|||||||
97
pnpm-lock.yaml
generated
97
pnpm-lock.yaml
generated
@@ -17,6 +17,12 @@ importers:
|
|||||||
ffmpeg-static:
|
ffmpeg-static:
|
||||||
specifier: ^5.2.0
|
specifier: ^5.2.0
|
||||||
version: 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:
|
music-metadata:
|
||||||
specifier: ^11.7.3
|
specifier: ^11.7.3
|
||||||
version: 11.7.3
|
version: 11.7.3
|
||||||
@@ -63,6 +69,12 @@ importers:
|
|||||||
electron-builder:
|
electron-builder:
|
||||||
specifier: ^24.13.3
|
specifier: ^24.13.3
|
||||||
version: 24.13.3(electron-builder-squirrel-windows@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:
|
mitt:
|
||||||
specifier: ^3.0.1
|
specifier: ^3.0.1
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
@@ -293,6 +305,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@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':
|
'@babel/template@7.27.2':
|
||||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -1365,6 +1381,9 @@ packages:
|
|||||||
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
|
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
cross-fetch@4.0.0:
|
||||||
|
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -1809,6 +1828,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
|
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
|
||||||
engines: {node: '>=18.18.0'}
|
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:
|
iconv-corefoundation@1.1.7:
|
||||||
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
||||||
engines: {node: ^8.11.2 || >=10}
|
engines: {node: ^8.11.2 || >=10}
|
||||||
@@ -2137,6 +2176,15 @@ packages:
|
|||||||
node-fetch-native@1.6.6:
|
node-fetch-native@1.6.6:
|
||||||
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
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:
|
node-gyp-build@4.8.4:
|
||||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -2582,6 +2630,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tr46@0.0.3:
|
||||||
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
truncate-utf8-bytes@1.0.2:
|
truncate-utf8-bytes@1.0.2:
|
||||||
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
|
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
|
||||||
|
|
||||||
@@ -2794,6 +2845,12 @@ packages:
|
|||||||
webpack-plugin-vuetify:
|
webpack-plugin-vuetify:
|
||||||
optional: true
|
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:
|
which@2.0.2:
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -3083,6 +3140,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@babel/runtime@7.28.3': {}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
@@ -4256,6 +4315,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
|
|
||||||
|
cross-fetch@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -4789,6 +4854,25 @@ snapshots:
|
|||||||
|
|
||||||
human-signals@8.0.1: {}
|
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:
|
iconv-corefoundation@1.1.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
cli-truncate: 2.1.0
|
cli-truncate: 2.1.0
|
||||||
@@ -5067,6 +5151,10 @@ snapshots:
|
|||||||
|
|
||||||
node-fetch-native@1.6.6: {}
|
node-fetch-native@1.6.6: {}
|
||||||
|
|
||||||
|
node-fetch@2.7.0:
|
||||||
|
dependencies:
|
||||||
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
node-gyp-build@4.8.4:
|
node-gyp-build@4.8.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -5538,6 +5626,8 @@ snapshots:
|
|||||||
|
|
||||||
totalist@3.0.1: {}
|
totalist@3.0.1: {}
|
||||||
|
|
||||||
|
tr46@0.0.3: {}
|
||||||
|
|
||||||
truncate-utf8-bytes@1.0.2:
|
truncate-utf8-bytes@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
utf8-byte-length: 1.0.5
|
utf8-byte-length: 1.0.5
|
||||||
@@ -5743,6 +5833,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.2
|
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:
|
which@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export default {
|
|
||||||
appName: 'AI Short Video Factory - 短视频工厂',
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,35 @@
|
|||||||
<div class="layout-container">
|
<div class="layout-container">
|
||||||
<div class="logo" v-if="!route.meta.hideAppIcon">
|
<div class="logo" v-if="!route.meta.hideAppIcon">
|
||||||
<img src="/icon.png" alt="" />
|
<img src="/icon.png" alt="" />
|
||||||
<span>{{ GlobalSetting.appName }}</span>
|
<span>{{ t('app.name') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="window-control-bar">
|
<div class="window-control-bar">
|
||||||
|
<div class="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">
|
<div class="control-btn control-btn-min" @click="handleMin">
|
||||||
<v-icon icon="mdi-window-minimize" size="small" />
|
<v-icon icon="mdi-window-minimize" size="small" />
|
||||||
</div>
|
</div>
|
||||||
@@ -21,13 +47,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import GlobalSetting from '../../setting.global'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { 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 route = useRoute()
|
||||||
const windowIsMaxed = ref(false)
|
const windowIsMaxed = ref(false)
|
||||||
|
|
||||||
|
const handleChangeLanguage = (lng: unknown) => {
|
||||||
|
console.log('handleChangeLanguage', lng)
|
||||||
|
if ((lng as string[])[0]) {
|
||||||
|
window.i18n.changeLanguage((lng as string[])[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', async () => {
|
window.addEventListener('resize', async () => {
|
||||||
windowIsMaxed.value = await window.electron.isWinMaxed()
|
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
|
||||||
27
src/main.ts
27
src/main.ts
@@ -12,11 +12,14 @@ import 'virtual:uno.css'
|
|||||||
import './assets/base.scss'
|
import './assets/base.scss'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import GlobalSetting from '../setting.global'
|
|
||||||
import router from './router/index.ts'
|
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 App from './App.vue'
|
||||||
|
|
||||||
|
import i18next from 'i18next'
|
||||||
|
import I18NextVue from 'i18next-vue'
|
||||||
|
import i18nInitialized from './lib/i18n.ts'
|
||||||
|
|
||||||
const vuetify = createVuetify({
|
const vuetify = createVuetify({
|
||||||
components,
|
components,
|
||||||
directives,
|
directives,
|
||||||
@@ -29,8 +32,6 @@ const vuetify = createVuetify({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
document.title = GlobalSetting.appName
|
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(vuetify)
|
app.use(vuetify)
|
||||||
@@ -38,9 +39,19 @@ app.use(Toast, { position: 'bottom-left', pauseOnFocusLoss: false } as PluginOpt
|
|||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(store)
|
app.use(store)
|
||||||
|
|
||||||
app.mount('#app').$nextTick(() => {
|
// 初始化并应用国际化
|
||||||
// Use contextBridge
|
i18nInitialized().then(() => {
|
||||||
window.ipcRenderer.on('main-process-message', (_event, message) => {
|
app.use(I18NextVue, { i18next })
|
||||||
console.log(message)
|
app.mount('#app').$nextTick(() => {
|
||||||
|
// 测试消息
|
||||||
|
window.ipcRenderer.on('main-process-message', (_event, message) => {
|
||||||
|
console.log(message)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听主进程切换语言
|
||||||
|
window.ipcRenderer.on('i18n-changeLanguage', (_event, lng) => {
|
||||||
|
i18next.changeLanguage(lng)
|
||||||
|
useAppStore().updateLocale(lng)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ export enum RenderStatus {
|
|||||||
export const useAppStore = defineStore(
|
export const useAppStore = defineStore(
|
||||||
'app',
|
'app',
|
||||||
() => {
|
() => {
|
||||||
|
// 国际化区域设置
|
||||||
|
const locale = ref('')
|
||||||
|
const updateLocale = (newLocale: string) => {
|
||||||
|
locale.value = newLocale
|
||||||
|
}
|
||||||
|
|
||||||
// 大模型文案生成
|
// 大模型文案生成
|
||||||
const prompt = ref('')
|
const prompt = ref('')
|
||||||
const llmConfig = ref({
|
const llmConfig = ref({
|
||||||
@@ -72,6 +78,9 @@ export const useAppStore = defineStore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
locale,
|
||||||
|
updateLocale,
|
||||||
|
|
||||||
prompt,
|
prompt,
|
||||||
llmConfig,
|
llmConfig,
|
||||||
updateLLMConfig,
|
updateLLMConfig,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<v-textarea
|
<v-textarea
|
||||||
class="h-full"
|
class="h-full"
|
||||||
v-model="appStore.prompt"
|
v-model="appStore.prompt"
|
||||||
label="提示词"
|
:label="t('prompt.label')"
|
||||||
counter
|
counter
|
||||||
persistent-counter
|
persistent-counter
|
||||||
no-resize
|
no-resize
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="handleGenerate"
|
@click="handleGenerate"
|
||||||
>
|
>
|
||||||
生成
|
{{ t('actions.generate') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-else
|
v-else
|
||||||
@@ -29,51 +29,55 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="handleStopGenerate"
|
@click="handleStopGenerate"
|
||||||
>
|
>
|
||||||
停止
|
{{ t('actions.stop') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
<v-btn v-bind="activatorProps" :disabled="disabled"> 配置 </v-btn>
|
<v-btn v-bind="activatorProps" :disabled="disabled">
|
||||||
|
{{ t('actions.config') }}
|
||||||
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-card prepend-icon="mdi-text-box-edit-outline" title="配置大语言模型接口">
|
<v-card prepend-icon="mdi-text-box-edit-outline" :title="t('llm.configTitle')">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="模型名称"
|
:label="t('llm.modelName')"
|
||||||
v-model="config.modelName"
|
v-model="config.modelName"
|
||||||
required
|
required
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="API 地址"
|
:label="t('llm.apiUrl')"
|
||||||
v-model="config.apiUrl"
|
v-model="config.apiUrl"
|
||||||
required
|
required
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="API Key"
|
:label="t('llm.apiKey')"
|
||||||
v-model="config.apiKey"
|
v-model="config.apiKey"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<small class="text-caption text-medium-emphasis">兼容任意 OpenAI 标准接口</small>
|
<small class="text-caption text-medium-emphasis">{{
|
||||||
|
t('llm.compatibleNote')
|
||||||
|
}}</small>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn text="关闭" variant="plain" @click="handleCloseDialog"></v-btn>
|
<v-btn :text="t('common.close')" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="success"
|
color="success"
|
||||||
text="测试"
|
:text="t('common.test')"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
:loading="testStatus === TestStatusEnum.LOADING"
|
:loading="testStatus === TestStatusEnum.LOADING"
|
||||||
@click="handleTestConfig"
|
@click="handleTestConfig"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
text="保存"
|
:text="t('common.save')"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
@click="handleSaveConfig"
|
@click="handleSaveConfig"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
@@ -86,7 +90,7 @@
|
|||||||
<v-textarea
|
<v-textarea
|
||||||
class="h-full"
|
class="h-full"
|
||||||
v-model="outputText"
|
v-model="outputText"
|
||||||
label="输出文案(可编辑)"
|
:label="t('output.label')"
|
||||||
counter
|
counter
|
||||||
persistent-counter
|
persistent-counter
|
||||||
no-resize
|
no-resize
|
||||||
@@ -102,9 +106,11 @@ import { nextTick, ref, toRaw } from 'vue'
|
|||||||
import { createOpenAI } from '@ai-sdk/openai'
|
import { createOpenAI } from '@ai-sdk/openai'
|
||||||
import { generateText, streamText } from 'ai'
|
import { generateText, streamText } from 'ai'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { useTranslation } from 'i18next-vue'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -116,8 +122,8 @@ const isGenerating = ref(false)
|
|||||||
const abortController = ref<AbortController | null>(null)
|
const abortController = ref<AbortController | null>(null)
|
||||||
const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
||||||
if (!appStore.prompt) {
|
if (!appStore.prompt) {
|
||||||
!oprions?.noToast && toast.warning('提示词不能为空')
|
!oprions?.noToast && toast.warning(t('errors.promptRequired'))
|
||||||
throw new Error('提示词不能为空')
|
throw new Error(t('errors.promptRequired') as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openai = createOpenAI({
|
const openai = createOpenAI({
|
||||||
@@ -150,7 +156,7 @@ const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
|||||||
const errorMessage = error?.message || error?.error?.message
|
const errorMessage = error?.message || error?.error?.message
|
||||||
!oprions?.noToast &&
|
!oprions?.noToast &&
|
||||||
toast.error(
|
toast.error(
|
||||||
`生成失败,请检查大模型配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
`${t('errors.generateFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`,
|
||||||
)
|
)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -197,15 +203,13 @@ const handleTestConfig = async () => {
|
|||||||
})
|
})
|
||||||
console.log(`result`, result)
|
console.log(`result`, result)
|
||||||
testStatus.value = TestStatusEnum.SUCCESS
|
testStatus.value = TestStatusEnum.SUCCESS
|
||||||
toast.success('大模型连接成功')
|
toast.success(t('llm.connectSuccess'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
testStatus.value = TestStatusEnum.ERROR
|
testStatus.value = TestStatusEnum.ERROR
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const errorMessage = error?.message
|
const errorMessage = error?.message
|
||||||
toast.error(
|
toast.error(`${t('llm.connectFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`)
|
||||||
`大模型连接失败,请检查配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
<v-combobox
|
<v-combobox
|
||||||
v-model="appStore.language"
|
v-model="appStore.language"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="语言"
|
:label="t('tts.language')"
|
||||||
:items="appStore.languageList"
|
:items="appStore.languageList"
|
||||||
no-data-text="无数据"
|
:no-data-text="t('common.noData')"
|
||||||
@update:model-value="clearVoice"
|
@update:model-value="clearVoice"
|
||||||
></v-combobox>
|
></v-combobox>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="appStore.gender"
|
v-model="appStore.gender"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="性别"
|
:label="t('tts.gender')"
|
||||||
:items="appStore.genderList"
|
:items="genderItems"
|
||||||
item-title="label"
|
item-title="label"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
@update:model-value="clearVoice"
|
@update:model-value="clearVoice"
|
||||||
@@ -22,24 +22,24 @@
|
|||||||
<v-select
|
<v-select
|
||||||
v-model="appStore.voice"
|
v-model="appStore.voice"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="声音"
|
:label="t('tts.voice')"
|
||||||
:items="filteredVoicesList"
|
:items="filteredVoicesList"
|
||||||
item-title="FriendlyName"
|
item-title="FriendlyName"
|
||||||
return-object
|
return-object
|
||||||
no-data-text="请先选择语言和性别"
|
:no-data-text="t('tts.selectLanguageGenderFirst')"
|
||||||
></v-select>
|
></v-select>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="appStore.speed"
|
v-model="appStore.speed"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="语速"
|
:label="t('tts.speed')"
|
||||||
:items="appStore.speedList"
|
:items="speedItems"
|
||||||
item-title="label"
|
item-title="label"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
></v-select>
|
></v-select>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="appStore.tryListeningText"
|
v-model="appStore.tryListeningText"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
label="试听文本"
|
:label="t('tts.tryText')"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="handleTryListening"
|
@click="handleTryListening"
|
||||||
>
|
>
|
||||||
试听
|
{{ t('tts.tryListen') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</v-form>
|
</v-form>
|
||||||
@@ -60,9 +60,11 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { useTranslation } from 'i18next-vue'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -70,12 +72,12 @@ defineProps<{
|
|||||||
|
|
||||||
const configValid = () => {
|
const configValid = () => {
|
||||||
if (!appStore.voice) {
|
if (!appStore.voice) {
|
||||||
toast.warning('请选择一个声音')
|
toast.warning(t('tts.selectVoiceWarning'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!appStore.tryListeningText) {
|
if (!appStore.tryListeningText) {
|
||||||
toast.warning('试听文本不能为空')
|
toast.warning(t('tts.tryTextEmptyWarning'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +99,10 @@ const handleTryListening = async () => {
|
|||||||
})
|
})
|
||||||
const audio = new Audio(`data:audio/mp3;base64,${speech}`)
|
const audio = new Audio(`data:audio/mp3;base64,${speech}`)
|
||||||
audio.play()
|
audio.play()
|
||||||
toast.info('播放试听语音')
|
toast.info(t('tts.playTryAudio'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('试听语音合成失败', error)
|
console.log('试听语音合成失败', error)
|
||||||
toast.error('试听语音合成失败,请检查网络')
|
toast.error(t('tts.trySynthesisFailedNetwork'))
|
||||||
} finally {
|
} finally {
|
||||||
tryListeningLoading.value = false
|
tryListeningLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -116,13 +118,28 @@ const filteredVoicesList = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const genderItems = computed(() => {
|
||||||
|
return [
|
||||||
|
{ label: t('tts.genderMale'), value: 'Male' },
|
||||||
|
{ label: t('tts.genderFemale'), value: 'Female' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const speedItems = computed(() => {
|
||||||
|
return [
|
||||||
|
{ label: t('tts.speedSlow'), value: -30 },
|
||||||
|
{ label: t('tts.speedMedium'), value: 0 },
|
||||||
|
{ label: t('tts.speedFast'), value: 30 },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const fetchVoices = async () => {
|
const fetchVoices = async () => {
|
||||||
try {
|
try {
|
||||||
appStore.originalVoicesList = await window.electron.edgeTtsGetVoiceList()
|
appStore.originalVoicesList = await window.electron.edgeTtsGetVoiceList()
|
||||||
console.log('EdgeTTS语音列表更新:', appStore.originalVoicesList)
|
console.log('EdgeTTS语音列表更新:', appStore.originalVoicesList)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('获取EdgeTTS语音列表失败', error)
|
console.log('获取EdgeTTS语音列表失败', error)
|
||||||
toast.error('获取EdgeTTS语音列表失败,请检查网络')
|
toast.error(t('errors.edgeTtsListFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -133,7 +150,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boolean }) => {
|
const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boolean }) => {
|
||||||
if (!configValid()) throw new Error('TTS语音合成配置无效')
|
if (!configValid()) throw new Error(t('errors.ttsConfigInvalid'))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electron.edgeTtsSynthesizeToFile({
|
const result = await window.electron.edgeTtsSynthesizeToFile({
|
||||||
@@ -147,7 +164,7 @@ const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boo
|
|||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('语音合成失败', error)
|
console.log('语音合成失败', error)
|
||||||
throw error
|
throw new Error(t('errors.ttsSynthesisFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex gap-2 mb-2">
|
<div class="flex gap-2 mb-2">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="appStore.videoAssetsFolder"
|
v-model="appStore.videoAssetsFolder"
|
||||||
label="分镜视频素材文件夹"
|
:label="t('videoManage.assetsFolderLabel')"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
readonly
|
readonly
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="handleSelectFolder"
|
@click="handleSelectFolder"
|
||||||
>
|
>
|
||||||
选择
|
{{ t('common.select') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<v-empty-state
|
<v-empty-state
|
||||||
v-else
|
v-else
|
||||||
headline="暂无内容"
|
:headline="t('empty.noContent')"
|
||||||
text="从上面选择一个包含足够分镜素材的文件夹"
|
:text="t('empty.hintSelectFolder')"
|
||||||
></v-empty-state>
|
></v-empty-state>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
:loading="refreshAssetsLoading"
|
:loading="refreshAssetsLoading"
|
||||||
@click="refreshAssets"
|
@click="refreshAssets"
|
||||||
>
|
>
|
||||||
刷新素材库
|
{{ t('actions.refreshAssets') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRaw } from 'vue'
|
import { ref, toRaw } from 'vue'
|
||||||
|
import { useTranslation } from 'i18next-vue'
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { ListFilesFromFolderRecord } from '~/electron/types'
|
import { ListFilesFromFolderRecord } from '~/electron/types'
|
||||||
@@ -75,6 +76,7 @@ import random from 'random'
|
|||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -83,7 +85,7 @@ defineProps<{
|
|||||||
// 选择文件夹
|
// 选择文件夹
|
||||||
const handleSelectFolder = async () => {
|
const handleSelectFolder = async () => {
|
||||||
const folderPath = await window.electron.selectFolder({
|
const folderPath = await window.electron.selectFolder({
|
||||||
title: '选择分镜素材文件夹',
|
title: t('dialogs.selectAssetsFolderTitle'),
|
||||||
defaultPath: appStore.videoAssetsFolder,
|
defaultPath: appStore.videoAssetsFolder,
|
||||||
})
|
})
|
||||||
console.log('用户选择分镜素材文件夹,绝对路径:', folderPath)
|
console.log('用户选择分镜素材文件夹,绝对路径:', folderPath)
|
||||||
@@ -109,16 +111,16 @@ const refreshAssets = async () => {
|
|||||||
videoAssets.value = assets.filter((asset) => asset.name.endsWith('.mp4'))
|
videoAssets.value = assets.filter((asset) => asset.name.endsWith('.mp4'))
|
||||||
if (!videoAssets.value.length) {
|
if (!videoAssets.value.length) {
|
||||||
if (assets.length) {
|
if (assets.length) {
|
||||||
toast.warning('选择的文件夹中不包含MP4视频文件')
|
toast.warning(t('videoManage.noMp4InFolder'))
|
||||||
} else {
|
} else {
|
||||||
toast.warning('选择的文件夹为空')
|
toast.warning(t('videoManage.emptyFolder'))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.success('素材读取成功')
|
toast.success(t('videoManage.readSuccess'))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
toast.error('素材读取失败,请检查文件夹是否存在')
|
toast.error(t('videoManage.readFailed'))
|
||||||
} finally {
|
} finally {
|
||||||
refreshAssetsLoading.value = false
|
refreshAssetsLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -130,7 +132,7 @@ const videoInfoList = ref<VideoInfo[]>([])
|
|||||||
const getVideoSegments = (options: { duration: number }) => {
|
const getVideoSegments = (options: { duration: number }) => {
|
||||||
// 判断素材库是否满足时长要求
|
// 判断素材库是否满足时长要求
|
||||||
if (videoInfoList.value.reduce((pre, cur) => pre + cur.duration, 0) < options.duration) {
|
if (videoInfoList.value.reduce((pre, cur) => pre + cur.duration, 0) < options.duration) {
|
||||||
throw new Error('素材总时长不足')
|
throw new Error(t('errors.assetsDurationInsufficient'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜集随机素材片段
|
// 搜集随机素材片段
|
||||||
@@ -153,7 +155,7 @@ const getVideoSegments = (options: { duration: number }) => {
|
|||||||
|
|
||||||
// 获取一个随机素材以及相关信息
|
// 获取一个随机素材以及相关信息
|
||||||
const randomAsset = random.choice(tempVideoAssets)!
|
const randomAsset = random.choice(tempVideoAssets)!
|
||||||
const randomAssetIndex = tempVideoAssets.indexOf(randomAsset)
|
const randomAssetIndex = videoAssets.value.findIndex((asset) => asset.path === randomAsset.path)
|
||||||
const randomAssetInfo = videoInfoList.value[randomAssetIndex]
|
const randomAssetInfo = videoInfoList.value[randomAssetIndex]
|
||||||
|
|
||||||
// 删除已选素材
|
// 删除已选素材
|
||||||
@@ -180,8 +182,8 @@ const getVideoSegments = (options: { duration: number }) => {
|
|||||||
|
|
||||||
// 处理最后一个片段时长小于最小片段时长情况
|
// 处理最后一个片段时长小于最小片段时长情况
|
||||||
if (options.duration - currentTotalDuration - randomSegmentDuration < minSegmentDuration) {
|
if (options.duration - currentTotalDuration - randomSegmentDuration < minSegmentDuration) {
|
||||||
if (randomSegmentDuration + minSegmentDuration < randomAssetInfo.duration) {
|
if (options.duration - currentTotalDuration < randomAssetInfo.duration) {
|
||||||
randomSegmentDuration += minSegmentDuration
|
randomSegmentDuration = options.duration - currentTotalDuration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-0 flex-1 relative">
|
<div class="h-0 flex-1 relative">
|
||||||
<div class="absolute top-1/12 w-full flex justify-center cursor-default select-none">
|
<div class="absolute top-1/12 w-full flex justify-center cursor-default select-none">
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.None"> 空闲,可以开始合成 </v-chip>
|
<v-chip v-if="appStore.renderStatus === RenderStatus.None">
|
||||||
|
{{ t('render.status.idle') }}
|
||||||
|
</v-chip>
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.GenerateText" variant="elevated">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.GenerateText" variant="elevated">
|
||||||
正在使用 AI 大模型生成文案
|
{{ t('render.status.generatingText') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SynthesizedSpeech" variant="elevated">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.SynthesizedSpeech" variant="elevated">
|
||||||
正在使用 TTS 合成语音
|
{{ t('render.status.synthesizingSpeech') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SegmentVideo" variant="elevated">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.SegmentVideo" variant="elevated">
|
||||||
正在处理分镜素材
|
{{ t('render.status.segmentingVideo') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Rendering" variant="elevated">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.Rendering" variant="elevated">
|
||||||
正在渲染视频
|
{{ t('render.status.rendering') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip
|
<v-chip
|
||||||
v-if="appStore.renderStatus === RenderStatus.Completed"
|
v-if="appStore.renderStatus === RenderStatus.Completed"
|
||||||
variant="elevated"
|
variant="elevated"
|
||||||
color="success"
|
color="success"
|
||||||
>
|
>
|
||||||
渲染成功,可以开始下一个
|
{{ t('render.status.success') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Failed" variant="elevated" color="error">
|
<v-chip v-if="appStore.renderStatus === RenderStatus.Failed" variant="elevated" color="error">
|
||||||
渲染失败,请重新尝试
|
{{ t('render.status.failed') }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@
|
|||||||
prepend-icon="mdi-rocket-launch"
|
prepend-icon="mdi-rocket-launch"
|
||||||
@click="emit('renderVideo')"
|
@click="emit('renderVideo')"
|
||||||
>
|
>
|
||||||
开始合成
|
{{ t('render.startRender') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-else
|
v-else
|
||||||
@@ -55,31 +57,36 @@
|
|||||||
prepend-icon="mdi-stop"
|
prepend-icon="mdi-stop"
|
||||||
@click="emit('cancelRender')"
|
@click="emit('cancelRender')"
|
||||||
>
|
>
|
||||||
停止合成
|
{{ t('render.stopRender') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||||
<template v-slot:activator="{ props: activatorProps }">
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
<v-btn v-bind="activatorProps" :disabled="taskInProgress"> 合成配置 </v-btn>
|
<v-btn v-bind="activatorProps" :disabled="taskInProgress">
|
||||||
|
{{ t('actions.config') }}
|
||||||
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-card prepend-icon="mdi-text-box-edit-outline" title="配置合成选项">
|
<v-card
|
||||||
|
prepend-icon="mdi-text-box-edit-outline"
|
||||||
|
:title="t('dialogs.renderConfigTitle')"
|
||||||
|
>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="w-full flex gap-2 mb-4 items-center">
|
<div class="w-full flex gap-2 mb-4 items-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="导出视频宽度"
|
:label="t('render.output.width')"
|
||||||
v-model="config.outputSize.width"
|
v-model="config.outputSize.width"
|
||||||
hide-details
|
hide-details
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="config.outputSize.height"
|
v-model="config.outputSize.height"
|
||||||
label="导出视频高度"
|
:label="t('render.output.height')"
|
||||||
hide-details
|
hide-details
|
||||||
required
|
required
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex gap-2 mb-4 items-center">
|
<div class="w-full flex gap-2 mb-4 items-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="导出文件名"
|
:label="t('render.output.fileName')"
|
||||||
v-model="config.outputFileName"
|
v-model="config.outputFileName"
|
||||||
hide-details
|
hide-details
|
||||||
required
|
required
|
||||||
@@ -88,7 +95,7 @@
|
|||||||
<v-text-field
|
<v-text-field
|
||||||
class="w-[120px] flex-none"
|
class="w-[120px] flex-none"
|
||||||
v-model="config.outputFileExt"
|
v-model="config.outputFileExt"
|
||||||
label="导出格式"
|
:label="t('render.output.format')"
|
||||||
hide-details
|
hide-details
|
||||||
readonly
|
readonly
|
||||||
required
|
required
|
||||||
@@ -96,7 +103,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="w-full flex gap-2 mb-4 items-center">
|
<div class="w-full flex gap-2 mb-4 items-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="导出文件夹"
|
:label="t('render.output.folder')"
|
||||||
v-model="config.outputPath"
|
v-model="config.outputPath"
|
||||||
hide-details
|
hide-details
|
||||||
readonly
|
readonly
|
||||||
@@ -107,12 +114,12 @@
|
|||||||
prepend-icon="mdi-folder-open"
|
prepend-icon="mdi-folder-open"
|
||||||
@click="handleSelectOutputFolder"
|
@click="handleSelectOutputFolder"
|
||||||
>
|
>
|
||||||
选择
|
{{ t('common.select') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex gap-2 mb-2 items-center">
|
<div class="w-full flex gap-2 mb-2 items-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="背景音乐文件夹(.mp3格式,从中随机选取)"
|
:label="t('render.bgmFolderLabel')"
|
||||||
v-model="config.bgmPath"
|
v-model="config.bgmPath"
|
||||||
hide-details
|
hide-details
|
||||||
readonly
|
readonly
|
||||||
@@ -124,17 +131,17 @@
|
|||||||
prepend-icon="mdi-folder-open"
|
prepend-icon="mdi-folder-open"
|
||||||
@click="handleSelectBgmFolder"
|
@click="handleSelectBgmFolder"
|
||||||
>
|
>
|
||||||
选择
|
{{ t('common.select') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn text="关闭" variant="plain" @click="handleCloseDialog"></v-btn>
|
<v-btn :text="t('common.close')" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
text="保存"
|
:text="t('common.save')"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
@click="handleSaveConfig"
|
@click="handleSaveConfig"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
@@ -147,7 +154,7 @@
|
|||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="appStore.autoBatch"
|
v-model="appStore.autoBatch"
|
||||||
label="自动批量合成"
|
:label="t('render.autoBatch')"
|
||||||
color="indigo"
|
color="indigo"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
@@ -158,7 +165,7 @@
|
|||||||
|
|
||||||
<div class="absolute bottom-2 w-full flex justify-center text-sm">
|
<div class="absolute bottom-2 w-full flex justify-center text-sm">
|
||||||
<span class="text-indigo cursor-pointer select-none" @click="handleOpenHomePage">
|
<span class="text-indigo cursor-pointer select-none" @click="handleOpenHomePage">
|
||||||
Powered by YILS(博客地址:https://yils.blog)
|
{{ t('footer.poweredBy') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,9 +173,11 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRaw, nextTick, computed } from 'vue'
|
import { ref, toRaw, nextTick, computed } from 'vue'
|
||||||
|
import { useTranslation } from 'i18next-vue'
|
||||||
import { RenderStatus, useAppStore } from '@/store'
|
import { RenderStatus, useAppStore } from '@/store'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'renderVideo'): void
|
(e: 'renderVideo'): void
|
||||||
@@ -206,7 +215,7 @@ const handleSaveConfig = () => {
|
|||||||
// 选择文件夹
|
// 选择文件夹
|
||||||
const handleSelectOutputFolder = async () => {
|
const handleSelectOutputFolder = async () => {
|
||||||
const folderPath = await window.electron.selectFolder({
|
const folderPath = await window.electron.selectFolder({
|
||||||
title: '选择视频导出文件夹',
|
title: t('dialogs.selectOutputFolderTitle'),
|
||||||
defaultPath: config.value.outputPath,
|
defaultPath: config.value.outputPath,
|
||||||
})
|
})
|
||||||
console.log('用户选择视频导出文件夹,绝对路径:', folderPath)
|
console.log('用户选择视频导出文件夹,绝对路径:', folderPath)
|
||||||
@@ -216,7 +225,7 @@ const handleSelectOutputFolder = async () => {
|
|||||||
}
|
}
|
||||||
const handleSelectBgmFolder = async () => {
|
const handleSelectBgmFolder = async () => {
|
||||||
const folderPath = await window.electron.selectFolder({
|
const folderPath = await window.electron.selectFolder({
|
||||||
title: '选择背景音乐文件夹',
|
title: t('dialogs.selectBgmFolderTitle'),
|
||||||
defaultPath: config.value.bgmPath,
|
defaultPath: config.value.bgmPath,
|
||||||
})
|
})
|
||||||
console.log('用户选择背景音乐文件夹,绝对路径:', folderPath)
|
console.log('用户选择背景音乐文件夹,绝对路径:', folderPath)
|
||||||
|
|||||||
@@ -36,12 +36,14 @@ import VideoRender from './components/video-render.vue'
|
|||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { RenderStatus, useAppStore } from '@/store'
|
import { RenderStatus, useAppStore } from '@/store'
|
||||||
|
import { useTranslation } from 'i18next-vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { ListFilesFromFolderRecord } from '~/electron/types'
|
import { ListFilesFromFolderRecord } from '~/electron/types'
|
||||||
import random from 'random'
|
import random from 'random'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
// 渲染合成视频
|
// 渲染合成视频
|
||||||
const TextGenerateInstance = ref<InstanceType<typeof TextGenerate> | null>()
|
const TextGenerateInstance = ref<InstanceType<typeof TextGenerate> | null>()
|
||||||
@@ -49,15 +51,15 @@ const VideoManageInstance = ref<InstanceType<typeof VideoManage> | null>()
|
|||||||
const TtsControlInstance = ref<InstanceType<typeof TtsControl> | null>()
|
const TtsControlInstance = ref<InstanceType<typeof TtsControl> | null>()
|
||||||
const handleRenderVideo = async () => {
|
const handleRenderVideo = async () => {
|
||||||
if (!appStore.renderConfig.outputFileName) {
|
if (!appStore.renderConfig.outputFileName) {
|
||||||
toast.warning('请先配置导出文件名')
|
toast.warning(t('errors.outputFileNameRequired'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!appStore.renderConfig.outputPath) {
|
if (!appStore.renderConfig.outputPath) {
|
||||||
toast.warning('请先配置导出文件夹')
|
toast.warning(t('errors.outputPathRequired'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!appStore.renderConfig.outputSize?.width || !appStore.renderConfig.outputSize?.height) {
|
if (!appStore.renderConfig.outputSize?.width || !appStore.renderConfig.outputSize?.height) {
|
||||||
toast.warning('请先配置导出分辨率(宽高)')
|
toast.warning(t('errors.outputSizeRequired'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +73,7 @@ const handleRenderVideo = async () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('获取背景音乐列表失败', error)
|
console.log('获取背景音乐列表失败', error)
|
||||||
toast.error('获取背景音乐列表失败,请检查文件夹是否存在')
|
toast.error(t('errors.bgmListFailed'))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -92,10 +94,10 @@ const handleRenderVideo = async () => {
|
|||||||
withCaption: true,
|
withCaption: true,
|
||||||
})
|
})
|
||||||
if (ttsResult?.duration === undefined) {
|
if (ttsResult?.duration === undefined) {
|
||||||
throw new Error('语音合成失败,音频文件损坏')
|
throw new Error(t('errors.ttsFailedCorrupt'))
|
||||||
}
|
}
|
||||||
if (ttsResult?.duration === 0) {
|
if (ttsResult?.duration === 0) {
|
||||||
throw new Error('语音时长为0秒,检查TTS语音合成配置及网络连接是否正常')
|
throw new Error(t('errors.ttsZeroDuration'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取视频片段
|
// 获取视频片段
|
||||||
@@ -132,11 +134,11 @@ const handleRenderVideo = async () => {
|
|||||||
appStore.renderConfig.outputFileExt,
|
appStore.renderConfig.outputFileExt,
|
||||||
})
|
})
|
||||||
|
|
||||||
toast.success('视频合成成功')
|
toast.success(t('success.renderSuccess'))
|
||||||
appStore.updateRenderStatus(RenderStatus.Completed)
|
appStore.updateRenderStatus(RenderStatus.Completed)
|
||||||
|
|
||||||
if (appStore.autoBatch) {
|
if (appStore.autoBatch) {
|
||||||
toast.info('开始合成下一个')
|
toast.info(t('info.batchNext'))
|
||||||
TextGenerateInstance.value?.clearOutputText()
|
TextGenerateInstance.value?.clearOutputText()
|
||||||
handleRenderVideo()
|
handleRenderVideo()
|
||||||
}
|
}
|
||||||
@@ -146,9 +148,7 @@ const handleRenderVideo = async () => {
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const errorMessage = error?.message || error?.error?.message
|
const errorMessage = error?.message || error?.error?.message
|
||||||
toast.error(
|
toast.error(`${t('errors.renderFailedPrefix')}${errorMessage ? '\n' + errorMessage : ''}`)
|
||||||
`视频合成失败,请检查各项配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
|
||||||
)
|
|
||||||
appStore.updateRenderStatus(RenderStatus.Failed)
|
appStore.updateRenderStatus(RenderStatus.Failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ const handleCancelRender = () => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
appStore.updateRenderStatus(RenderStatus.None)
|
appStore.updateRenderStatus(RenderStatus.None)
|
||||||
toast.info('视频合成已终止')
|
toast.info(t('info.renderCanceled'))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default defineConfig({
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '0',
|
top: '0',
|
||||||
right: '0',
|
right: '0',
|
||||||
width: '120px',
|
width: '168px',
|
||||||
height: '35px',
|
height: '35px',
|
||||||
'-webkit-app-region': 'no-drag',
|
'-webkit-app-region': 'no-drag',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
'~': fileURLToPath(new URL('./', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user