mirror of
https://github.com/YILS-LIN/short-video-factory.git
synced 2025-11-25 19:37:50 +08:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56600b3353 | ||
|
|
1cedfae6a7 | ||
|
|
b35c4420f2 | ||
|
|
3768010b60 | ||
|
|
7002e0a153 | ||
|
|
5bf62a7c71 | ||
|
|
42c94a56f8 | ||
|
|
7097a95d78 | ||
|
|
240c0bce72 | ||
|
|
d2dfee949b | ||
|
|
ff94d9679c | ||
|
|
cd68ba810a | ||
|
|
0f880f81bb | ||
|
|
53e940ea4f | ||
|
|
b19e421a8c | ||
|
|
5a5bc3387a | ||
|
|
ef281573a1 | ||
|
|
00a75a69a6 | ||
|
|
41ee746613 | ||
|
|
3f13a62cbd | ||
|
|
fb198c1b00 | ||
|
|
8ea9d06efa | ||
|
|
6c2733b8af | ||
|
|
e3939ae743 | ||
|
|
13a45a817b | ||
|
|
b66714cb0e | ||
|
|
7f600c9dbf | ||
|
|
3696f20277 | ||
|
|
9be840e88e | ||
|
|
8608c401c9 | ||
|
|
a997766543 | ||
|
|
00a21aef46 | ||
|
|
50e3c403d0 | ||
|
|
29adaa3989 | ||
|
|
31780f4703 | ||
|
|
2cead83ddc | ||
|
|
95b1aa359f | ||
|
|
b9b656da9e | ||
|
|
a328a5aea3 | ||
|
|
204468935b | ||
|
|
46c612946b | ||
|
|
8c77530f58 |
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -32,6 +32,7 @@ jobs:
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
export ELECTRON_BUILDER_EXTRA_ARGS="--universal"
|
||||
pnpm run lipo-ffmpeg
|
||||
pnpm run build
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -71,22 +72,18 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
# Upload artifacts
|
||||
- name: Upload artifacts
|
||||
# Publish Release
|
||||
- name: Publish Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
release/${env.VERSION}/*.dmg
|
||||
release/${env.VERSION}/*.exe
|
||||
release/${env.VERSION}/*.deb
|
||||
release/${env.VERSION}/*.rpm
|
||||
release/${env.VERSION}/*.AppImage
|
||||
release/${env.VERSION}/latest*.yml
|
||||
release/${env.VERSION}/*.blockmap
|
||||
release/${{ env.VERSION }}/*.dmg
|
||||
release/${{ env.VERSION }}/*.exe
|
||||
release/${{ env.VERSION }}/*.deb
|
||||
release/${{ env.VERSION }}/*.rpm
|
||||
release/${{ env.VERSION }}/*.AppImage
|
||||
body: ${{ env.NOTES }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
overwrite_files: false
|
||||
append_body: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"scss.validate": false,
|
||||
"css.validate": false,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
},
|
||||
"css.customData": [
|
||||
".vscode/unocss.json"
|
||||
],
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,7 +1,42 @@
|
||||
# Changelog
|
||||
All significant changes to this project will be recorded in this file.
|
||||
此项目的所有显著更改都将记录在此文件中。
|
||||
|
||||
## [v1.0.0] - 2025-08-07
|
||||
## [v1.1.9] - 2025-10-22
|
||||
### Added
|
||||
- Add MAC embedding FFmpeg support
|
||||
### Fixed
|
||||
- Resolve the error of not setting BGM folder
|
||||
- Resolve the issue of reporting errors when there are non mp3 files in the BGM folder
|
||||
- Remove the restriction that the total duration of video materials should not be shorter than that of voice
|
||||
- Optimize some details
|
||||
### 添加
|
||||
- 增加MAC嵌入FFmpeg支持
|
||||
### 修复
|
||||
- 解决不设置bgm文件夹报错
|
||||
- 解决 BGM 文件夹存在非 mp3 文件时报错问题
|
||||
- 移除视频素材总时长不的短于语音的限制
|
||||
- 优化一些细节
|
||||
|
||||
|
||||
## [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
|
||||
- 发布第一个正式版本
|
||||
- 支持使用大语言模型生成文案(推荐免费的GLM-4.5-Flash)
|
||||
@@ -11,7 +46,7 @@
|
||||
- 支持自动化批量任务
|
||||
- 美观的UI界面
|
||||
|
||||
## [v0.7.5] - 2025-08-07
|
||||
## [v0.7.12] - 2025-08-08
|
||||
### Added
|
||||
- 构建测试
|
||||
- 跨平台: macOS dmg, Windows exe, Linux AppImage.
|
||||
- 跨平台: macOS dmg, Windows exe, Linux AppImage.
|
||||
|
||||
70
README.md
70
README.md
@@ -21,12 +21,15 @@
|
||||
</p>
|
||||
|
||||
<!-- 项目徽章 -->
|
||||
|
||||
[![贡献者][contributors-shield]][contributors-url]
|
||||
[![分支][forks-shield]][forks-url]
|
||||
[![星标][stars-shield]][stars-url]
|
||||
[![问题][issues-shield]][issues-url]
|
||||
<!-- [![最新版本][release-shield]][release-url]
|
||||
![发布日期][release-date-shield] -->
|
||||
[![最新版本][release-shield]][release-url]
|
||||
|
||||
<!-- ![发布日期][release-date-shield] -->
|
||||
|
||||
[![许可证][license-shield]][license-url]
|
||||
|
||||
<p align="center">
|
||||
@@ -37,6 +40,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 关于项目 -->
|
||||
|
||||
## 📖 关于项目
|
||||
|
||||
短视频工厂是一个开源的桌面端应用,旨在通过AI技术简化短视频的制作流程。用户可以通过简单的提示词文本+视频分镜素材,快速且自动的剪辑出高质量的产品营销和泛内容短视频。该项目集成了AI驱动的文案生成、语音合成、视频剪辑、字幕特效等功能,旨在为用户提供开箱即用的短视频制作体验。
|
||||
@@ -48,11 +52,21 @@
|
||||
- 🎥 **自动剪辑**:支持多种视频格式,自动化批量处理视频剪辑任务
|
||||
- 🎙️ **语音合成**:将生成的文案转换为自然流畅的语音
|
||||
- 🎬 **字幕特效**:自动添加字幕和特效,提升视频质量
|
||||
- 📦 **批量处理**:支持批量任务,按预设自动持续合成视频
|
||||
- 🌐 **多语言支持**:支持中文、英文等多种语言,满足不同用户需求
|
||||
- 📦 **开箱即用**:无需复杂配置,用户可以快速上手
|
||||
- 📈 **持续更新**:定期发布新版本,修复bug并添加新功能
|
||||
- 🔒 **安全可靠**:完全本地本地化运行,确保用户数据安全
|
||||
- 🎨 **用户友好**:简洁直观的用户界面,易于操作
|
||||
- 🌐 **多平台支持**:支持Windows、macOS和Linux等多个操作系统
|
||||
- 💻 **多平台支持**:支持Windows、macOS和Linux等多个操作系统
|
||||
|
||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||
|
||||
## 🚀 开始使用
|
||||
|
||||
前往 [Github Release](https://github.com/YILS-LIN/short-video-factory/releases) 下载最新版本
|
||||
|
||||
前往 [官方文档](https://short-video-factory.yils.blog) 查看使用手册
|
||||
|
||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||
|
||||
@@ -66,14 +80,39 @@
|
||||
- [x] 语音合成,支持EdgeTTS
|
||||
- [x] 视频剪辑,文案、视频、音频、字幕合成,自动混剪
|
||||
- [x] 批量处理,支持一个批量任务,按预设自动持续合成视频
|
||||
- [x] 多语言支持,能够支持中文、英文等多种语言
|
||||
- [x] 完善的使用手册
|
||||
- [ ] 更全面的参数调整
|
||||
- [ ] 更多的语音合成API
|
||||
- [ ] 字幕特效,支持多种字幕样式和特效
|
||||
|
||||
|
||||
查看[开放问题](https://github.com/YILS-LIN/short-video-factory/issues)以获取提议功能(和已知问题)的完整列表。
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 贡献 -->
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
贡献让开源社区成为了一个学习、启发和创造的绝佳场所。**非常感谢**您所做的任何贡献。
|
||||
@@ -103,6 +142,7 @@
|
||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||
|
||||
<!-- 许可证 -->
|
||||
|
||||
## 🎗 许可证
|
||||
|
||||
[![许可证][license-shield]][license-url]
|
||||
@@ -111,7 +151,22 @@ Copyright © 2025 YILS.
|
||||
|
||||
<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>
|
||||
|
||||
👉 在此处查看捐赠者名单:[千古留名 - 捐赠者留言板](https://short-video-factory.yils.blog/donate/list.html)
|
||||
|
||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||
|
||||
<!-- 星标历史 -->
|
||||
|
||||
## ⭐ 星标历史
|
||||
|
||||
<div align="center">
|
||||
@@ -120,7 +175,10 @@ Copyright © 2025 YILS.
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p align="right">(<a href="#readme-top">返回顶部</a>)</p>
|
||||
|
||||
<!-- MARKDOWN链接和图片 -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/YILS-LIN/short-video-factory.svg?color=c4f042&labelColor=black&style=flat-square
|
||||
[contributors-url]: https://github.com/YILS-LIN/short-video-factory/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/YILS-LIN/short-video-factory.svg?color=8ae8ff&labelColor=black&style=flat-square
|
||||
@@ -129,8 +187,8 @@ Copyright © 2025 YILS.
|
||||
[stars-url]: https://github.com/YILS-LIN/short-video-factory/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/YILS-LIN/short-video-factory.svg?labelColor=black&style=flat-square
|
||||
[issues-url]: https://github.com/YILS-LIN/short-video-factory/issues
|
||||
[release-shield]: https://img.shields.io/github/v/release/YILS-LIN/short-video-factory?style=flat-round
|
||||
[release-shield]: https://img.shields.io/github/v/release/YILS-LIN/short-video-factory?labelColor=black&style=flat-square
|
||||
[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
|
||||
[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: {
|
||||
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: {
|
||||
target: ['dmg'],
|
||||
artifactName: '${productName}-Mac-${arch}-${version}-Installer.${ext}',
|
||||
target: [
|
||||
{
|
||||
target: 'dmg',
|
||||
arch: ['universal'],
|
||||
},
|
||||
],
|
||||
artifactName: '${name}-${version}-mac-${arch}-installer.${ext}',
|
||||
icon: './public/icon.png',
|
||||
},
|
||||
win: {
|
||||
@@ -21,7 +26,7 @@
|
||||
target: 'nsis',
|
||||
},
|
||||
],
|
||||
artifactName: '${productName}-Windows-${arch}-${version}-Setup.${ext}',
|
||||
artifactName: '${name}-${version}-win-${arch}-setup.${ext}',
|
||||
icon: './public/icon.png',
|
||||
},
|
||||
nsis: {
|
||||
@@ -33,7 +38,8 @@
|
||||
},
|
||||
linux: {
|
||||
target: ['AppImage'],
|
||||
artifactName: '${productName}-Linux-${arch}-${version}.${ext}',
|
||||
artifactName: '${name}-${version}-linux-${arch}.${ext}',
|
||||
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` 中暴露方法
|
||||
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
|
||||
|
||||
@@ -8,11 +8,10 @@ import { generateUniqueFileName } from '../lib/tools'
|
||||
|
||||
const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
|
||||
const isWindows = process.platform === 'win32'
|
||||
const ffmpegPath: string = isWindows
|
||||
? VITE_DEV_SERVER_URL
|
||||
? require('ffmpeg-static')
|
||||
: (require('ffmpeg-static') as string).replace('app.asar', 'app.asar.unpacked')
|
||||
: 'ffmpeg'
|
||||
|
||||
const ffmpegPath: string = VITE_DEV_SERVER_URL
|
||||
? require('ffmpeg-static')
|
||||
: (require('ffmpeg-static') as string).replace('app.asar', 'app.asar.unpacked')
|
||||
|
||||
// async function test() {
|
||||
// try {
|
||||
@@ -86,7 +85,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]`)
|
||||
@@ -122,6 +124,8 @@ export async function renderVideo(
|
||||
'aac',
|
||||
'-b:a',
|
||||
'128k',
|
||||
'-fps_mode',
|
||||
'cfr',
|
||||
'-s',
|
||||
`${outputSize.width}x${outputSize.height}`,
|
||||
'-progress',
|
||||
@@ -132,6 +136,7 @@ export async function renderVideo(
|
||||
)
|
||||
|
||||
// 打印命令
|
||||
// console.log('传入参数:', params)
|
||||
// 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')
|
||||
: RENDERER_DIST
|
||||
|
||||
export default function initIPC(win: BrowserWindow) {
|
||||
export default function initIPC() {
|
||||
// sqlite 查询
|
||||
ipcMain.handle('sqlite-query', (_event, params) => sqQuery(params))
|
||||
// sqlite 插入
|
||||
@@ -33,15 +33,18 @@ export default function initIPC(win: BrowserWindow) {
|
||||
ipcMain.handle('sqlite-bulk-insert-or-update', (_event, params) => sqBulkInsertOrUpdate(params))
|
||||
|
||||
// 是否最大化
|
||||
ipcMain.handle('is-win-maxed', () => {
|
||||
ipcMain.handle('is-win-maxed', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
return win?.isMaximized()
|
||||
})
|
||||
//最小化
|
||||
ipcMain.on('win-min', () => {
|
||||
ipcMain.on('win-min', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.minimize()
|
||||
})
|
||||
//最大化
|
||||
ipcMain.on('win-max', () => {
|
||||
ipcMain.on('win-max', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (win?.isMaximized()) {
|
||||
win?.restore()
|
||||
} else {
|
||||
@@ -49,7 +52,8 @@ export default function initIPC(win: BrowserWindow) {
|
||||
}
|
||||
})
|
||||
//关闭程序
|
||||
ipcMain.on('win-close', () => {
|
||||
ipcMain.on('win-close', (event) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
win?.close()
|
||||
})
|
||||
|
||||
@@ -59,7 +63,12 @@ export default function initIPC(win: BrowserWindow) {
|
||||
})
|
||||
|
||||
// 选择文件夹
|
||||
ipcMain.handle('select-folder', async (_event, params?: SelectFolderParams) => {
|
||||
ipcMain.handle('select-folder', async (event, params?: SelectFolderParams) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!win) {
|
||||
throw new Error('无法获取窗口')
|
||||
}
|
||||
|
||||
const result = await dialog.showOpenDialog(win, {
|
||||
properties: ['openDirectory'],
|
||||
title: params?.title || '选择文件夹',
|
||||
|
||||
1
electron/lib/is-dev.ts
Normal file
1
electron/lib/is-dev.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const isDev = !!process.env['VITE_DEV_SERVER_URL']
|
||||
@@ -2,8 +2,6 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { app } from 'electron'
|
||||
|
||||
// import packageJson from '~/package.json'
|
||||
|
||||
/**
|
||||
* 生成有序的唯一文件名,用于处理文件已存在的情况
|
||||
*/
|
||||
|
||||
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 { 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 模块的方法
|
||||
@@ -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 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
|
||||
|
||||
@@ -38,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,
|
||||
@@ -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除外。在那里,这很常见
|
||||
//让应用程序及其菜单栏保持活动状态,直到用户退出
|
||||
//显式使用Cmd+Q。
|
||||
@@ -91,9 +180,14 @@ app.on('activate', () => {
|
||||
// app.disableHardwareAcceleration();
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
initSqlite()
|
||||
initIPC(win as BrowserWindow)
|
||||
initI18n()
|
||||
initIPC()
|
||||
createWindow()
|
||||
|
||||
i18next.on('languageChanged', () => {
|
||||
buildMenu()
|
||||
})
|
||||
|
||||
// 允许跨站请求携带cookie
|
||||
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', {
|
||||
isWinMaxed: () => ipcRenderer.invoke('is-win-maxed'),
|
||||
winMin: () => ipcRenderer.send('win-min'),
|
||||
|
||||
147
locales/en/common.json
Normal file
147
locales/en/common.json
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"close": "Close",
|
||||
"test": "Test",
|
||||
"save": "Save",
|
||||
"select": "Select",
|
||||
"noData": "No data"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
"stop": "Stop",
|
||||
"config": "Configure",
|
||||
"refreshAssets": "Refresh Assets"
|
||||
},
|
||||
"dialogs": {
|
||||
"selectAssetsFolderTitle": "Select storyboard assets folder",
|
||||
"selectOutputFolderTitle": "Select video export folder",
|
||||
"selectBgmFolderTitle": "Select background music folder",
|
||||
"renderConfigTitle": "Configure render options"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"llm": {
|
||||
"promptLabel": "Prompt",
|
||||
"outputLabel": "Output Text (editable)",
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Powered by YILS (Blog: https://yils.blog)"
|
||||
}
|
||||
}
|
||||
147
locales/zh-CN/common.json
Normal file
147
locales/zh-CN/common.json
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"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": "了解更多"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"close": "关闭",
|
||||
"test": "测试",
|
||||
"save": "保存",
|
||||
"select": "选择",
|
||||
"noData": "无数据"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "生成",
|
||||
"stop": "停止",
|
||||
"config": "配置",
|
||||
"refreshAssets": "刷新素材库"
|
||||
},
|
||||
"dialogs": {
|
||||
"selectAssetsFolderTitle": "选择分镜素材文件夹",
|
||||
"selectOutputFolderTitle": "选择视频导出文件夹",
|
||||
"selectBgmFolderTitle": "选择背景音乐文件夹",
|
||||
"renderConfigTitle": "配置合成选项"
|
||||
},
|
||||
"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": "从上面选择一个包含足够分镜素材的文件夹"
|
||||
},
|
||||
"llm": {
|
||||
"promptLabel": "提示词",
|
||||
"outputLabel": "输出文案(可编辑)",
|
||||
"configTitle": "配置大语言模型接口",
|
||||
"modelName": "模型名称",
|
||||
"apiUrl": "API 地址",
|
||||
"apiKey": "API Key",
|
||||
"compatibleNote": "兼容任意 OpenAI 标准接口",
|
||||
"connectSuccess": "大模型连接成功",
|
||||
"connectFailedPrefix": "大模型连接失败,请检查配置是否正确"
|
||||
},
|
||||
"videoManage": {
|
||||
"assetsFolderLabel": "分镜视频素材文件夹",
|
||||
"noMp4InFolder": "选择的文件夹中不包含MP4视频文件",
|
||||
"emptyFolder": "选择的文件夹为空",
|
||||
"readSuccess": "素材读取成功",
|
||||
"readFailed": "素材读取失败,请检查文件夹是否存在"
|
||||
},
|
||||
"tts": {
|
||||
"language": "语言",
|
||||
"gender": "性别",
|
||||
"voice": "声音",
|
||||
"speed": "语速",
|
||||
"tryText": "试听文本",
|
||||
"tryListen": "试听",
|
||||
"selectLanguageGenderFirst": "请先选择语言和性别",
|
||||
"selectVoiceWarning": "请选择一个声音",
|
||||
"tryTextEmptyWarning": "试听文本不能为空",
|
||||
"playTryAudio": "播放试听语音",
|
||||
"trySynthesisFailedNetwork": "试听语音合成失败,请检查网络",
|
||||
"genderMale": "男性",
|
||||
"genderFemale": "女性",
|
||||
"speedSlow": "慢",
|
||||
"speedMedium": "中",
|
||||
"speedFast": "快"
|
||||
},
|
||||
"render": {
|
||||
"status": {
|
||||
"idle": "空闲,可以开始合成",
|
||||
"generatingText": "正在使用 AI 大模型生成文案",
|
||||
"synthesizingSpeech": "正在使用 TTS 合成语音",
|
||||
"segmentingVideo": "正在处理分镜素材",
|
||||
"rendering": "正在渲染视频",
|
||||
"success": "渲染成功,可以开始下一个",
|
||||
"failed": "渲染失败,请重新尝试"
|
||||
},
|
||||
"startRender": "开始合成",
|
||||
"stopRender": "停止合成",
|
||||
"autoBatch": "自动批量合成",
|
||||
"bgmFolderLabel": "背景音乐文件夹(随机选取.mp3,为空则无背景音)",
|
||||
"output": {
|
||||
"width": "导出视频宽度",
|
||||
"height": "导出视频高度",
|
||||
"fileName": "导出文件名",
|
||||
"format": "导出格式",
|
||||
"folder": "导出文件夹"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Powered by YILS(博客地址:https://yils.blog)"
|
||||
}
|
||||
}
|
||||
25
package.json
25
package.json
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "short-video-factory",
|
||||
"description": "短视频工厂,一键生成产品营销与泛内容短视频,AI批量自动剪辑",
|
||||
"version": "0.7.5",
|
||||
"version": "1.1.9",
|
||||
"author": {
|
||||
"name": "YILS",
|
||||
"developer": "YILS",
|
||||
"email": "yils_lin@163.com",
|
||||
"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",
|
||||
"scripts": {
|
||||
"dev": "cross-env VITE_CJS_IGNORE_WARNING=true vite",
|
||||
@@ -16,12 +16,15 @@
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"postinstall": "node scripts/post-install.js"
|
||||
"postinstall": "node scripts/post-install.js",
|
||||
"lipo-ffmpeg":"node scripts/lipo-ffmpeg.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"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"
|
||||
@@ -39,6 +42,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",
|
||||
@@ -57,6 +62,20 @@
|
||||
"vue-tsc": "3.0.1",
|
||||
"vuetify": "^3.9.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"ignoredBuiltDependencies": [
|
||||
"better-sqlite3",
|
||||
"ffmpeg-static"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"bufferutil",
|
||||
"electron",
|
||||
"esbuild",
|
||||
"utf-8-validate",
|
||||
"vue-demi"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.12.4",
|
||||
"engines": {
|
||||
"node": ">=22.17.0",
|
||||
|
||||
97
pnpm-lock.yaml
generated
97
pnpm-lock.yaml
generated
@@ -17,6 +17,12 @@ 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
|
||||
@@ -63,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
|
||||
@@ -293,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'}
|
||||
@@ -1365,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'}
|
||||
@@ -1809,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}
|
||||
@@ -2137,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
|
||||
@@ -2582,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==}
|
||||
|
||||
@@ -2794,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'}
|
||||
@@ -3083,6 +3140,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/runtime@7.28.3': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
@@ -4256,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
|
||||
@@ -4789,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
|
||||
@@ -5067,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
|
||||
|
||||
@@ -5538,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
|
||||
@@ -5743,6 +5833,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
ignoredBuiltDependencies:
|
||||
- better-sqlite3
|
||||
- ffmpeg-static
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- bufferutil
|
||||
- electron
|
||||
- esbuild
|
||||
- utf-8-validate
|
||||
- vue-demi
|
||||
42
scripts/lipo-ffmpeg.js
Normal file
42
scripts/lipo-ffmpeg.js
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 1. 设置环境变量 FFMPEG_BINARIES_URL
|
||||
* 2. 分别 rebuild x64 / arm64 两个架构
|
||||
* 3. 用 lipo 合并为通用二进制
|
||||
*/
|
||||
const { execSync } = require('child_process')
|
||||
const path = require('path')
|
||||
|
||||
// 1. 环境变量
|
||||
const FFMPEG_BINARIES_URL = process.env['npm_config_ffmpeg_binaries_url']
|
||||
|
||||
// 2. 工具函数:执行命令并带彩色输出
|
||||
function run(cmd, opts = {}) {
|
||||
console.log(`\n→ ${cmd}`)
|
||||
try {
|
||||
execSync(cmd, { stdio: 'inherit', shell: true, ...opts })
|
||||
} catch (e) {
|
||||
console.error(`❌ 命令失败: ${cmd}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 开始干活
|
||||
const archs = ['x64', 'arm64']
|
||||
const ffmpegStaticDir = path.join(__dirname, '..', 'node_modules', 'ffmpeg-static')
|
||||
|
||||
// rebuild 两次
|
||||
archs.forEach((arch) => {
|
||||
run(
|
||||
`pnpm cross-env FFMPEG_BINARIES_URL=${FFMPEG_BINARIES_URL} npm --arch=${arch} rebuild -f ffmpeg-static`,
|
||||
)
|
||||
run(`mv ${ffmpegStaticDir}/ffmpeg ${ffmpegStaticDir}/ffmpeg-${arch}`)
|
||||
})
|
||||
|
||||
// 合并
|
||||
run('lipo -create ffmpeg-arm64 ffmpeg-x64 -output ffmpeg', { cwd: ffmpegStaticDir })
|
||||
|
||||
// 赋权
|
||||
run('chmod 0755 ffmpeg', { cwd: ffmpegStaticDir })
|
||||
|
||||
console.log('\n✅ 通用 ffmpeg 已生成:', path.join(ffmpegStaticDir, 'ffmpeg'))
|
||||
@@ -1,22 +1,18 @@
|
||||
const { execSync } = require('node:child_process')
|
||||
const ffmpeg = require('ffmpeg-static')
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
|
||||
const isWindows = process.platform === 'win32'
|
||||
console.log('Downloading ffmpeg...')
|
||||
execSync(
|
||||
`cross-env FFMPEG_BINARIES_URL=${process.env['npm_config_ffmpeg_binaries_url']} npm rebuild ffmpeg-static`,
|
||||
)
|
||||
console.log(`FFmpeg downloaded to path: ${ffmpeg}`)
|
||||
|
||||
if (isWindows) {
|
||||
console.log('Windows detected, running install for ffmpeg-static...')
|
||||
try {
|
||||
// 进入 ffmpeg-static 目录并运行其构建脚本
|
||||
execSync('npm explore ffmpeg-static -- pnpm run install ', {
|
||||
cwd: process.cwd(),
|
||||
stdio: 'inherit',
|
||||
})
|
||||
console.log('ffmpeg-static installed successfully.')
|
||||
} catch (error) {
|
||||
console.error('Failed to install ffmpeg-static:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
console.log('Not Windows, skipping install ffmpeg-static.')
|
||||
const isWindows = process.platform === 'win32'
|
||||
if (!isWindows) {
|
||||
console.log('Not Windows, need to set ffmpeg permissions.')
|
||||
fs.chmodSync(ffmpeg, 0o755)
|
||||
execSync(`chmod +x ${ffmpeg}`)
|
||||
console.log('FFmpeg permissions already set.')
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
appName: 'AI Short Video Factory - 短视频工厂',
|
||||
}
|
||||
@@ -2,9 +2,35 @@
|
||||
<div class="layout-container">
|
||||
<div class="logo" v-if="!route.meta.hideAppIcon">
|
||||
<img src="/icon.png" alt="" />
|
||||
<span>{{ GlobalSetting.appName }}</span>
|
||||
<span>{{ t('app.name') }}</span>
|
||||
</div>
|
||||
<div class="window-control-bar">
|
||||
<div class="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>
|
||||
@@ -21,13 +47,27 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import GlobalSetting from '../../setting.global'
|
||||
import { ref } from 'vue'
|
||||
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 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 () => {
|
||||
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
|
||||
29
src/main.ts
29
src/main.ts
@@ -1,4 +1,4 @@
|
||||
import 'vuetify/styles'
|
||||
import 'vuetify/styles/main.sass'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
@@ -12,11 +12,14 @@ import 'virtual:uno.css'
|
||||
import './assets/base.scss'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import GlobalSetting from '../setting.global'
|
||||
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,8 +32,6 @@ const vuetify = createVuetify({
|
||||
},
|
||||
})
|
||||
|
||||
document.title = GlobalSetting.appName
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(vuetify)
|
||||
@@ -38,9 +39,19 @@ app.use(Toast, { position: 'bottom-left', pauseOnFocusLoss: false } as PluginOpt
|
||||
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)
|
||||
})
|
||||
|
||||
// 监听主进程切换语言
|
||||
window.ipcRenderer.on('i18n-changeLanguage', (_event, lng) => {
|
||||
i18next.changeLanguage(lng)
|
||||
useAppStore().updateLocale(lng)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,12 @@ export enum RenderStatus {
|
||||
export const useAppStore = defineStore(
|
||||
'app',
|
||||
() => {
|
||||
// 国际化区域设置
|
||||
const locale = ref('')
|
||||
const updateLocale = (newLocale: string) => {
|
||||
locale.value = newLocale
|
||||
}
|
||||
|
||||
// 大模型文案生成
|
||||
const prompt = ref('')
|
||||
const llmConfig = ref({
|
||||
@@ -72,6 +78,9 @@ export const useAppStore = defineStore(
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
updateLocale,
|
||||
|
||||
prompt,
|
||||
llmConfig,
|
||||
updateLLMConfig,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<v-textarea
|
||||
class="h-full"
|
||||
v-model="appStore.prompt"
|
||||
label="提示词"
|
||||
:label="t('llm.promptLabel')"
|
||||
counter
|
||||
persistent-counter
|
||||
no-resize
|
||||
@@ -19,7 +19,7 @@
|
||||
:disabled="disabled"
|
||||
@click="handleGenerate"
|
||||
>
|
||||
生成
|
||||
{{ t('actions.generate') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
@@ -29,51 +29,55 @@
|
||||
:disabled="disabled"
|
||||
@click="handleStopGenerate"
|
||||
>
|
||||
停止
|
||||
{{ t('actions.stop') }}
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" :disabled="disabled"> 配置 </v-btn>
|
||||
<v-btn v-bind="activatorProps" :disabled="disabled">
|
||||
{{ t('actions.config') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card prepend-icon="mdi-text-box-edit-outline" title="配置大语言模型接口">
|
||||
<v-card prepend-icon="mdi-text-box-edit-outline" :title="t('llm.configTitle')">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
label="模型名称"
|
||||
:label="t('llm.modelName')"
|
||||
v-model="config.modelName"
|
||||
required
|
||||
clearable
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
label="API 地址"
|
||||
:label="t('llm.apiUrl')"
|
||||
v-model="config.apiUrl"
|
||||
required
|
||||
clearable
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
label="API Key"
|
||||
:label="t('llm.apiKey')"
|
||||
v-model="config.apiKey"
|
||||
type="password"
|
||||
required
|
||||
clearable
|
||||
></v-text-field>
|
||||
<small class="text-caption text-medium-emphasis">兼容任意 OpenAI 标准接口</small>
|
||||
<small class="text-caption text-medium-emphasis">{{
|
||||
t('llm.compatibleNote')
|
||||
}}</small>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text="关闭" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||
<v-btn :text="t('common.close')" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||
<v-btn
|
||||
color="success"
|
||||
text="测试"
|
||||
:text="t('common.test')"
|
||||
variant="tonal"
|
||||
:loading="testStatus === TestStatusEnum.LOADING"
|
||||
@click="handleTestConfig"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
text="保存"
|
||||
:text="t('common.save')"
|
||||
variant="tonal"
|
||||
@click="handleSaveConfig"
|
||||
></v-btn>
|
||||
@@ -86,7 +90,7 @@
|
||||
<v-textarea
|
||||
class="h-full"
|
||||
v-model="outputText"
|
||||
label="输出文案(可编辑)"
|
||||
:label="t('llm.outputLabel')"
|
||||
counter
|
||||
persistent-counter
|
||||
no-resize
|
||||
@@ -102,9 +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 { useTranslation } from 'i18next-vue'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useTranslation()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
@@ -116,8 +122,8 @@ const isGenerating = ref(false)
|
||||
const abortController = ref<AbortController | null>(null)
|
||||
const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
||||
if (!appStore.prompt) {
|
||||
!oprions?.noToast && toast.warning('提示词不能为空')
|
||||
throw new Error('提示词不能为空')
|
||||
!oprions?.noToast && toast.warning(t('errors.promptRequired'))
|
||||
throw new Error(t('errors.promptRequired') as string)
|
||||
}
|
||||
|
||||
const openai = createOpenAI({
|
||||
@@ -150,7 +156,7 @@ const handleGenerate = async (oprions?: { noToast?: boolean }) => {
|
||||
const errorMessage = error?.message || error?.error?.message
|
||||
!oprions?.noToast &&
|
||||
toast.error(
|
||||
`生成失败,请检查大模型配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
||||
`${t('errors.generateFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
@@ -197,15 +203,13 @@ const handleTestConfig = async () => {
|
||||
})
|
||||
console.log(`result`, result)
|
||||
testStatus.value = TestStatusEnum.SUCCESS
|
||||
toast.success('大模型连接成功')
|
||||
toast.success(t('llm.connectSuccess'))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
testStatus.value = TestStatusEnum.ERROR
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message
|
||||
toast.error(
|
||||
`大模型连接失败,请检查配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
||||
)
|
||||
toast.error(`${t('llm.connectFailedPrefix')}\n${errorMessage ? 'Error: ' + errorMessage : ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<v-combobox
|
||||
v-model="appStore.language"
|
||||
density="comfortable"
|
||||
label="语言"
|
||||
:label="t('tts.language')"
|
||||
:items="appStore.languageList"
|
||||
no-data-text="无数据"
|
||||
:no-data-text="t('common.noData')"
|
||||
@update:model-value="clearVoice"
|
||||
></v-combobox>
|
||||
<v-select
|
||||
v-model="appStore.gender"
|
||||
density="comfortable"
|
||||
label="性别"
|
||||
:items="appStore.genderList"
|
||||
:label="t('tts.gender')"
|
||||
:items="genderItems"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
@update:model-value="clearVoice"
|
||||
@@ -22,24 +22,24 @@
|
||||
<v-select
|
||||
v-model="appStore.voice"
|
||||
density="comfortable"
|
||||
label="声音"
|
||||
:label="t('tts.voice')"
|
||||
:items="filteredVoicesList"
|
||||
item-title="FriendlyName"
|
||||
return-object
|
||||
no-data-text="请先选择语言和性别"
|
||||
:no-data-text="t('tts.selectLanguageGenderFirst')"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="appStore.speed"
|
||||
density="comfortable"
|
||||
label="语速"
|
||||
:items="appStore.speedList"
|
||||
:label="t('tts.speed')"
|
||||
:items="speedItems"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
<v-text-field
|
||||
v-model="appStore.tryListeningText"
|
||||
density="comfortable"
|
||||
label="试听文本"
|
||||
:label="t('tts.tryText')"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="mb-2"
|
||||
@@ -49,7 +49,7 @@
|
||||
:disabled="disabled"
|
||||
@click="handleTryListening"
|
||||
>
|
||||
试听
|
||||
{{ t('tts.tryListen') }}
|
||||
</v-btn>
|
||||
</v-sheet>
|
||||
</v-form>
|
||||
@@ -60,9 +60,11 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useTranslation } from 'i18next-vue'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useTranslation()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
@@ -70,12 +72,12 @@ defineProps<{
|
||||
|
||||
const configValid = () => {
|
||||
if (!appStore.voice) {
|
||||
toast.warning('请选择一个声音')
|
||||
toast.warning(t('tts.selectVoiceWarning'))
|
||||
return false
|
||||
}
|
||||
|
||||
if (!appStore.tryListeningText) {
|
||||
toast.warning('试听文本不能为空')
|
||||
toast.warning(t('tts.tryTextEmptyWarning'))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -97,10 +99,10 @@ const handleTryListening = async () => {
|
||||
})
|
||||
const audio = new Audio(`data:audio/mp3;base64,${speech}`)
|
||||
audio.play()
|
||||
toast.info('播放试听语音')
|
||||
toast.info(t('tts.playTryAudio'))
|
||||
} catch (error) {
|
||||
console.log('试听语音合成失败', error)
|
||||
toast.error('试听语音合成失败,请检查网络')
|
||||
toast.error(t('tts.trySynthesisFailedNetwork'))
|
||||
} finally {
|
||||
tryListeningLoading.value = false
|
||||
}
|
||||
@@ -116,13 +118,28 @@ const filteredVoicesList = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const genderItems = computed(() => {
|
||||
return [
|
||||
{ label: t('tts.genderMale'), value: 'Male' },
|
||||
{ label: t('tts.genderFemale'), value: 'Female' },
|
||||
]
|
||||
})
|
||||
|
||||
const speedItems = computed(() => {
|
||||
return [
|
||||
{ label: t('tts.speedSlow'), value: -30 },
|
||||
{ label: t('tts.speedMedium'), value: 0 },
|
||||
{ label: t('tts.speedFast'), value: 30 },
|
||||
]
|
||||
})
|
||||
|
||||
const fetchVoices = async () => {
|
||||
try {
|
||||
appStore.originalVoicesList = await window.electron.edgeTtsGetVoiceList()
|
||||
console.log('EdgeTTS语音列表更新:', appStore.originalVoicesList)
|
||||
} catch (error) {
|
||||
console.log('获取EdgeTTS语音列表失败', error)
|
||||
toast.error('获取EdgeTTS语音列表失败,请检查网络')
|
||||
toast.error(t('errors.edgeTtsListFailed'))
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
@@ -133,7 +150,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boolean }) => {
|
||||
if (!configValid()) throw new Error('TTS语音合成配置无效')
|
||||
if (!configValid()) throw new Error(t('errors.ttsConfigInvalid'))
|
||||
|
||||
try {
|
||||
const result = await window.electron.edgeTtsSynthesizeToFile({
|
||||
@@ -147,7 +164,7 @@ const synthesizedSpeechToFile = async (option: { text: string; withCaption?: boo
|
||||
return result
|
||||
} catch (error) {
|
||||
console.log('语音合成失败', error)
|
||||
throw error
|
||||
throw new Error(t('errors.ttsSynthesisFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="flex gap-2 mb-2">
|
||||
<v-text-field
|
||||
v-model="appStore.videoAssetsFolder"
|
||||
label="分镜视频素材文件夹"
|
||||
:label="t('videoManage.assetsFolderLabel')"
|
||||
density="compact"
|
||||
hide-details
|
||||
readonly
|
||||
@@ -17,7 +17,7 @@
|
||||
:disabled="disabled"
|
||||
@click="handleSelectFolder"
|
||||
>
|
||||
选择
|
||||
{{ t('common.select') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
</div>
|
||||
<v-empty-state
|
||||
v-else
|
||||
headline="暂无内容"
|
||||
text="从上面选择一个包含足够分镜素材的文件夹"
|
||||
:headline="t('empty.noContent')"
|
||||
:text="t('empty.hintSelectFolder')"
|
||||
></v-empty-state>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
:loading="refreshAssetsLoading"
|
||||
@click="refreshAssets"
|
||||
>
|
||||
刷新素材库
|
||||
{{ t('actions.refreshAssets') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-sheet>
|
||||
@@ -66,6 +66,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRaw } from 'vue'
|
||||
import { useTranslation } from 'i18next-vue'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ListFilesFromFolderRecord } from '~/electron/types'
|
||||
@@ -75,6 +76,7 @@ import random from 'random'
|
||||
|
||||
const toast = useToast()
|
||||
const appStore = useAppStore()
|
||||
const { t } = useTranslation()
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean
|
||||
@@ -83,7 +85,7 @@ defineProps<{
|
||||
// 选择文件夹
|
||||
const handleSelectFolder = async () => {
|
||||
const folderPath = await window.electron.selectFolder({
|
||||
title: '选择分镜素材文件夹',
|
||||
title: t('dialogs.selectAssetsFolderTitle'),
|
||||
defaultPath: appStore.videoAssetsFolder,
|
||||
})
|
||||
console.log('用户选择分镜素材文件夹,绝对路径:', folderPath)
|
||||
@@ -109,16 +111,16 @@ const refreshAssets = async () => {
|
||||
videoAssets.value = assets.filter((asset) => asset.name.endsWith('.mp4'))
|
||||
if (!videoAssets.value.length) {
|
||||
if (assets.length) {
|
||||
toast.warning('选择的文件夹中不包含MP4视频文件')
|
||||
toast.warning(t('videoManage.noMp4InFolder'))
|
||||
} else {
|
||||
toast.warning('选择的文件夹为空')
|
||||
toast.warning(t('videoManage.emptyFolder'))
|
||||
}
|
||||
} else {
|
||||
toast.success('素材读取成功')
|
||||
toast.success(t('videoManage.readSuccess'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
toast.error('素材读取失败,请检查文件夹是否存在')
|
||||
toast.error(t('videoManage.readFailed'))
|
||||
} finally {
|
||||
refreshAssetsLoading.value = false
|
||||
}
|
||||
@@ -128,11 +130,19 @@ refreshAssets()
|
||||
// 获取视频分镜随机素材片段
|
||||
const videoInfoList = ref<VideoInfo[]>([])
|
||||
const getVideoSegments = (options: { duration: number }) => {
|
||||
// 判断素材库是否满足时长要求
|
||||
if (videoInfoList.value.reduce((pre, cur) => pre + cur.duration, 0) < options.duration) {
|
||||
throw new Error('素材总时长不足')
|
||||
// 视频资源总时长
|
||||
const videeAssetsTotalDuration = videoInfoList.value.reduce((pre, cur) => pre + cur.duration, 0)
|
||||
|
||||
// 判断素材库视频时长是否过短
|
||||
if (videeAssetsTotalDuration < 1) {
|
||||
throw new Error(t('errors.assetsDurationInsufficient'))
|
||||
}
|
||||
|
||||
// 判断素材库是否满足TTS时长要求
|
||||
// if (videeAssetsTotalDuration < options.duration) {
|
||||
// throw new Error(t('errors.assetsDurationInsufficient'))
|
||||
// }
|
||||
|
||||
// 搜集随机素材片段
|
||||
const segments: Pick<RenderVideoParams, 'videoFiles' | 'timeRanges'> = {
|
||||
videoFiles: [],
|
||||
@@ -153,7 +163,7 @@ const getVideoSegments = (options: { duration: number }) => {
|
||||
|
||||
// 获取一个随机素材以及相关信息
|
||||
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]
|
||||
|
||||
// 删除已选素材
|
||||
@@ -180,8 +190,8 @@ const getVideoSegments = (options: { duration: number }) => {
|
||||
|
||||
// 处理最后一个片段时长小于最小片段时长情况
|
||||
if (options.duration - currentTotalDuration - randomSegmentDuration < minSegmentDuration) {
|
||||
if (randomSegmentDuration + minSegmentDuration < randomAssetInfo.duration) {
|
||||
randomSegmentDuration += minSegmentDuration
|
||||
if (options.duration - currentTotalDuration < randomAssetInfo.duration) {
|
||||
randomSegmentDuration = options.duration - currentTotalDuration
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
<template>
|
||||
<div class="h-0 flex-1 relative">
|
||||
<div class="absolute top-1/12 w-full flex justify-center cursor-default select-none">
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.None"> 空闲,可以开始合成 </v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.None">
|
||||
{{ t('render.status.idle') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.GenerateText" variant="elevated">
|
||||
正在使用 AI 大模型生成文案
|
||||
{{ t('render.status.generatingText') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SynthesizedSpeech" variant="elevated">
|
||||
正在使用 TTS 合成语音
|
||||
{{ t('render.status.synthesizingSpeech') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.SegmentVideo" variant="elevated">
|
||||
正在处理分镜素材
|
||||
{{ t('render.status.segmentingVideo') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Rendering" variant="elevated">
|
||||
正在渲染视频
|
||||
{{ t('render.status.rendering') }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="appStore.renderStatus === RenderStatus.Completed"
|
||||
variant="elevated"
|
||||
color="success"
|
||||
>
|
||||
渲染成功,可以开始下一个
|
||||
{{ t('render.status.success') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="appStore.renderStatus === RenderStatus.Failed" variant="elevated" color="error">
|
||||
渲染失败,请重新尝试
|
||||
{{ t('render.status.failed') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +48,7 @@
|
||||
prepend-icon="mdi-rocket-launch"
|
||||
@click="emit('renderVideo')"
|
||||
>
|
||||
开始合成
|
||||
{{ t('render.startRender') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
@@ -55,31 +57,36 @@
|
||||
prepend-icon="mdi-stop"
|
||||
@click="emit('cancelRender')"
|
||||
>
|
||||
停止合成
|
||||
{{ t('render.stopRender') }}
|
||||
</v-btn>
|
||||
<v-dialog v-model="configDialogShow" max-width="600" persistent>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" :disabled="taskInProgress"> 合成配置 </v-btn>
|
||||
<v-btn v-bind="activatorProps" :disabled="taskInProgress">
|
||||
{{ t('actions.config') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card prepend-icon="mdi-text-box-edit-outline" title="配置合成选项">
|
||||
<v-card
|
||||
prepend-icon="mdi-text-box-edit-outline"
|
||||
:title="t('dialogs.renderConfigTitle')"
|
||||
>
|
||||
<v-card-text>
|
||||
<div class="w-full flex gap-2 mb-4 items-center">
|
||||
<v-text-field
|
||||
label="导出视频宽度"
|
||||
:label="t('render.output.width')"
|
||||
v-model="config.outputSize.width"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="config.outputSize.height"
|
||||
label="导出视频高度"
|
||||
:label="t('render.output.height')"
|
||||
hide-details
|
||||
required
|
||||
></v-text-field>
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mb-4 items-center">
|
||||
<v-text-field
|
||||
label="导出文件名"
|
||||
:label="t('render.output.fileName')"
|
||||
v-model="config.outputFileName"
|
||||
hide-details
|
||||
required
|
||||
@@ -88,7 +95,7 @@
|
||||
<v-text-field
|
||||
class="w-[120px] flex-none"
|
||||
v-model="config.outputFileExt"
|
||||
label="导出格式"
|
||||
:label="t('render.output.format')"
|
||||
hide-details
|
||||
readonly
|
||||
required
|
||||
@@ -96,7 +103,7 @@
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mb-4 items-center">
|
||||
<v-text-field
|
||||
label="导出文件夹"
|
||||
:label="t('render.output.folder')"
|
||||
v-model="config.outputPath"
|
||||
hide-details
|
||||
readonly
|
||||
@@ -107,12 +114,12 @@
|
||||
prepend-icon="mdi-folder-open"
|
||||
@click="handleSelectOutputFolder"
|
||||
>
|
||||
选择
|
||||
{{ t('common.select') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="w-full flex gap-2 mb-2 items-center">
|
||||
<v-text-field
|
||||
label="背景音乐文件夹(.mp3格式,从中随机选取)"
|
||||
:label="t('render.bgmFolderLabel')"
|
||||
v-model="config.bgmPath"
|
||||
hide-details
|
||||
readonly
|
||||
@@ -124,17 +131,17 @@
|
||||
prepend-icon="mdi-folder-open"
|
||||
@click="handleSelectBgmFolder"
|
||||
>
|
||||
选择
|
||||
{{ t('common.select') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text="关闭" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||
<v-btn :text="t('common.close')" variant="plain" @click="handleCloseDialog"></v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
text="保存"
|
||||
:text="t('common.save')"
|
||||
variant="tonal"
|
||||
@click="handleSaveConfig"
|
||||
></v-btn>
|
||||
@@ -147,7 +154,7 @@
|
||||
<div class="w-full flex justify-center">
|
||||
<v-switch
|
||||
v-model="appStore.autoBatch"
|
||||
label="自动批量合成"
|
||||
:label="t('render.autoBatch')"
|
||||
color="indigo"
|
||||
density="compact"
|
||||
hide-details
|
||||
@@ -158,7 +165,7 @@
|
||||
|
||||
<div class="absolute bottom-2 w-full flex justify-center text-sm">
|
||||
<span class="text-indigo cursor-pointer select-none" @click="handleOpenHomePage">
|
||||
Powered by YILS(博客地址:https://yils.blog)
|
||||
{{ t('footer.poweredBy') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,9 +173,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRaw, nextTick, computed } from 'vue'
|
||||
import { useTranslation } from 'i18next-vue'
|
||||
import { RenderStatus, useAppStore } from '@/store'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'renderVideo'): void
|
||||
@@ -206,7 +215,7 @@ const handleSaveConfig = () => {
|
||||
// 选择文件夹
|
||||
const handleSelectOutputFolder = async () => {
|
||||
const folderPath = await window.electron.selectFolder({
|
||||
title: '选择视频导出文件夹',
|
||||
title: t('dialogs.selectOutputFolderTitle'),
|
||||
defaultPath: config.value.outputPath,
|
||||
})
|
||||
console.log('用户选择视频导出文件夹,绝对路径:', folderPath)
|
||||
@@ -216,7 +225,7 @@ const handleSelectOutputFolder = async () => {
|
||||
}
|
||||
const handleSelectBgmFolder = async () => {
|
||||
const folderPath = await window.electron.selectFolder({
|
||||
title: '选择背景音乐文件夹',
|
||||
title: t('dialogs.selectBgmFolderTitle'),
|
||||
defaultPath: config.value.bgmPath,
|
||||
})
|
||||
console.log('用户选择背景音乐文件夹,绝对路径:', folderPath)
|
||||
@@ -226,7 +235,7 @@ const handleSelectBgmFolder = async () => {
|
||||
}
|
||||
|
||||
const handleOpenHomePage = () => {
|
||||
window.electron.openExternal({ url: 'https://yils.blog' })
|
||||
window.electron.openExternal({ url: 'https://yils.blog/?ref=short-video-factory' })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -36,12 +36,14 @@ import VideoRender from './components/video-render.vue'
|
||||
|
||||
import { ref } from 'vue'
|
||||
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 } = useTranslation()
|
||||
|
||||
// 渲染合成视频
|
||||
const TextGenerateInstance = ref<InstanceType<typeof TextGenerate> | null>()
|
||||
@@ -49,29 +51,31 @@ const VideoManageInstance = ref<InstanceType<typeof VideoManage> | null>()
|
||||
const TtsControlInstance = ref<InstanceType<typeof TtsControl> | null>()
|
||||
const handleRenderVideo = async () => {
|
||||
if (!appStore.renderConfig.outputFileName) {
|
||||
toast.warning('请先配置导出文件名')
|
||||
toast.warning(t('errors.outputFileNameRequired'))
|
||||
return
|
||||
}
|
||||
if (!appStore.renderConfig.outputPath) {
|
||||
toast.warning('请先配置导出文件夹')
|
||||
toast.warning(t('errors.outputPathRequired'))
|
||||
return
|
||||
}
|
||||
if (!appStore.renderConfig.outputSize?.width || !appStore.renderConfig.outputSize?.height) {
|
||||
toast.warning('请先配置导出分辨率(宽高)')
|
||||
toast.warning(t('errors.outputSizeRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
let randomBgm: ListFilesFromFolderRecord | undefined = undefined
|
||||
try {
|
||||
const bgmList = await window.electron.listFilesFromFolder({
|
||||
folderPath: appStore.renderConfig.bgmPath.replace(/\\/g, '/'),
|
||||
})
|
||||
if (bgmList.length > 0) {
|
||||
randomBgm = random.choice(bgmList)
|
||||
if (appStore.renderConfig.bgmPath) {
|
||||
try {
|
||||
const bgmList = (await window.electron.listFilesFromFolder({
|
||||
folderPath: appStore.renderConfig.bgmPath.replace(/\\/g, '/'),
|
||||
})).filter((asset) => asset.name.endsWith('.mp3'))
|
||||
if (bgmList.length > 0) {
|
||||
randomBgm = random.choice(bgmList)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('获取背景音乐列表失败', error)
|
||||
toast.error(t('errors.bgmListFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('获取背景音乐列表失败', error)
|
||||
toast.error('获取背景音乐列表失败,请检查文件夹是否存在')
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -92,10 +96,10 @@ const handleRenderVideo = async () => {
|
||||
withCaption: true,
|
||||
})
|
||||
if (ttsResult?.duration === undefined) {
|
||||
throw new Error('语音合成失败,音频文件损坏')
|
||||
throw new Error(t('errors.ttsFailedCorrupt'))
|
||||
}
|
||||
if (ttsResult?.duration === 0) {
|
||||
throw new Error('语音时长为0秒,检查TTS语音合成配置及网络连接是否正常')
|
||||
throw new Error(t('errors.ttsZeroDuration'))
|
||||
}
|
||||
|
||||
// 获取视频片段
|
||||
@@ -132,11 +136,11 @@ const handleRenderVideo = async () => {
|
||||
appStore.renderConfig.outputFileExt,
|
||||
})
|
||||
|
||||
toast.success('视频合成成功')
|
||||
toast.success(t('success.renderSuccess'))
|
||||
appStore.updateRenderStatus(RenderStatus.Completed)
|
||||
|
||||
if (appStore.autoBatch) {
|
||||
toast.info('开始合成下一个')
|
||||
toast.info(t('info.batchNext'))
|
||||
TextGenerateInstance.value?.clearOutputText()
|
||||
handleRenderVideo()
|
||||
}
|
||||
@@ -146,9 +150,7 @@ const handleRenderVideo = async () => {
|
||||
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message || error?.error?.message
|
||||
toast.error(
|
||||
`视频合成失败,请检查各项配置是否正确\n${errorMessage ? '错误信息:“' + errorMessage + '”' : ''}`,
|
||||
)
|
||||
toast.error(`${t('errors.renderFailedPrefix')}${errorMessage ? '\n' + errorMessage : ''}`)
|
||||
appStore.updateRenderStatus(RenderStatus.Failed)
|
||||
}
|
||||
}
|
||||
@@ -173,7 +175,7 @@ const handleCancelRender = () => {
|
||||
break
|
||||
}
|
||||
appStore.updateRenderStatus(RenderStatus.None)
|
||||
toast.info('视频合成已终止')
|
||||
toast.info(t('info.renderCanceled'))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"~/*": ["./*"]
|
||||
|
||||
@@ -24,7 +24,7 @@ export default defineConfig({
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
right: '0',
|
||||
width: '120px',
|
||||
width: '168px',
|
||||
height: '35px',
|
||||
'-webkit-app-region': 'no-drag',
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'~': fileURLToPath(new URL('./', import.meta.url)),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user