diff --git a/OBFUSCATION_README.md b/OBFUSCATION_README.md new file mode 100644 index 0000000..4427e53 --- /dev/null +++ b/OBFUSCATION_README.md @@ -0,0 +1,153 @@ +# Renderer 代码混淆配置说明 + +## ⚠️ 白屏问题解决方案 + +如果使用 `npm run build:win:obfuscated` 出现白屏,这是因为之前的混淆配置过于激进。现在已修复并提供更安全的配置。 + +### 🛠️ 已修复的问题 +- **移除所有unsafe选项**: 避免破坏Vue响应式系统 +- **保留函数名和类名**: 确保Vue组件正常工作 +- **关闭属性名混淆**: 防止破坏Vue的内部机制 +- **保留更多关键标识符**: 包括Vue、Pinia、Naive UI相关的所有重要名称 + +## 🎯 推荐使用方案 + +### 安全混淆构建 (推荐) +```bash +npm run build:win:safe +``` +这个命令使用新的安全混淆配置,提供基础保护的同时确保应用正常运行。 + +### 普通构建 (无混淆) +```bash +npm run build:win +``` +如果不需要混淆,使用普通构建即可。 + +## 混淆功能特性 + +### 安全混淆级别 (OBFUSCATE=true) +- ✅ **变量名混淆**: 将局部变量名转换为短字符 +- ✅ **代码压缩**: 移除空格、换行、注释 +- ✅ **Console移除**: 移除console.log等调试输出 +- ✅ **文件名混淆**: 生成短哈希文件名 +- ✅ **保留Vue生态**: 完全兼容Vue、Naive UI、Pinia +- ❌ 属性名混淆: 已禁用,避免破坏响应式 +- ❌ Unsafe优化: 已禁用,确保稳定性 + +## 构建命令对比 + +| 命令 | 混淆级别 | 安全性 | 兼容性 | 推荐场景 | +|------|----------|--------|--------|----------| +| `npm run build:win` | 无 | 低 | 100% | 开发测试 | +| `npm run build:win:safe` | 安全混淆 | 中等 | 99% | **生产推荐** | + +## 验证构建结果 + +### 1. 检查应用启动 +```bash +npm run build:unpack:safe +npm start +``` + +### 2. 检查混淆效果 +查看 `out/renderer/assets/*.js` 文件: +- 变量名应该是 a, b, c 等短字符 +- 代码应该压缩在少数几行 +- 不应有console.log输出 + +### 3. 功能测试 +- 所有页面正常显示 +- Vue组件响应式正常 +- Naive UI组件正常工作 +- Electron API正常调用 + +## 安全保护措施 + +### 保留的重要标识符 +```javascript +// Vue 核心 +'Vue', 'vue', 'reactive', 'ref', 'computed', 'watch' + +// Vue Router +'router', 'route', 'useRouter', 'useRoute' + +// Pinia +'pinia', 'store', 'useStore', 'defineStore' + +// Naive UI +'naive', 'NaiveUi', 'useDialog', 'useMessage' + +// Electron +'ElectronAPI', 'ipcRenderer', 'contextBridge' +``` + +### 禁用的危险选项 +```javascript +// 这些选项会破坏Vue,已全部禁用 +unsafe: false, +unsafe_comps: false, +unsafe_Function: false, +unsafe_methods: false, +properties: false // 属性名混淆 +``` + +## 故障排除 + +### 如果仍然白屏 +1. **使用普通构建**: + ```bash + npm run build:win + ``` + +2. **检查控制台错误**: + 打开开发者工具查看具体错误信息 + +3. **分步测试**: + ```bash + npm run build:unpack:safe # 先构建不打包 + npm start # 测试是否正常 + ``` + +### 如果某些功能异常 +1. **检查是否有动态属性访问**: + 如 `obj[dynamicKey]` 可能需要特殊处理 + +2. **添加保留名称**: + 在 `reserved` 数组中添加相关标识符 + +3. **临时禁用混淆**: + ```bash + npm run build:win # 使用普通构建 + ``` + +## 技术细节 + +### 混淆配置要点 +```javascript +terserOptions: { + compress: { + drop_console: true, // 移除console + keep_fargs: true, // 保留函数参数(Vue需要) + keep_classnames: true, // 保留类名(Vue组件) + keep_fnames: true, // 保留函数名(Vue方法) + unsafe: false // 禁用unsafe优化 + }, + mangle: { + properties: false, // 不混淆属性名 + reserved: [...] // 大量保留名称 + } +} +``` + +### 为什么这样配置 +- **Vue响应式系统**依赖属性名不被改变 +- **组件系统**需要保留类名和函数名 +- **动态属性访问**在混淆后可能失效 +- **第三方库集成**需要保留特定标识符 + +## 结论 + +新的安全混淆配置在保护代码的同时确保应用稳定运行。虽然混淆强度相比之前有所降低,但对于大多数安全需求已经足够,且避免了白屏等兼容性问题。 + +**推荐使用**: `npm run build:win:safe` \ No newline at end of file diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 17692e7..1ccb159 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -6,53 +6,142 @@ import Components from 'unplugin-vue-components/vite' import tsconfigPaths from 'vite-tsconfig-paths' import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' -export default defineConfig({ - main: { - resolve: { - alias: { - '@': resolve('src'), - '@renderer': resolve('src/renderer/src') +export default defineConfig(({ command }) => { + const isProduction = command === 'build' + const enableObfuscation = process.env.OBFUSCATE === 'true' + + return { + main: { + resolve: { + alias: { + '@': resolve('src'), + '@renderer': resolve('src/renderer/src') + } + }, + plugins: [externalizeDepsPlugin(), bytecodePlugin(), tsconfigPaths()] + }, + preload: { + plugins: [externalizeDepsPlugin(), bytecodePlugin()], + resolve: { + alias: { + '@': resolve('src'), + '@renderer': resolve('src/renderer/src') + } } }, - plugins: [externalizeDepsPlugin(), bytecodePlugin(), tsconfigPaths()] - }, - preload: { - plugins: [externalizeDepsPlugin(), bytecodePlugin()], - resolve: { - alias: { - '@': resolve('src'), - '@renderer': resolve('src/renderer/src') + renderer: { + resolve: { + alias: { + '@': resolve('src'), + '@renderer': resolve('src/renderer/src') + } + }, + plugins: [ + bytecodePlugin(), + vue({ + template: { + compilerOptions: { + // 将webview标签标记为自定义元素 + isCustomElement: (tag) => ['webview'].includes(tag) + } + } + }), + AutoImport({ + imports: [ + 'vue', + { + 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'] + } + ] + }), + Components({ + resolvers: [NaiveUiResolver()] + }) + ], + build: { + // 为生产环境添加代码混淆和优化 + ...(isProduction ? { + minify: 'terser', + terserOptions: { + compress: { + // 安全的压缩选项 - 避免破坏Vue响应式系统 + drop_console: enableObfuscation, // 根据环境变量决定是否移除console + drop_debugger: true, // 移除debugger + pure_funcs: enableObfuscation ? ['console.log', 'console.info', 'console.debug', 'console.warn'] : [], + passes: 1, // 只进行一轮压缩,避免过度优化 + keep_fargs: true, // 保留函数参数,对Vue很重要 + // 关闭所有unsafe选项,确保Vue和响应式系统正常工作 + unsafe: false, + unsafe_comps: false, + unsafe_Function: false, + unsafe_math: false, + unsafe_symbols: false, + unsafe_methods: false, + unsafe_proto: false, + unsafe_regexp: false, + unsafe_undefined: false, + // 保留一些对Vue重要的功能 + keep_classnames: true, // 保留类名 + keep_fnames: true, // 保留函数名,对Vue组件很重要 + // 移除条件编译,避免影响动态代码 + global_defs: {} + }, + mangle: enableObfuscation ? { + // 更保守的混淆策略 + properties: false, // 完全关闭属性名混淆,避免破坏Vue + // 保留更多重要的标识符 + reserved: [ + // Vue 核心 + 'Vue', 'vue', 'VNode', 'Component', 'Directive', 'Plugin', 'App', 'app', + 'reactive', 'ref', 'computed', 'watch', 'watchEffect', 'onMounted', 'onUnmounted', + 'provide', 'inject', 'createApp', 'mount', 'unmount', 'nextTick', + // Vue Router + 'router', 'route', 'useRouter', 'useRoute', 'RouterView', 'RouterLink', + // Pinia + 'pinia', 'store', 'useStore', 'defineStore', 'storeToRefs', + // Naive UI 相关 + 'naive', 'NaiveUi', 'n-', 'NButton', 'NInput', 'NForm', 'NSelect', 'NModal', + 'NCard', 'NLayout', 'NSpace', 'NGrid', 'NGridItem', 'NIcon', 'NText', + 'useDialog', 'useMessage', 'useNotification', 'useLoadingBar', + // Electron 相关 + 'ElectronAPI', 'electron', 'ipcRenderer', 'contextBridge', 'electronAPI', + // 通用保留 + 'require', 'exports', 'module', '__dirname', '__filename', + 'window', 'document', 'global', 'process', 'Buffer', + // 保留以$开头的Vue特殊属性 + '$', '$emit', '$props', '$attrs', '$slots', '$refs', '$parent', '$root', + // 保留一些常用的方法名 + 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', + // 保留事件相关 + 'addEventListener', 'removeEventListener', 'dispatchEvent' + ] + } : false, + format: { + // 更安全的格式化选项 + comments: false, // 移除注释 + beautify: false, // 不美化代码 + // 使用更保守的格式化设置 + ascii_only: false, // 不强制ASCII,避免中文问题 + wrap_iife: false, // 不包装IIFE,避免作用域问题 + semicolons: true // 保留分号,确保代码正确性 + } + }, + rollupOptions: { + output: { + // 文件名混淆保持简单 + chunkFileNames: enableObfuscation ? + 'assets/c[hash:8].js' : + 'assets/[name]-[hash].js', + entryFileNames: enableObfuscation ? + 'assets/e[hash:8].js' : + 'assets/[name]-[hash].js', + assetFileNames: enableObfuscation ? + 'assets/a[hash:8].[ext]' : + 'assets/[name]-[hash].[ext]' + } + } + } : {}) } } - }, - renderer: { - resolve: { - alias: { - '@': resolve('src'), - '@renderer': resolve('src/renderer/src') - } - }, - plugins: [ - bytecodePlugin(), - vue({ - template: { - compilerOptions: { - // 将webview标签标记为自定义元素 - isCustomElement: (tag) => ['webview'].includes(tag) - } - } - }), - AutoImport({ - imports: [ - 'vue', - { - 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'] - } - ] - }), - Components({ - resolvers: [NaiveUiResolver()] - }) - ] } }) diff --git a/package-lock.json b/package-lock.json index d8e0a8a..64fa9af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "laitool-pro", - "version": "v1.0.0", + "version": "v4.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "laitool-pro", - "version": "v1.0.0", + "version": "v4.0.2", "hasInstallScript": true, "dependencies": { "@alicloud/alimt20181012": "^1.3.0", @@ -39,15 +39,20 @@ "@electron-toolkit/eslint-config-prettier": "3.0.0", "@electron-toolkit/eslint-config-ts": "^3.0.0", "@electron-toolkit/tsconfig": "^1.0.1", + "@rollup/plugin-terser": "^0.4.4", "@types/node": "^22.13.4", "@vitejs/plugin-vue": "^5.2.1", + "cross-env": "^10.0.0", "electron": "^34.2.0", "electron-builder": "^25.1.8", "electron-vite": "^3.0.0", "eslint": "^9.20.1", "eslint-plugin-vue": "^9.32.0", + "javascript-obfuscator": "^4.1.1", "naive-ui": "^2.41.0", "prettier": "^3.5.1", + "rollup-plugin-obfuscator": "^1.1.0", + "terser": "^5.44.0", "typescript": "^5.7.3", "unplugin-auto-import": "^19.1.2", "unplugin-vue-components": "^28.4.1", @@ -1006,6 +1011,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/win32-x64": { "version": "0.24.2", "cpu": [ @@ -1357,6 +1369,89 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@javascript-obfuscator/escodegen": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@javascript-obfuscator/escodegen/-/escodegen-2.3.0.tgz", + "integrity": "sha512-QVXwMIKqYMl3KwtTirYIA6gOCiJ0ZDtptXqAv/8KWLG9uQU2fZqTVy7a/A5RvcoZhbDoFfveTxuGxJ5ibzQtkw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@javascript-obfuscator/estraverse": "^5.3.0", + "esprima": "^4.0.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/@javascript-obfuscator/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@javascript-obfuscator/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@javascript-obfuscator/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@javascript-obfuscator/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@javascript-obfuscator/estraverse": { + "version": "5.4.0", + "resolved": "https://registry.npmmirror.com/@javascript-obfuscator/estraverse/-/estraverse-5.4.0.tgz", + "integrity": "sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "dev": true, @@ -1386,6 +1481,17 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "license": "MIT" @@ -1609,6 +1715,65 @@ "node": ">=18" } }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.37.0", "cpu": [ @@ -1722,6 +1887,13 @@ "version": "2.0.3", "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "dev": true, @@ -1745,6 +1917,13 @@ "version": "1.3.5", "license": "MIT" }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/web-bluetooth": { "version": "0.0.16", "dev": true, @@ -2354,7 +2533,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -2649,6 +2830,49 @@ "version": "2.0.1", "license": "Python-2.0" }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assert": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/assert/-/assert-2.0.0.tgz", + "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es6-object-assign": "^1.1.0", + "is-nan": "^1.2.1", + "object-is": "^1.0.1", + "util": "^0.12.0" + } + }, "node_modules/async": { "version": "3.2.6", "license": "MIT" @@ -2678,6 +2902,22 @@ "node": ">= 4.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "1.8.4", "license": "MIT", @@ -3039,6 +3279,25 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "license": "MIT", @@ -3050,6 +3309,23 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "dev": true, @@ -3092,6 +3368,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chance": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/chance/-/chance-1.1.9.tgz", + "integrity": "sha512-TfxnA/DcZXRTA4OekA2zL9GH8qscbbl6X0ZqU4tXhGveVY/mXWvEQLt5GwZcYXTEyEFflVtj+pG8nc8EwSm1RQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.6.0", "dev": true, @@ -3153,6 +3456,18 @@ "node": ">=8" } }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmmirror.com/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "dev": true, @@ -3540,6 +3855,24 @@ "node": ">= 10" } }, + "node_modules/cross-env": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.0.0.tgz", + "integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "dev": true, @@ -3553,6 +3886,16 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/crypto-js": { "version": "4.2.0", "license": "MIT" @@ -3682,8 +4025,8 @@ }, "node_modules/define-data-property": { "version": "1.1.4", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3698,8 +4041,8 @@ }, "node_modules/define-properties": { "version": "1.2.1", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -4219,6 +4562,13 @@ "license": "MIT", "optional": true }, + "node_modules/es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.24.2", "dev": true, @@ -4495,6 +4845,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "dev": true, @@ -4784,6 +5148,22 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "dev": true, @@ -5109,8 +5489,8 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -5366,6 +5746,13 @@ "node": ">= 4.5.0" } }, + "node_modules/inversify": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/inversify/-/inversify-6.0.1.tgz", + "integrity": "sha512-B3ex30927698TJENHR++8FfEaJGqoWOgI6ZY5Ht/nLUsFCwHn6akbwtnUAPCgUepAnTpe2qHxhDNjoKLyz6rgQ==", + "dev": true, + "license": "MIT" + }, "node_modules/ip-address": { "version": "9.0.5", "dev": true, @@ -5378,6 +5765,23 @@ "node": ">= 12" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -5395,6 +5799,26 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-ci": { "version": "3.0.1", "dev": true, @@ -5422,6 +5846,25 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "dev": true, @@ -5446,6 +5889,23 @@ "dev": true, "license": "MIT" }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "dev": true, @@ -5454,6 +5914,25 @@ "node": ">=0.12.0" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "license": "MIT", @@ -5464,6 +5943,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "dev": true, @@ -5556,6 +6051,129 @@ "node": "*" } }, + "node_modules/javascript-obfuscator": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/javascript-obfuscator/-/javascript-obfuscator-4.1.1.tgz", + "integrity": "sha512-gt+KZpIIrrxXHEQGD8xZrL8mTRwRY0U76/xz/YX0gZdPrSqQhT/c7dYLASlLlecT3r+FxE7je/+C0oLnTDCx4A==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-2-Clause", + "dependencies": { + "@javascript-obfuscator/escodegen": "2.3.0", + "@javascript-obfuscator/estraverse": "5.4.0", + "acorn": "8.8.2", + "assert": "2.0.0", + "chalk": "4.1.2", + "chance": "1.1.9", + "class-validator": "0.14.1", + "commander": "10.0.0", + "eslint-scope": "7.1.1", + "eslint-visitor-keys": "3.3.0", + "fast-deep-equal": "3.1.3", + "inversify": "6.0.1", + "js-string-escape": "1.0.1", + "md5": "2.3.0", + "mkdirp": "2.1.3", + "multimatch": "5.0.0", + "opencollective-postinstall": "2.0.3", + "process": "0.11.10", + "reflect-metadata": "0.1.13", + "source-map-support": "0.5.21", + "string-template": "1.0.0", + "stringz": "2.1.0", + "tslib": "2.5.0" + }, + "bin": { + "javascript-obfuscator": "bin/javascript-obfuscator" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/javascript-obfuscator" + } + }, + "node_modules/javascript-obfuscator/node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/javascript-obfuscator/node_modules/commander": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/javascript-obfuscator/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/javascript-obfuscator/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/javascript-obfuscator/node_modules/mkdirp": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-2.1.3.tgz", + "integrity": "sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/javascript-obfuscator/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "dev": true, @@ -5707,6 +6325,13 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.18", + "resolved": "https://registry.npmmirror.com/libphonenumber-js/-/libphonenumber-js-1.12.18.tgz", + "integrity": "sha512-k0pdkX8DXHqVrby7yJ23WBcHMCX1lhwvX/Uazh0vf3wfGQa0qDIyRB2Z2C01JREGGt8Assfwl1yZduq59OjXXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/local-pkg": { "version": "1.1.1", "dev": true, @@ -5937,6 +6562,18 @@ "node": ">= 0.4" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "1.1.0", "license": "MIT", @@ -6191,6 +6828,50 @@ "dev": true, "license": "MIT" }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/music-metadata": { "version": "7.14.0", "license": "MIT", @@ -6471,10 +7152,27 @@ "node": ">= 6" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" } @@ -6507,6 +7205,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "dev": true, + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -6766,6 +7474,16 @@ "node": ">=10.4.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.3", "funding": [ @@ -6982,6 +7700,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/rc": { "version": "1.2.8", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", @@ -7131,6 +7859,13 @@ } } }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/require-directory": { "version": "2.1.1", "dev": true, @@ -7278,6 +8013,20 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-obfuscator": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/rollup-plugin-obfuscator/-/rollup-plugin-obfuscator-1.1.0.tgz", + "integrity": "sha512-cMfQIKyGePlfHGGO+rSDhSATMBx7WWxXW/X66c53HylUE/owanTbG6nhttUQOkbdCiQH8tClskNEH/IPRoqZwA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "peerDependencies": { + "javascript-obfuscator": "*", + "rollup": "^2.56.3||^3.0.0||^4.0.0" + } + }, "node_modules/rollup/node_modules/@types/estree": { "version": "1.0.6", "devOptional": true, @@ -7323,6 +8072,24 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "license": "MIT", @@ -7382,11 +8149,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "dev": true, "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/sharp": { "version": "0.34.1", "hasInstallScript": true, @@ -7565,6 +8360,13 @@ "npm": ">= 3.0.0" } }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, "node_modules/socks": { "version": "2.8.4", "dev": true, @@ -7678,6 +8480,13 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-template": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/string-template/-/string-template-1.0.0.tgz", + "integrity": "sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "dev": true, @@ -7705,6 +8514,16 @@ "node": ">=8" } }, + "node_modules/stringz": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/stringz/-/stringz-2.1.0.tgz", + "integrity": "sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "dev": true, @@ -7997,6 +8816,32 @@ "version": "1.13.0", "license": "0BSD" }, + "node_modules/terser": { + "version": "5.44.0", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/text-hex": { "version": "1.0.0", "license": "MIT" @@ -8476,6 +9321,20 @@ "dev": true, "license": "(WTFPL OR MIT)" }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmmirror.com/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "license": "MIT" @@ -8491,6 +9350,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vdirs": { "version": "0.1.8", "dev": true, @@ -8842,6 +9711,28 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wide-align": { "version": "1.1.5", "dev": true, diff --git a/package.json b/package.json index b9f713d..fd955fd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "laitool-pro", "productName": "LaiToolPro", - "version": "v4.0.1", + "version": "v4.0.2", "description": "来推 Pro - 一款集音频处理、文案生成、图片生成、视频生成等功能于一体的多合一AI工具软件。", "main": "./out/main/index.js", "author": "xiangbei", @@ -15,11 +15,16 @@ "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", + "build:safe": "cross-env OBFUSCATE=true npm run typecheck && cross-env OBFUSCATE=true electron-vite build", "postinstall": "electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir", + "build:unpack:safe": "npm run build:safe && electron-builder --dir", "build:win": "npm run build && electron-builder --win", + "build:win:safe": "npm run build:safe && electron-builder --win", "build:mac": "npm run build && electron-builder --mac", - "build:linux": "npm run build && electron-builder --linux" + "build:mac:safe": "npm run build:safe && electron-builder --mac", + "build:linux": "npm run build && electron-builder --linux", + "build:linux:obfuscated": "npm run build:obfuscated && electron-builder --linux" }, "dependencies": { "@alicloud/alimt20181012": "^1.3.0", @@ -52,15 +57,20 @@ "@electron-toolkit/eslint-config-prettier": "3.0.0", "@electron-toolkit/eslint-config-ts": "^3.0.0", "@electron-toolkit/tsconfig": "^1.0.1", + "@rollup/plugin-terser": "^0.4.4", "@types/node": "^22.13.4", "@vitejs/plugin-vue": "^5.2.1", + "cross-env": "^10.0.0", "electron": "^34.2.0", "electron-builder": "^25.1.8", "electron-vite": "^3.0.0", "eslint": "^9.20.1", "eslint-plugin-vue": "^9.32.0", + "javascript-obfuscator": "^4.1.1", "naive-ui": "^2.41.0", "prettier": "^3.5.1", + "rollup-plugin-obfuscator": "^1.1.0", + "terser": "^5.44.0", "typescript": "^5.7.3", "unplugin-auto-import": "^19.1.2", "unplugin-vue-components": "^28.4.1", diff --git a/src/define/Tools/image.ts b/src/define/Tools/image.ts index ee9c828..d8b8633 100644 --- a/src/define/Tools/image.ts +++ b/src/define/Tools/image.ts @@ -158,7 +158,7 @@ export function GetImageTypeFromBase64(base64String: string): string { * @param url 本地文件路径或网络图片URL * @returns Promise 返回一个Promise,解析为包含MIME类型的base64字符串 */ -export function GetImageBase64(url: string): Promise { +export function GetImageBase64(url: string, noPrefix: boolean = false): Promise { if (!url) { return Promise.reject(t("{data} 不能为空", { data: 'URL' @@ -173,8 +173,12 @@ export function GetImageBase64(url: string): Promise { response.on('data', (chunk) => data.push(chunk)) response.on('end', () => { const buffer = Buffer.concat(data) + if (noPrefix) { + return resolve(buffer.toString('base64')) + } const base64Data = `data:${mimeType};base64,${buffer.toString('base64')}` resolve(base64Data) + }) }) .on('error', (err) => reject(err)) @@ -186,6 +190,9 @@ export function GetImageBase64(url: string): Promise { reject(err) } else { const mimeType = GetMimeType(url) + if (noPrefix) { + return resolve(data.toString('base64')) + } const base64Data = `data:${mimeType};base64,${data.toString('base64')}` resolve(base64Data) } @@ -371,7 +378,7 @@ export async function ImageSplit( if (blockWidth <= 0 || blockHeight <= 0) { throw new Error(t('图片分块尺寸计算错误')) } - + if (xOffset + blockWidth > metadata.width || yOffset + blockHeight > metadata.height) { throw new Error(t('图片分块超出边界')) } @@ -387,7 +394,7 @@ export async function ImageSplit( width: Math.max(1, Math.floor(blockWidth)), height: Math.max(1, Math.floor(blockHeight)) } - + // 再次验证边界 if (extractOptions.left + extractOptions.width > metadata.width) { extractOptions.width = metadata.width - extractOptions.left @@ -395,17 +402,17 @@ export async function ImageSplit( if (extractOptions.top + extractOptions.height > metadata.height) { extractOptions.height = metadata.height - extractOptions.top } - + // 使用 buffer 方式更安全,避免 sharp 直接写文件时的崩溃 const sharpInstance = sharp(inputPath) const extractedImage = sharpInstance.extract(extractOptions) - + // 先转为 buffer,再写入文件 const buffer = await extractedImage.png().toBuffer() - + // 确保输出文件的目录存在 await CheckFolderExistsOrCreate(path.dirname(outFile)) - + // 写入文件 await fs.promises.writeFile(outFile, buffer) } catch (extractError) { diff --git a/src/define/data/aiData/aiData.ts b/src/define/data/aiData/aiData.ts index 2374e86..5d5bd8d 100644 --- a/src/define/data/aiData/aiData.ts +++ b/src/define/data/aiData/aiData.ts @@ -1,20 +1,11 @@ import { t } from '@/i18n' -import { AIStoryboardMasterAIEnhance } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterAIEnhance' -import { AIStoryboardMasterGeneral } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterGeneral' -import { AIStoryboardMasterMJAncientStyle } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterMJAncientStyle' -import { AIStoryboardMasterOptimize } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterOptimize' -import { AIStoryboardMasterScenePrompt } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterScenePrompt' -import { AIStoryboardMasterSDEnglish } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterSDEnglish' -import { AIStoryboardMasterSingleFrame } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterSingleFrame' -import { AIStoryboardMasterSingleFrameWithCharacter } from './aiPrompt/bookStoryboardPrompt/aiStoryboardMasterSingleFrameWithCharacter' -import { AIStoryboardMasterSpecialEffects } from './aiPrompt/bookStoryboardPrompt/aitoryboardMasterSpecialEffects' export type AiInferenceModelModel = { value: string // AI选项值 label: string // AI选项标签 hasExample: boolean // 是否有示例 mustCharacter: boolean // 是否必须包含角色 - requestBody: OpenAIRequest.Request // AI请求体 + requestBody: string | OpenAIRequest.Request // AI请求体 allAndExampleContent: string | null // 所有和示例内容 } @@ -28,7 +19,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [ label: t('【LaiTool】场景提示大师(上下文-提示词不包含人物)'), hasExample: false, mustCharacter: false, - requestBody: AIStoryboardMasterScenePrompt, + requestBody: "AIStoryboardMasterScenePrompt", allAndExampleContent: null }, { @@ -36,7 +27,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [ label: t('【LaiTool】分镜大师-特效增强版(上下文-人物场景固定)'), hasExample: false, mustCharacter: true, - requestBody: AIStoryboardMasterSpecialEffects, + requestBody: "AIStoryboardMasterSpecialEffects", allAndExampleContent: null }, { @@ -44,7 +35,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [ label: t('【LaiTool】分镜大师-通用版(上下文-人物场景固定-类型推理)'), hasExample: false, mustCharacter: true, - requestBody: AIStoryboardMasterGeneral, + requestBody: "AIStoryboardMasterGeneral", allAndExampleContent: null }, { @@ -52,7 +43,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [ label: t('【LaiTool】分镜大师-全面版-AI增强(上下文-人物场景固定-单帧)'), hasExample: false, mustCharacter: true, - requestBody: AIStoryboardMasterAIEnhance, + requestBody: "AIStoryboardMasterAIEnhance", allAndExampleContent: null }, { @@ -60,7 +51,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [ label: t('【LaiTool】分镜大师-全能优化版(上下文-人物固定)'), hasExample: false, mustCharacter: true, - requestBody: AIStoryboardMasterOptimize, + requestBody: "AIStoryboardMasterOptimize", allAndExampleContent: null }, { @@ -68,7 +59,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [ label: t('【LaiTool】分镜大师-MJ超精细化版(上下文-人物场景固定)'), hasExample: false, mustCharacter: true, - requestBody: AIStoryboardMasterMJAncientStyle, + requestBody: "AIStoryboardMasterMJAncientStyle", allAndExampleContent: null }, { @@ -76,7 +67,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [ label: t('【LaiTool】分镜大师-SD英文版(上下文-人物场景固定-SD-英文提示词)'), hasExample: false, mustCharacter: true, - requestBody: AIStoryboardMasterSDEnglish, + requestBody: "AIStoryboardMasterSDEnglish", allAndExampleContent: null }, { @@ -84,7 +75,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [ label: t('【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物自动推理)'), hasExample: false, mustCharacter: false, - requestBody: AIStoryboardMasterSingleFrame, + requestBody: "AIStoryboardMasterSingleFrame", allAndExampleContent: null }, { @@ -92,7 +83,7 @@ export const aiOptionsData: AiInferenceModelModel[] = [ label: t('【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物场景固定)'), hasExample: false, mustCharacter: true, - requestBody: AIStoryboardMasterSingleFrameWithCharacter, + requestBody: "AIStoryboardMasterSingleFrameWithCharacter", allAndExampleContent: null } ] diff --git a/src/define/data/aiData/aiPrompt/bookStoryboardPrompt/index.ts b/src/define/data/aiData/aiPrompt/bookStoryboardPrompt/index.ts new file mode 100644 index 0000000..3bc2e32 --- /dev/null +++ b/src/define/data/aiData/aiPrompt/bookStoryboardPrompt/index.ts @@ -0,0 +1,39 @@ +import { t } from "@/i18n"; +import { AIStoryboardMasterAIEnhance } from "./aiStoryboardMasterAIEnhance"; +import { AIStoryboardMasterGeneral } from "./aiStoryboardMasterGeneral"; +import { AIStoryboardMasterMJAncientStyle } from "./aiStoryboardMasterMJAncientStyle"; +import { AIStoryboardMasterOptimize } from "./aiStoryboardMasterOptimize"; +import { AIStoryboardMasterScenePrompt } from "./aiStoryboardMasterScenePrompt"; +import { AIStoryboardMasterSDEnglish } from "./aiStoryboardMasterSDEnglish"; +import { AIStoryboardMasterSingleFrame } from "./aiStoryboardMasterSingleFrame"; +import { AIStoryboardMasterSingleFrameWithCharacter } from "./aiStoryboardMasterSingleFrameWithCharacter"; +import { AIStoryboardMasterSpecialEffects } from "./aitoryboardMasterSpecialEffects"; + +// 根据 value 返回对应的分镜预设请求体对象 +// value: 预设类型字符串 +// 返回: OpenAIRequest.Request 对象 +// 如果未找到对应类型会抛出错误(带有国际化提示) +export function GetAIPromptRequestBodyByValue(value: string): OpenAIRequest.Request { + switch (value) { + case "AIStoryboardMasterScenePrompt": + return AIStoryboardMasterScenePrompt; + case "AIStoryboardMasterSpecialEffects": + return AIStoryboardMasterSpecialEffects; + case "AIStoryboardMasterGeneral": + return AIStoryboardMasterGeneral; + case "AIStoryboardMasterAIEnhance": + return AIStoryboardMasterAIEnhance; + case "AIStoryboardMasterOptimize": + return AIStoryboardMasterOptimize; + case "AIStoryboardMasterMJAncientStyle": + return AIStoryboardMasterMJAncientStyle; + case "AIStoryboardMasterSDEnglish": + return AIStoryboardMasterSDEnglish; + case "AIStoryboardMasterSingleFrame": + return AIStoryboardMasterSingleFrame; + case "AIStoryboardMasterSingleFrameWithCharacter": + return AIStoryboardMasterSingleFrameWithCharacter; + default: + throw new Error(t('未找到对应的分镜预设的请求数据,请检查')) + } +} \ No newline at end of file diff --git a/src/define/data/apiData.ts b/src/define/data/apiData.ts index 43b07b0..601a630 100644 --- a/src/define/data/apiData.ts +++ b/src/define/data/apiData.ts @@ -1,6 +1,34 @@ import { t } from "@/i18n" -export const apiDefineData = [ +// API MJ URL 配置接口 +interface ApiMjUrl { + imagine: string; + describe: string; + video?: string; + update_file: string; + once_get_task: string; + query_url?: string; +} + +// API D3 URL 配置接口 +interface ApiD3Url { + image: string; +} + +// API 定义数据项接口 +export interface APIProviderDataItem { + label: string; + value: string; + id?: string; + gpt_url?: string; + base_url: string; + mj_url?: ApiMjUrl; + d3_url?: ApiD3Url; + buy_url?: string; + isPackage?: boolean; +} + +export const apiDefineData: APIProviderDataItem[] = [ { label: t('LAI API - 香港'), value: 'b44c6f24-59e4-4a71-b2c7-3df0c4e35e65', diff --git a/src/define/data/softwareData.ts b/src/define/data/softwareData.ts index cf9def2..3100f4a 100644 --- a/src/define/data/softwareData.ts +++ b/src/define/data/softwareData.ts @@ -30,8 +30,8 @@ interface ISoftwareData { } export const SoftwareData: ISoftwareData = { - version: 'V4.0.1', - date: '2025-09-21', + version: 'V4.0.2', + date: '2025-09-23', systemInfo: { documentationUrl: 'https://rvgyir5wk1c.feishu.cn/wiki/WdaWwAfDdiLOnjkywIgcaQoKnog', updateUrl: 'https://pvwu1oahp5m.feishu.cn/docx/CAjGdTDlboJ3nVx0cQccOuNHnvd', diff --git a/src/define/enum/bookEnum.ts b/src/define/enum/bookEnum.ts index aecb6d0..9d28471 100644 --- a/src/define/enum/bookEnum.ts +++ b/src/define/enum/bookEnum.ts @@ -115,6 +115,8 @@ export enum BookBackTaskType { LUMA_VIDEO = 'luma_video', // kling 生成视频 KLING_VIDEO = 'kling_video', + // kling 视频拓展 + KLING_VIDEO_EXTEND = 'kling_video_extend', // MJ Video MJ_VIDEO = 'mj_video', // MJ VIDEO EXTEND 视频拓展 diff --git a/src/define/enum/softwareEnum.ts b/src/define/enum/softwareEnum.ts index cb50edd..7d576a1 100644 --- a/src/define/enum/softwareEnum.ts +++ b/src/define/enum/softwareEnum.ts @@ -68,6 +68,7 @@ export enum ResponseMessageType { RUNWAY_VIDEO = 'RUNWAY_VIDEO', // Runway生成视频 LUMA_VIDEO = 'LUMA_VIDEO', // Luma生成视频 KLING_VIDEO = 'KLING_VIDEO', // Kling生成视频 + KLING_VIDEO_EXTEND = 'KLING_VIDEO_EXTEND', // Kling生成视频拓展 MJ_VIDEO = 'MJ_VIDEO', // MJ生成视频 MJ_VIDEO_EXTEND = 'MJ_VIDEO_EXTEND', // MJ生成视频拓展 VIDEO_SUCESS = 'VIDEO_SUCESS' //视频生成成功 diff --git a/src/define/enum/video.ts b/src/define/enum/video.ts index fabc1d8..6218254 100644 --- a/src/define/enum/video.ts +++ b/src/define/enum/video.ts @@ -11,6 +11,8 @@ export enum ImageToVideoModels { LUMA = 'LUMA', /** 可灵生成视频 */ KLING = 'KLING', + /** 可灵视频拓展 */ + KLING_VIDEO_EXTEND = 'KLING_VIDEO_EXTEND', /** Pika 生成视频 */ PIKA = 'PIKA', /** MJ 图转视频 */ @@ -19,6 +21,7 @@ export enum ImageToVideoModels { MJ_VIDEO_EXTEND = 'MJ_VIDEO_EXTEND' } + export const MappingTaskTypeToVideoModel = (type: BookBackTaskType | string) => { switch (type) { case BookBackTaskType.LUMA_VIDEO: @@ -27,6 +30,8 @@ export const MappingTaskTypeToVideoModel = (type: BookBackTaskType | string) => return ImageToVideoModels.RUNWAY case BookBackTaskType.KLING_VIDEO: return ImageToVideoModels.KLING + case BookBackTaskType.KLING_VIDEO_EXTEND: + return ImageToVideoModels.KLING_VIDEO_EXTEND case BookBackTaskType.MJ_VIDEO: return ImageToVideoModels.MJ_VIDEO case BookBackTaskType.MJ_VIDEO_EXTEND: @@ -103,6 +108,8 @@ export enum VideoModel { export enum VideoStatus { /** 等待 */ WAIT = 'wait', + /** 提交成功 */ + SUBMITTED = 'submitted', /** 处理中 */ PROCESSING = 'processing', /** 完成 */ @@ -149,6 +156,12 @@ export enum RunwaySeconds { //#region 可灵相关 + +/** + * 可灵生成视频的模式 + * - std:高性能 + * - pro:高表现 + */ export enum KlingMode { /** 高性能 */ STD = 'std', @@ -156,6 +169,171 @@ export enum KlingMode { PRO = 'pro' } +/** + * 获取可灵生成模式的标签 + * + * @param mode 可灵生成模式枚举值或字符串 + * @returns 返回对应的中文标签 + */ +export function GetKlingModeLabel(mode: KlingMode | string) { + switch (mode) { + case KlingMode.STD: + return t('高性能 (std)') + case KlingMode.PRO: + return t('高表现 (pro)') + default: + return t('未知') + } +} + +/** + * 获取可灵生成模式的选项列表 + * + * @returns 返回包含标签和值的选项数组,用于下拉选择框等UI组件 + */ +export function GetKlingModeOptions() { + return [ + { + label: GetKlingModeLabel(KlingMode.STD), + value: KlingMode.STD + }, + { + label: GetKlingModeLabel(KlingMode.PRO), + value: KlingMode.PRO + } + ] +} + + +/** + * 可灵生成视频的时长 + * - '5':5秒 + * - '10':10秒 + */ +export enum KlingDuration { + FIVE = 5, + TEN = 10 +} + +/** + * 获取可灵视频时长的标签 + * + * @param duration 可灵视频时长枚举值或字符串 + * @returns 返回对应的中文标签 + */ +export function GetKlingDurationLabel(duration: KlingDuration | string) { + switch (duration) { + case KlingDuration.FIVE: + return t('5秒') + case KlingDuration.TEN: + return t('10秒') + default: + return t('未知') + } +} + +/** + * 获取可灵视频时长的选项列表 + * + * @returns 返回包含标签和值的选项数组,用于下拉选择框等UI组件 + */ +export function GetKlingDurationOptions() { + return [ + { + label: GetKlingDurationLabel(KlingDuration.FIVE), + value: KlingDuration.FIVE + }, + { + label: GetKlingDurationLabel(KlingDuration.TEN), + value: KlingDuration.TEN + } + ] +} + + +/** + * 可灵模型名称 + * - kling-v1:V1 版本 + * - kling-v1-5:V1.5 版本 + * - kling-v1-6:V1.6 版本 + * - kling-v2-master:V2 master 版本 + * - kling-v2-1:V2.1 版本 + * - kling-v2-1-master:V2.1 master 版本 + */ +export enum KlingModelName { + /** V1 版本 */ + KLING_V1 = 'kling-v1', + /** V1.5 版本 */ + KLING_V1_5 = 'kling-v1-5', + /** V1.6 版本 */ + KLING_V1_6 = 'kling-v1-6', + /** V2 master 版本 */ + KLING_V2_MASTER = 'kling-v2-master', + /** V2.1 版本 */ + KLING_V2_1 = 'kling-v2-1', + /** V2.1 master 版本 */ + KLING_V2_1_MASTER = 'kling-v2-1-master' +} + +/** + * 获取可灵模型名称的标签 + * + * @param modelName 可灵模型名称枚举值或字符串 + * @returns 返回对应的中文标签 + */ +export function GetKlingModelNameLabel(modelName: KlingModelName | string) { + switch (modelName) { + case KlingModelName.KLING_V1: + return t('Kling V1') + case KlingModelName.KLING_V1_5: + return t('Kling V1.5') + case KlingModelName.KLING_V1_6: + return t('Kling V1.6') + case KlingModelName.KLING_V2_MASTER: + return t('Kling V2 Master') + case KlingModelName.KLING_V2_1: + return t('Kling V2.1') + case KlingModelName.KLING_V2_1_MASTER: + return t('Kling V2.1 Master') + default: + return t('未知') + } +} + +/** + * 获取可灵模型名称的选项列表 + * + * @returns 返回包含标签和值的选项数组,用于下拉选择框等UI组件 + */ +export function GetKlingModelNameOptions() { + return [ + { + label: GetKlingModelNameLabel(KlingModelName.KLING_V1), + value: KlingModelName.KLING_V1 + }, + { + label: GetKlingModelNameLabel(KlingModelName.KLING_V1_5), + value: KlingModelName.KLING_V1_5 + }, + { + label: GetKlingModelNameLabel(KlingModelName.KLING_V1_6), + value: KlingModelName.KLING_V1_6 + }, + { + label: GetKlingModelNameLabel(KlingModelName.KLING_V2_MASTER), + value: KlingModelName.KLING_V2_MASTER + }, + { + label: GetKlingModelNameLabel(KlingModelName.KLING_V2_1), + value: KlingModelName.KLING_V2_1 + }, + { + label: GetKlingModelNameLabel(KlingModelName.KLING_V2_1_MASTER), + value: KlingModelName.KLING_V2_1_MASTER + } + ] +} + //#endregion //#region MJ Video diff --git a/src/define/ipcDefineString/subDefineString/bookDefineString.ts b/src/define/ipcDefineString/subDefineString/bookDefineString.ts index 9497a01..75ab7da 100644 --- a/src/define/ipcDefineString/subDefineString/bookDefineString.ts +++ b/src/define/ipcDefineString/subDefineString/bookDefineString.ts @@ -162,9 +162,12 @@ const BOOK = { UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE: 'UPDATE_BOOK_TASK_DETAIL_VIDEO_MESSAGE', /** MJ VIDEO 图转视频返回前端数据任务 */ - MJ_VIDEO_TO_VIDEO_RETURN: 'MJ_VIDEO_TO_VIDEO_RETURN' + MJ_VIDEO_TO_VIDEO_RETURN: 'MJ_VIDEO_TO_VIDEO_RETURN', + + /** Kling 图转视频返回前端数据任务 */ + KLING_IMAGE_TO_VIDEO_RETURN: 'KLING_IMAGE_TO_VIDEO_RETURN', + - //#endregion } diff --git a/src/define/model/book/bookTaskDetail.d.ts b/src/define/model/book/bookTaskDetail.d.ts index ca26c34..c8d09c8 100644 --- a/src/define/model/book/bookTaskDetail.d.ts +++ b/src/define/model/book/bookTaskDetail.d.ts @@ -1,6 +1,8 @@ import { ImageToVideoModels, + KlingDuration, KlingMode, + KlingModelName, MJVideoBatchSize, MJVideoType, RunawayModel, @@ -71,16 +73,59 @@ declare namespace BookTaskDetail { request_model?: string // 请求的模型,快速还是慢速 } + /** + * Kling 合成视频参数 + */ type klingOptions = { - model?: string // 模型(kling-v1) - image: string // 图片地址,必须,支持Base64编码或图片URL,支持.jpg / .jpeg / .png格式,大小不能超过10MB,分辨率不小于300*300px - image_tail?: string // 尾帧图片地址,支持Base64编码或图片URL,支持.jpg / .jpeg / .png格式,大小不能超过10MB,分辨率不小于300*300px - prompt?: string // 提示词,正向文本提示, 可选,不能超过500个字符 - negative_prompt?: string // 负面提示,负向文本提示,可选,不能超过200个字符 - cfg_scale?: number // 提示词相关性,可选,范围0-1 - mode?: KlingMode // 生成视频的模式,可选,枚举值:std(高性能)或 pro(高表现) - duration?: RunwaySeconds // 生成视频时长,单位秒,可选,枚举值:5,10(包含尾帧的请求仅支持5秒) - callback_url?: string // 回调地址,可选,生成视频完成后,会向该地址发送通知 + /** + * 模型名称,可选,枚举值: + * - kling-v1 + * - kling-v1-5 + * - kling-v1-6 + * - kling-v2-master + * - kling-v2-1 + * - kling-v2-1-master + * 默认值:kling-v1 + */ + model_name?: KlingModelName; + /** + * 参考图像,必须,支持Base64编码或图片URL,支持.jpg / .jpeg / .png格式,大小不能超过10MB,分辨率不小于300*300px + */ + image: string; + /** + * 参考图像 - 尾帧控制,可选,支持Base64编码或图片URL,支持.jpg / .jpeg / .png格式,大小不能超过10MB,分辨率不小于300*300px + */ + image_tail?: string; + /** + * 正向文本提示,可选,不能超过500个字符 + */ + prompt?: string; + /** + * 负向文本提示,可选,不能超过200个字符 + */ + negative_prompt?: string; + /** + * 生成视频的自由度,可选,值越大相关性越强,取值范围:[0, 1] + */ + cfg_scale?: number; + /** + * 生成视频的模式,可选,枚举值:std(高性能)或 pro(高表现) + */ + mode?: KlingMode; + /** + * 生成视频时长,单位秒,可选,枚举值:'5' | '10'(包含尾帧的请求仅支持5秒) + */ + duration?: KlingDuration; + + /** + * 视频ID,扩展视频时使用 + */ + video_id?: string; + + /** + * 任务ID,扩展视频时使用 + */ + task_id?: string; } interface MjVideoOptions { diff --git a/src/define/model/setting.d.ts b/src/define/model/setting.d.ts index 7606818..9254454 100644 --- a/src/define/model/setting.d.ts +++ b/src/define/model/setting.d.ts @@ -2,6 +2,7 @@ import { ImageToVideoCategory } from '@/define/data/imageData' import { ImageGenerateMode, MJRobotType, MJSpeed } from '../data/mjData' import { JianyingKeyFrameEnum } from '../enum/jianyingEnum' import { ImageToVideoModels } from '@/define/enum/video' +import { APIProviderDataItem } from '../data/apiData' declare namespace SettingModal { //#region 基础设置 @@ -194,6 +195,16 @@ declare namespace SettingModal { translationModel: string } + /** + * AI 推理设置与提供商组合接口 + * 继承 InferenceAISettings 的所有属性,并添加了 API 提供商的详细信息 + * 用于在需要同时获取推理设置和对应 API 提供商配置的场景 + */ + interface InferenceAISettingAndProvider extends InferenceAISettings { + /** API 提供商详细配置项 - 包含完整的 API 提供商信息 */ + apiProviderItem: APIProviderDataItem + } + //#endregion //#region SD设置 diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 8e766b9..b862d63 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -58,6 +58,7 @@ export default { "未知错误": 'Unknown Error', "未知类型": 'Unknown Type', "未知操作": 'Unknown Operation', + "未知状态": "Unknown Status", "下载成功": 'Download Successful', "下载失败": 'Download Failed', "页面不存在": "Page Not Found", @@ -244,9 +245,10 @@ export default { "当前分镜数据的MJ图转视频参数为空或参数校验失败,请检查": "Current storyboard data MJ image-to-video parameters are empty or validation failed, please check", '当前Midjourney模式不支持视频生成功能,请更换为MJ API或本地代理模式后重试!': 'Current Midjourney mode does not support video generation, please switch to MJ API or local proxy mode and try again!', 'Midjourney图转视频任务执行失败,失败信息如下:{error}': 'Midjourney image-to-video task execution failed, error details: {error}', + "图转视频任务执行完成!": "Image-to-video task completed!", 'Midjourney图转视频任务执行完成。': 'Midjourney image-to-video task execution completed.', 'Midjourney图转视频任务执行中...': 'Midjourney image-to-video task executing...', - '已成功提交Midjourney图转视频任务,任务ID:{taskId}': 'Successfully submitted Midjourney image-to-video task, Task ID: {taskId}', + '已成功提交{type}图转视频任务,任务ID:{taskId}': 'Successfully submitted {type} image-to-video task, task ID: {taskId}', "小说批次任务的分镜数据的转视频配置为空,请检查": "Video conversion configuration for storyboard data of novel batch task is empty, please check", "分镜的图片没有全部出完,不能继续该操作!!": "Storyboard images are not all generated, cannot continue this operation!!", "分镜 {name} 图片在本地未找到,不能继续该操作,请检查对应分镜的图片路径是否正确": "Storyboard {name} image not found locally, cannot continue this operation, please check if the corresponding storyboard image path is correct", @@ -311,8 +313,16 @@ export default { '同步主图信息失败,{error}': 'Failed to sync main image information, {error}', '该操作会将当前批次的所有分镜的提示词全部重置为空,此操作不可撤销,重置的数据不可恢复,是否继续?': 'This operation will reset all storyboards\' prompts in the current batch to empty. This action is irreversible, and reset data cannot be recovered. Do you want to continue?', '正在执行重置提示词任务,请稍等...': 'Resetting prompts, please wait...', - "重置提示词失败,{error}": "重置提示词失败,{error}", + "重置提示词失败,{error}": "Reset prompts failed, {error}", "重置提示词成功": "Reset prompts successfully", + "该操作会选择 TXT 文件进行导入提示词,\n\n提示词文件格式要求:\n每行一个提示词,顺序和当前分镜顺序一致,\n如果某个分镜不需要导入提示词,可以留空该行,\n超出分镜的提示词会被删除,不足则只导入文本中有的提示词数据\n\n是否继续?": "This operation will select a TXT file to import prompts,\n\nPrompt file format requirements:\nOne prompt per line, in the same order as the current storyboard,\nIf a storyboard does not need a prompt, leave that line empty,\nPrompts exceeding the number of storyboards will be deleted, insufficient prompts will only import the available prompt data\n\nDo you want to continue?", + "导入提示词失败,{error}": "Import prompts failed, {error}", + "导入提示词成功": "Import prompts successfully", + "导入的提示词文件内容为空": "Imported prompt file content is empty", + '导入第 {line} 行提示词失败,{error}': 'Failed to import prompt on line {line}, {error}', + "正在批量应用当前设置...": "Batch applying current settings...", + "将当前转视频的基础设置批量应用到所有的分镜中": "Batch apply current video conversion basic settings to all storyboards", + "应用设置": "Apply Settings", //#endregion //#region 出图 @@ -978,6 +988,7 @@ export default { "AI处理文案成功": "AI content processing successful", "AI处理文案失败,{error}": "AI content processing failed, {error}", "检测系统当前的语言已修改,继续执行会刷新当前页面并应用当前语言,是否继续?": 'System language has been modified, continuing will refresh the current page and apply the current language, continue?', + "当前API提供商数据不存在,请检查数据是否正确": "Current API provider data does not exist, please check if the data is correct", //#endregion //#region 预设 @@ -1183,6 +1194,8 @@ export default { '场景分析': 'Scene Analysis', '推理所有提示词': 'Infer All Prompts', '推理空白分镜提示词': 'Infer Blank Storyboard Prompts', + "重置所有提示词": "Reset All Prompts", + "导入提示词": "Import Prompts", '生成所有图片': 'Generate All Images', '生成未生成图片分镜': 'Generate Un-generated Image Storyboards', '生成失败图片分镜': 'Generate Failed Image Storyboards', @@ -1545,6 +1558,10 @@ export default { '7. 需要在外部手动选择需要的{type}数据时,请点击“{button}” 按钮进行导入到标签集中': '7. When you need to manually select the required {type} data externally, please click the "{button}" button to import it into the tag set', '即将开始自动推理,该操作会将之前的 {type} 数据覆盖,是否继续?': 'About to start automatic inference. This operation will overwrite previous {type} data. Continue?', '正在推理,请稍等...': 'Inferring, please wait...', + "通用前/后缀": "General Prefix/Suffix", + "提示词前后缀设置": "Prompt Prefix/Suffix Settings", + "通用前缀": "General Prefix", + "通用后缀": "General Suffix", //#endregion //#region 转视频 @@ -1602,6 +1619,51 @@ export default { "当前批次任务已经开启图转视频,是否直接跳转到图文转视频界面?": "Current batch task has enabled image-to-video conversion, do you want to jump directly to the image-to-video interface?", "正在跳转到图文转视频界面...": "Jumping to image-to-video interface...", "已取消跳转,你可以在转视频模块中查看该任务": "Jump cancelled, you can view the task in the video conversion module", + '是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置:视频类型(SD/HD),运动变化(Motion),批次数据(Batch),视频原始(Raw),首尾循环(Loop) \n\n批量应用后,其余分镜的上述基础设置会被替换为当前分镜的数据,是否继续?': 'Do you want to apply the current storyboard settings to all other storyboards in batch?\n\nSynchronized settings: Video Type (SD/HD), Motion, Batch Data, Raw Video, Loop\n\nAfter batch application, the above basic settings of other storyboards will be replaced with the current storyboard data. Continue?', + "批量应用当前设置失败,{error}": "Failed to batch apply current settings, {error}", + "批量应用当前设置成功!": "Successfully batch applied current settings!", + "配置验证失败": "Configuration validation failed", + + "可灵图转视频任务失败,失败信息:{error}": "Kling image-to-video task failed, error details: {error}", + "Kling图转视频任务完成!": "Kling image-to-video task completed!", + "未找到有效的API地址": "No valid API address found", + "请先配置AI推理的API密钥": "Please configure the API key for AI inference first", + "当前分镜数据的可灵图转视频参数为空或参数校验失败,请检查": "The Kling image-to-video parameters for the current storyboard data are empty or validation failed, please check", + "当前分镜数据的图片地址为空,请检查": "The image URL for the current storyboard data is empty, please check", + "视频ID数量与视频链接数量不匹配": "The number of video IDs does not match the number of video links", + "未找到有效的GPT API地址": "No valid GPT API address found", + "未知的视频生成方式,请检查": "Unknown video generation method, please check", + "当前分镜数据的可灵视频ID为空,请检查": 'The Kling video ID for the current storyboard data is empty, please check', + "当前分镜数据的可灵任务ID为空,请检查": "The Kling task ID for the current storyboard data is empty, please check", + "可灵视频延长任务完成!": "Kling video extension task completed!", + "可灵视频延长任务失败,失败信息:{error}": "Kling video extension task failed, error details: {error}", + "可灵图转视频任务执行中...": "Kling image-to-video task in progress...", + "可灵视频延长任务正在执行中...": "Kling video extension task in progress...", + "参考图像": "Reference Image", + "正向文本提示,可选,不能超过2500个字符": "Positive text prompt, optional, cannot exceed 2500 characters", + "负向文本提示,可选,不能超过2500个字符": "Negative text prompt, optional, cannot exceed 2500 characters", + "自由度": "Degree of Freedom", + '0-1之间': "Between 0-1", + "生成视频的自由度

值越大,模型自由度越小
与用户输入的提示词相关性越强

取值范围:[0, 1]": "Degree of freedom for video generation

The larger the value, the smaller the model's degree of freedom
The stronger the correlation with user input prompts

Range: [0, 1]", + "生成模式": "Generation Mode", + "生成视频的模式

枚举值:std,pro
其中std:标准模式(标准),基础模式,性价比高
其中pro:专家模式(高品质),高表现模式,生成视频质量更佳": "Video generation mode

Enum values: std, pro
Where std: Standard mode (standard), basic mode, cost-effective
Where pro: Expert mode (high quality), high-performance mode, better video quality", + "视频时长": "Video Duration", + '生成视频时长,单位s(5秒或10秒)': 'Video duration in seconds (5 or 10 seconds)', + "视频延长": "Video Extend", + "选择已有的视频任务作为延长的基础": "Select an existing video task as the basis for extension", + "视频ID": "Video ID", + "支持通过文本、图片和视频延长生成的视频的ID(原视频不能超过3分钟)": "Supports the ID of videos generated through text, image, and video extension (original video cannot exceed 3 minutes)", + "对应于视频ID的任务ID,通常在选择视频后自动填充": "Corresponding task ID for the video ID, usually auto-filled after selecting a video", + "是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置:模型名称(Model Name),生成模式(Mode),视频时长(Duration),自由度(CFG Scale) \n\n批量应用后,其余分镜的上述基础设置会被替换为当前分镜的数据,是否继续?": "Do you want to apply the current storyboard settings to all other storyboards in batch?\n\nSynchronized settings: Model Name, Mode, Duration, CFG Scale\n\nAfter batch application, the above basic settings of other storyboards will be replaced with the current storyboard data. Continue?", + "请选择一个已有的视频任务作为延长的基础": "Please select an existing video task as the basis for extension", + "父任务选择成功,视频ID已更新为: {videoId}": "Parent task selected successfully, video ID updated to: {videoId}", + "高性能 (std)": "High Performance (std)", + "高表现 (pro)": "High Performance (pro)", + "选择Video": "Select Video", + "必须

• 支持格式:.jpg/.jpeg/.png
• 文件大小:不超过10MB
• 分辨率:不小于300*300px
• 宽高比:1:2.5 ~ 2.5:1之间": "必须

• 支持格式:.jpg/.jpeg/.png
• 文件大小:不超过10MB
• 分辨率:不小于300*300px
• 宽高比:1:2.5 ~ 2.5:1之间", + "参考图像 - 尾帧控制": "参考图像 - 尾帧控制", + '可选

• 支持格式:.jpg/.jpeg/.png
• 文件大小:不超过10MB
• 分辨率:不小于300*300px
• 宽高比:1:2.5 ~ 2.5:1之间': '可选

• 支持格式:.jpg/.jpeg/.png
• 文件大小:不超过10MB
• 分辨率:不小于300*300px
• 宽高比:1:2.5 ~ 2.5:1之间', + //#endregion //#region MJ '基本信息': 'Basic Information', @@ -1721,7 +1783,6 @@ export default { '视频生成时长为5秒,但并非仅限于此。视频制作完成后,您可以在当前界面为选定的适配进行延长!': 'Video generation duration is 5 seconds, but not limited to this. After video production is completed, you can extend the selected adaptation in the current interface!', '您可以随意将视频延长最多 4 次,每次延长 4 秒,直至达到 21 秒(即可用的最大长度)。': 'You can freely extend the video up to 4 times, each extension adding 4 seconds, until reaching 21 seconds (the maximum available length).', - //#endregion //#endregion //#region 文案处理 diff --git a/src/i18n/locales/zh-cn.ts b/src/i18n/locales/zh-cn.ts index 1bdd08b..47b5ae8 100644 --- a/src/i18n/locales/zh-cn.ts +++ b/src/i18n/locales/zh-cn.ts @@ -58,6 +58,7 @@ export default { "未知错误": '未知错误', "未知类型": '未知类型', "未知操作": '未知操作', + "未知状态": "未知状态", "下载成功": '下载成功', "下载失败": '下载失败', "页面不存在": "页面不存在", @@ -244,9 +245,10 @@ export default { "当前分镜数据的MJ图转视频参数为空或参数校验失败,请检查": "当前分镜数据的MJ图转视频参数为空或参数校验失败,请检查", '当前Midjourney模式不支持视频生成功能,请更换为MJ API或本地代理模式后重试!': '当前Midjourney模式不支持视频生成功能,请更换为MJ API或本地代理模式后重试!', 'Midjourney图转视频任务执行失败,失败信息如下:{error}': 'Midjourney图转视频任务执行失败,失败信息如下:{error}', + "图转视频任务执行完成!": "图转视频任务执行完成!", 'Midjourney图转视频任务执行完成。': 'Midjourney图转视频任务执行完成。', 'Midjourney图转视频任务执行中...': 'Midjourney图转视频任务执行中...', - '已成功提交Midjourney图转视频任务,任务ID:{taskId}': '已成功提交Midjourney图转视频任务,任务ID:{taskId}', + '已成功提交{type}图转视频任务,任务ID:{taskId}': '已成功提交{type}图转视频任务,任务ID:{taskId}', "小说批次任务的分镜数据的转视频配置为空,请检查": "小说批次任务的分镜数据的转视频配置为空,请检查", "分镜的图片没有全部出完,不能继续该操作!!": "分镜的图片没有全部出完,不能继续该操作!!", "分镜 {name} 图片在本地未找到,不能继续该操作,请检查对应分镜的图片路径是否正确": "分镜 {name} 图片在本地未找到,不能继续该操作,请检查对应分镜的图片路径是否正确", @@ -313,6 +315,14 @@ export default { '正在执行重置提示词任务,请稍等...': '正在执行重置提示词任务,请稍等...', "重置提示词失败,{error}": "重置提示词失败,{error}", "重置提示词成功": "重置提示词成功", + "该操作会选择 TXT 文件进行导入提示词,\n\n提示词文件格式要求:\n每行一个提示词,顺序和当前分镜顺序一致,\n如果某个分镜不需要导入提示词,可以留空该行,\n超出分镜的提示词会被删除,不足则只导入文本中有的提示词数据\n\n是否继续?": "该操作会选择 TXT 文件进行导入提示词,\n\n提示词文件格式要求:\n每行一个提示词,顺序和当前分镜顺序一致,\n如果某个分镜不需要导入提示词,可以留空该行,\n超出分镜的提示词会被删除,不足则只导入文本中有的提示词数据\n\n是否继续?", + "导入提示词失败,{error}": "导入提示词失败,{error}", + "导入提示词成功": "导入提示词成功", + "导入的提示词文件内容为空": "导入的提示词文件内容为空", + '导入第 {line} 行提示词失败,{error}': '导入第 {line} 行提示词失败,{error}', + "正在批量应用当前设置...": "正在批量应用当前设置...", + "将当前转视频的基础设置批量应用到所有的分镜中": "将当前转视频的基础设置批量应用到所有的分镜中", + "应用设置": "应用设置", //#endregion //#region 出图 @@ -978,6 +988,7 @@ export default { "AI处理文案成功": "AI处理文案成功", "AI处理文案失败,{error}": "AI处理文案失败,{error}", "检测系统当前的语言已修改,继续执行会刷新当前页面并应用当前语言,是否继续?": "检测系统当前的语言已修改,继续执行会刷新当前页面并应用当前语言,是否继续?", + "当前API提供商数据不存在,请检查数据是否正确": "当前API提供商数据不存在,请检查数据是否正确", //#endregion //#region 预设 @@ -1183,6 +1194,8 @@ export default { '场景分析': '场景分析', '推理所有提示词': '推理所有提示词', '推理空白分镜提示词': '推理空白分镜提示词', + "重置所有提示词": "重置所有提示词", + "导入提示词": "导入提示词", '生成所有图片': '生成所有图片', '生成未生成图片分镜': '生成未生成图片分镜', '生成失败图片分镜': '生成失败图片分镜', @@ -1545,6 +1558,10 @@ export default { '7. 需要在外部手动选择需要的{type}数据时,请点击“{button}” 按钮进行导入到标签集中': '7. 需要在外部手动选择需要的{type}数据时,请点击“{button}” 按钮进行导入到标签集中', '即将开始自动推理,该操作会将之前的 {type} 数据覆盖,是否继续?': '即将开始自动推理,该操作会将之前的场景数据覆盖,是否继续?', '正在推理,请稍等...': '正在推理,请稍等...', + "通用前/后缀" : "通用前/后缀", + "提示词前后缀设置" : "提示词前后缀设置", + "通用前缀" : "通用前缀", + "通用后缀" : "通用后缀", //#endregion //#region 转视频 @@ -1602,6 +1619,51 @@ export default { "当前批次任务已经开启图转视频,是否直接跳转到图文转视频界面?": "当前批次任务已经开启图转视频,是否直接跳转到图文转视频界面?", "正在跳转到图文转视频界面...": "正在跳转到图文转视频界面...", "已取消跳转,你可以在转视频模块中查看该任务": "已取消跳转,你可以在转视频模块中查看该任务", + '是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置:视频类型(SD/HD),运动变化(Motion),批次数据(Batch),视频原始(Raw),首尾循环(Loop) \n\n批量应用后,其余分镜的上述基础设置会被替换为当前分镜的数据,是否继续?': '是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置:视频类型(SD/HD),运动变化(Motion),批次数据(Batch),视频原始(Raw),首尾循环(Loop) \n\n批量应用后,其余分镜的上述基础设置会被替换为当前分镜的数据,是否继续?', + "批量应用当前设置失败,{error}": "批量应用当前设置失败,{error}", + "批量应用当前设置成功!": "批量应用当前设置成功!", + "配置验证失败": "配置验证失败", + + "可灵图转视频任务失败,失败信息:{error}": "可灵图转视频任务失败,失败信息:{error}", + "Kling图转视频任务完成!": "Kling图转视频任务完成!", + "未找到有效的API地址": "未找到有效的API地址", + "请先配置AI推理的API密钥": "请先配置AI推理的API密钥", + "当前分镜数据的可灵图转视频参数为空或参数校验失败,请检查": "当前分镜数据的可灵图转视频参数为空或参数校验失败,请检查", + "当前分镜数据的图片地址为空,请检查": "当前分镜数据的图片地址为空,请检查", + "视频ID数量与视频链接数量不匹配": "视频ID数量与视频链接数量不匹配", + "未找到有效的GPT API地址": "未找到有效的GPT API地址", + "未知的视频生成方式,请检查": "未知的视频生成方式,请检查", + "当前分镜数据的可灵视频ID为空,请检查": '当前分镜数据的可灵视频ID为空,请检查', + "当前分镜数据的可灵任务ID为空,请检查": "当前分镜数据的可灵任务ID为空,请检查", + "可灵视频延长任务完成!": "可灵视频延长任务完成!", + "可灵视频延长任务失败,失败信息:{error}": "可灵视频延长任务失败,失败信息:{error}", + "可灵图转视频任务执行中...": "可灵图转视频任务执行中...", + "可灵视频延长任务正在执行中...": "可灵视频延长任务正在执行中...", + "参考图像": "参考图像", + "正向文本提示,可选,不能超过2500个字符": "正向文本提示,可选,不能超过2500个字符", + "负向文本提示,可选,不能超过2500个字符": "负向文本提示,可选,不能超过2500个字符", + "自由度": "自由度", + '0-1之间': "0-1之间", + "生成视频的自由度

值越大,模型自由度越小
与用户输入的提示词相关性越强

取值范围:[0, 1]": "生成视频的自由度

值越大,模型自由度越小
与用户输入的提示词相关性越强

取值范围:[0, 1]", + "生成模式": "生成模式", + "生成视频的模式

枚举值:std,pro
其中std:标准模式(标准),基础模式,性价比高
其中pro:专家模式(高品质),高表现模式,生成视频质量更佳": "生成视频的模式

枚举值:std,pro
其中std:标准模式(标准),基础模式,性价比高
其中pro:专家模式(高品质),高表现模式,生成视频质量更佳", + "视频时长": "视频时长", + '生成视频时长,单位s(5秒或10秒)': '生成视频时长,单位s(5秒或10秒)', + "视频延长": "视频延长", + "选择已有的视频任务作为延长的基础": "选择已有的视频任务作为延长的基础", + "视频ID": "视频ID", + "支持通过文本、图片和视频延长生成的视频的ID(原视频不能超过3分钟)": "支持通过文本、图片和视频延长生成的视频的ID(原视频不能超过3分钟)", + "对应于视频ID的任务ID,通常在选择视频后自动填充": "对应于视频ID的任务ID,通常在选择视频后自动填充", + "是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置:模型名称(Model Name),生成模式(Mode),视频时长(Duration),自由度(CFG Scale) \n\n批量应用后,其余分镜的上述基础设置会被替换为当前分镜的数据,是否继续?": "是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置:模型名称(Model Name),生成模式(Mode),视频时长(Duration),自由度(CFG Scale) \n\n批量应用后,其余分镜的上述基础设置会被替换为当前分镜的数据,是否继续?", + "请选择一个已有的视频任务作为延长的基础": "请选择一个已有的视频任务作为延长的基础", + "父任务选择成功,视频ID已更新为: {videoId}" : "父任务选择成功,视频ID已更新为: {videoId}", + "高性能 (std)" : "高性能 (std)", + "高表现 (pro)" : "高表现 (pro)", + "选择Video" : "选择Video", + "必须

• 支持格式:.jpg/.jpeg/.png
• 文件大小:不超过10MB
• 分辨率:不小于300*300px
• 宽高比:1:2.5 ~ 2.5:1之间": "必须

• 支持格式:.jpg/.jpeg/.png
• 文件大小:不超过10MB
• 分辨率:不小于300*300px
• 宽高比:1:2.5 ~ 2.5:1之间", + "参考图像 - 尾帧控制": "参考图像 - 尾帧控制", + '可选

• 支持格式:.jpg/.jpeg/.png
• 文件大小:不超过10MB
• 分辨率:不小于300*300px
• 宽高比:1:2.5 ~ 2.5:1之间': '可选

• 支持格式:.jpg/.jpeg/.png
• 文件大小:不超过10MB
• 分辨率:不小于300*300px
• 宽高比:1:2.5 ~ 2.5:1之间', + //#endregion //#region MJ '基本信息': '基本信息', @@ -1721,7 +1783,6 @@ export default { '视频生成时长为5秒,但并非仅限于此。视频制作完成后,您可以在当前界面为选定的适配进行延长!': '视频生成时长为5秒,但并非仅限于此。视频制作完成后,您可以在当前界面为选定的适配进行延长!', '您可以随意将视频延长最多 4 次,每次延长 4 秒,直至达到 21 秒(即可用的最大长度)。': '您可以随意将视频延长最多 4 次,每次延长 4 秒,直至达到 21 秒(即可用的最大长度)。', - //#endregion //#endregion //#region 文案处理 @@ -1730,7 +1791,7 @@ export default { '【LaiTool】分镜大师-通用版(上下文-人物场景固定-类型推理)': '【LaiTool】分镜大师-通用版(上下文-人物场景固定-类型推理)', '【LaiTool】分镜大师-全面版-AI增强(上下文-人物场景固定-单帧)': '【LaiTool】分镜大师-全面版-AI增强(上下文-人物场景固定-单帧)', '【LaiTool】分镜大师-全能优化版(上下文-人物固定)': '【LaiTool】分镜大师-全能优化版(上下文-人物固定)', - "【LaiTool】分镜大师-MJ超精细化版(上下文-人物场景固定)" : "【LaiTool】分镜大师-MJ超精细化版(上下文-人物场景固定)", + "【LaiTool】分镜大师-MJ超精细化版(上下文-人物场景固定)": "【LaiTool】分镜大师-MJ超精细化版(上下文-人物场景固定)", '【LaiTool】分镜大师-SD英文版(上下文-人物场景固定-SD-英文提示词)': '【LaiTool】分镜大师-SD英文版(上下文-人物场景固定-SD-英文提示词)', '【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物自动推理)': '【LaiTool】分镜大师-单帧分镜提示词(上下文-单帧-人物自动推理)', "没有找到对应的AI选项,请先检查配置": "没有找到对应的AI选项,请先检查配置", diff --git a/src/main/service/aiReason/aiReasonCommon.ts b/src/main/service/aiReason/aiReasonCommon.ts index ae3d377..2e41175 100644 --- a/src/main/service/aiReason/aiReasonCommon.ts +++ b/src/main/service/aiReason/aiReasonCommon.ts @@ -10,6 +10,7 @@ import { RetryWithBackoff } from '@/define/Tools/common' import { Book } from '@/define/model/book/book' import { AiInferenceModelModel, GetAIPromptOptionByValue } from '@/define/data/aiData/aiData' import { t } from '@/i18n' +import { GetAIPromptRequestBodyByValue } from '@/define/data/aiData/aiPrompt/bookStoryboardPrompt' /** * AI推理通用工具类 @@ -278,7 +279,15 @@ export class AiReasonCommon { throw new Error(t('当前模式需要提前分析或者设置角色场景数据,请先分析角色/场景数据!')) } - let requestBody = cloneDeep(selectInferenceModel.requestBody) + let requestBody: OpenAIRequest.Request | null = null + if (typeof selectInferenceModel.requestBody == 'string') { + requestBody = cloneDeep(GetAIPromptRequestBodyByValue(selectInferenceModel.requestBody as string)) + } else { + requestBody = cloneDeep(selectInferenceModel.requestBody as OpenAIRequest.Request) + } + + // 通过 requestBody 获取实际的 requestBody + if (requestBody == null) { throw new Error(t('未找到对应的分镜预设的请求数据,请检查')) } diff --git a/src/main/service/book/subBookHandle/bookBasicHandle.ts b/src/main/service/book/subBookHandle/bookBasicHandle.ts index 09bc1d5..0e47654 100644 --- a/src/main/service/book/subBookHandle/bookBasicHandle.ts +++ b/src/main/service/book/subBookHandle/bookBasicHandle.ts @@ -3,6 +3,18 @@ import { BookTaskService } from '@/define/db/service/book/bookTaskService' import { OptionRealmService } from '@/define/db/service/optionService' import { BookService } from '@/define/db/service/book/bookService' import { TaskListService } from '@/define/db/service/book/taskListService' +import { TaskModal } from '@/define/model/task' +import { Book } from '@/define/model/book/book' +import { getProjectPath } from '../../option/optionCommonService' +import path from 'path' +import { isEmpty } from 'lodash' +import axios from 'axios' +import { define } from '@/define/define' +import { CheckFolderExistsOrCreate, CopyFileOrFolder } from '@/define/Tools/file' +import { DownloadFile } from '@/define/Tools/common' +import { MappingTaskTypeToVideoModel } from '@/define/enum/video' +import { BookBackTaskType } from '@/define/enum/bookEnum' +import { t } from '@/i18n' export class BookBasicHandle { bookTaskDetailService!: BookTaskDetailService @@ -34,10 +46,164 @@ export class BookBasicHandle { } } + /** + * 检查所有的服务是否都已初始化 + * @returns + */ + CheckInit() { + if (this.bookTaskDetailService + && this.bookTaskService + && this.optionRealmService + && this.bookService + && this.taskListService) { + return true + } + return false + } + + /** 执行事务的方法 */ async transaction(callback: (realm: any) => void) { - await this.InitBookBasicHandle() + this.CheckInit() || await this.InitBookBasicHandle() this.bookService.transaction(() => { callback(this.bookService.realm) }) } + + /** + * 下载视频文件并处理路径映射 + * + * 此方法负责从远程URL下载视频文件到本地,并处理文件的存储路径、转存服务等。 + * 支持多种视频来源的处理,包括MidJourney、可灵等不同平台的视频文件。 + * 会自动处理文件转存(除MJ官方CDN和可灵视频外),并更新数据库中的路径信息。 + * + * @param {string[]} videoUrls - 需要下载的视频URL列表 + * @param {TaskModal.Task} task - 当前执行的任务对象,包含任务类型等信息 + * @param {Book.SelectBookTaskDetail} bookTaskDetail - 小说任务详情对象,包含分镜信息 + * @param {string} preffix - 文件名前缀,用于区分不同来源的视频文件 + * + * @returns {Promise<{outVideoPath: string, subVideoPath: string[]}>} 返回下载结果 + * - outVideoPath: 主输出视频的本地路径(第一个视频的副本) + * - subVideoPath: 所有子视频的路径信息数组(JSON字符串格式) + * + * @throws {Error} 当服务未初始化时 + * @throws {Error} 当文件下载失败时 + * @throws {Error} 当数据库操作失败时 + * + * @example + * ```typescript + * const result = await this.DownloadVideoUrls( + * ['http://example.com/video1.mp4', 'http://example.com/video2.mp4'], + * task, + * bookTaskDetail, + * 'MJ' + * ); + * console.log('主视频路径:', result.outVideoPath); + * console.log('所有视频路径:', result.subVideoPath); + * ``` + * + * @description + * 处理流程: + * 1. 初始化服务并获取项目路径 + * 2. 遍历每个视频URL进行下载 + * 3. 根据任务类型决定是否使用转存服务 + * 4. 创建本地存储目录并下载文件 + * 5. 将第一个视频复制为主输出视频 + * 6. 更新数据库中的路径信息 + * + * @note + * - MidJourney官方CDN (cdn.midjourney.com) 的视频不支持转存 + * - 可灵视频 (KLING_VIDEO, KLING_VIDEO_EXTEND) 不使用转存服务 + * - 转存服务需要全局machineId配置 + * - 视频文件按时间戳和索引命名以避免冲突 + */ + async DownloadVideoUrls(videoUrls: string[], task: TaskModal.Task, bookTaskDetail: Book.SelectBookTaskDetail, preffix: string, videoIds?: string[]): Promise<{ outVideoPath: string, subVideoPath: string[] }> { + this.CheckInit() || await this.InitBookBasicHandle() + + if (videoIds != undefined && videoIds.length != videoUrls.length) { + throw new Error(t("视频ID数量与视频链接数量不匹配")) + } + + let bookTask = await this.bookTaskService.GetBookTaskDataById( + bookTaskDetail.bookTaskId as string, + true + ) + + let tempVideoUrls = bookTaskDetail.subVideoPath || [] + let newVideoUrls: string[] = [] + let outVideoPath: string = '' + + const project_path = await getProjectPath() + + // 开始下载所有视频 + for (let i = 0; i < videoUrls.length; i++) { + const videoUrl = videoUrls[i] + // 处理文件地址和下载 + let videoPath = path.join( + bookTask.imageFolder as string, + `video/subVideo/${bookTaskDetail.name}/${new Date().getTime()}_${i}.mp4` + ) + + let remoteUrl = videoUrl + + // 开始处理下载 mj 官方的图片不支持转存 + if (global.machineId + && !isEmpty(global.machineId) + && !videoUrl.startsWith('https://cdn.midjourney.com') + && task.type != BookBackTaskType.KLING_VIDEO + && task.type != BookBackTaskType.KLING_VIDEO_EXTEND + ) { + // 转存一下视频文件 + // 获取当前url的文件名 + let fileName = preffix + "_" + path.basename(videoUrl) + let transferRes = await axios.post(define.lms_url + `/lms/FileUpload/UrlUpload/${global.machineId}`, { + url: videoUrl, + fileName: fileName + }) + if (transferRes.status == 200 && transferRes.data.code == 1) { + remoteUrl = transferRes.data.data.url + } + } + + if (isEmpty(remoteUrl)) { + remoteUrl = videoUrl + } + + await CheckFolderExistsOrCreate(path.dirname(videoPath)) + await DownloadFile(remoteUrl, videoPath) + + // 处理返回数据信息 + // 开始修改信息 + // 将信息添加到里面 + let a = { + localPath: path.relative(project_path, videoPath), + remotePath: remoteUrl, + taskId: bookTaskDetail.videoMessage?.taskId, + videoId: videoIds != undefined && videoIds[i] ? videoIds[i] : "", + index: i, + type: MappingTaskTypeToVideoModel(task.type as string) + } + newVideoUrls.push(JSON.stringify(a)) + if (i == 0) { + outVideoPath = path.join( + bookTask.imageFolder as string, + 'video', + bookTaskDetail.name + path.extname(videoPath) + ) + await CopyFileOrFolder(videoPath, outVideoPath as string) + } + } + + // 开始处理数据 + // 将原有的视频路径合并到新数组中 + newVideoUrls.push(...tempVideoUrls) + await this.bookTaskDetailService.ModifyBookTaskDetailById(bookTaskDetail.id as string, { + subVideoPath: newVideoUrls, + generateVideoPath: outVideoPath != '' ? outVideoPath : '' + }) + + return { + outVideoPath: outVideoPath, + subVideoPath: newVideoUrls + } + } } diff --git a/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts b/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts index ed16178..9f7a965 100644 --- a/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts +++ b/src/main/service/book/subBookHandle/bookVideoServiceHandle.ts @@ -3,7 +3,9 @@ import { BookBasicHandle } from './bookBasicHandle' import { Book } from '@/define/model/book/book' import { ImageToVideoModels, + KlingDuration, KlingMode, + KlingModelName, MJVideoBatchSize, MJVideoMotion, MJVideoType, @@ -23,6 +25,7 @@ import { getProjectPath } from '../../option/optionCommonService' import { TaskModal } from '@/define/model/task' import { BookBackTaskStatus, BookBackTaskType, BookTaskStatus } from '@/define/enum/bookEnum' import { VideoHandle } from '@/main/service/video/index' +import { ResponseMessageType } from '@/define/enum/softwareEnum' export class BookVideoServiceHandle extends BookBasicHandle { constructor() { @@ -190,7 +193,7 @@ export class BookVideoServiceHandle extends BookBasicHandle { ) let gptUrl = GetApiDefineDataById(inferenceSetting.apiProvider)?.gpt_url if (gptUrl == null || isEmpty(gptUrl)) { - throw new Error() + throw new Error(t('未找到有效的GPT API地址')) } // 开始设置默认设置 @@ -219,13 +222,15 @@ export class BookVideoServiceHandle extends BookBasicHandle { } let klingOptions: BookTaskDetail.klingOptions = { - model: 'kling-v1', + model_name: KlingModelName.KLING_V2_1, image: outImage, image_tail: '', prompt: '', negative_prompt: '', mode: KlingMode.STD, - duration: RunwaySeconds.FIVE + duration: KlingDuration.FIVE, + video_id: '', + cfg_scale: 0.5 } let mjVideoOptions: BookTaskDetail.MjVideoOptions = { @@ -305,14 +310,33 @@ export class BookVideoServiceHandle extends BookBasicHandle { try { // 更具不同的方式调用不同的处理方法 const videoHandle = new VideoHandle() + let res; switch (task.type) { case BookBackTaskType.MJ_VIDEO: - return await videoHandle.MJImageToVideo(task) + res = await videoHandle.MJImageToVideo(task) + break case BookBackTaskType.MJ_VIDEO_EXTEND: - return await videoHandle.MJVideoExtendToVideo(task) + res = await videoHandle.MJVideoExtendToVideo(task) + break + case BookBackTaskType.KLING_VIDEO: + res = await videoHandle.KlingImageToVideo(task) + break + case BookBackTaskType.KLING_VIDEO_EXTEND : + res = await videoHandle.KlingVideoExtend(task) + break default: - throw new Error('未知的视频生成方式,请检查') + throw new Error(t('未知的视频生成方式,请检查')) } + let newValue = await this.bookTaskDetailService.GetBookTaskDetailDataById(task.bookTaskDetailId as string, true) + SendReturnMessage( + { + code: 1, + id: task.bookTaskDetailId as string, + message: t('图转视频任务执行完成。'), + type: ResponseMessageType.VIDEO_SUCESS, + data: JSON.stringify(newValue) + }, task.messageName as string) + return res; } catch (error) { // 统一处理 报错信息 let message = t("图转视频失败,失败信息如下:{error}", { error: (error as Error).message }) diff --git a/src/main/service/option/optionCommonService.ts b/src/main/service/option/optionCommonService.ts index e5d000d..f646085 100644 --- a/src/main/service/option/optionCommonService.ts +++ b/src/main/service/option/optionCommonService.ts @@ -3,6 +3,7 @@ import { OptionKeyName } from '@/define/enum/option' import { optionSerialization } from './optionSerialization' import { SettingModal } from '@/define/model/setting' import { t } from '@/i18n' +import { GetApiDefineDataById } from '@/define/data/apiData' /** * 获取当前项目的路径 @@ -33,3 +34,40 @@ export async function getGeneralSetting() { ) as SettingModal.GeneralSettings return generalSetting } + +/** + * 获取AI推理设置(包含API提供商信息) + * + * 此方法从数据库中获取AI推理相关的配置设置,并自动关联对应的API提供商详细信息。 + * 返回的数据结构包含了完整的推理配置和API提供商配置,方便在业务逻辑中直接使用。 + * + * @returns {Promise} 包含推理设置和API提供商信息的完整配置对象 + * @throws {Error} 当指定的API提供商ID在系统中不存在时抛出错误 + * + * @example + * ```typescript + * const inferenceConfig = await getInferenceSetting(); + * console.log(inferenceConfig.inferenceModel); // AI推理模型名称 + * console.log(inferenceConfig.apiProviderItem.name); // API提供商名称 + * ``` + */ +export async function getInferenceSetting(): Promise { + const optionRealmService = await OptionRealmService.getInstance() + let res = optionRealmService.GetOptionByKey(OptionKeyName.InferenceAI.InferenceSetting) + let aiReasonSetting = optionSerialization( + res, + t('设置 -> 推理设置') + ) + + // 获取对应的provider + let apiProviderItem = GetApiDefineDataById(aiReasonSetting.apiProvider); + if (apiProviderItem == null) { + throw new Error(t('当前API提供商数据不存在,请检查数据是否正确')) + } + let result = { + ...aiReasonSetting, + apiProviderItem + } + + return result +} diff --git a/src/main/service/task/taskManage.ts b/src/main/service/task/taskManage.ts index bb72303..0948402 100644 --- a/src/main/service/task/taskManage.ts +++ b/src/main/service/task/taskManage.ts @@ -436,7 +436,8 @@ export class TaskManager { // case BookBackTaskType.RUNWAY_VIDEO: // case BookBackTaskType.LUMA_VIDEO: - // case BookBackTaskType.KLING_VIDEO: + case BookBackTaskType.KLING_VIDEO: + case BookBackTaskType.KLING_VIDEO_EXTEND: case BookBackTaskType.MJ_VIDEO: case BookBackTaskType.MJ_VIDEO_EXTEND: this.AddImageToVideo(task) diff --git a/src/main/service/translate/translateCommon.ts b/src/main/service/translate/translateCommon.ts index e6e23af..6087f89 100644 --- a/src/main/service/translate/translateCommon.ts +++ b/src/main/service/translate/translateCommon.ts @@ -50,7 +50,7 @@ export class TranslateCommon { let apiProvider = GetApiDefineDataById(aiSetting.apiProvider) if (apiProvider.gpt_url == null || isEmpty(apiProvider.gpt_url)) { - throw new Error('未找到有效的GPT API地址') + throw new Error(t('未找到有效的GPT API地址')) } this.translationBusiness = apiProvider.gpt_url this.translationAppId = aiSetting.translationModel diff --git a/src/main/service/video/index.ts b/src/main/service/video/index.ts index 6df44e8..666f9c5 100644 --- a/src/main/service/video/index.ts +++ b/src/main/service/video/index.ts @@ -1,11 +1,14 @@ import { TaskModal } from '@/define/model/task' import { MJVideoService } from './mjVideo' +import { KlingVideoService } from './klingVideo' export class VideoHandle { mjVideoService: MJVideoService + klingVideoService: KlingVideoService // 这里可以添加 VideoHandle 特有的方法 constructor() { // mixin 装饰器会处理初始化 this.mjVideoService = new MJVideoService() + this.klingVideoService = new KlingVideoService() } /** MJ图片转视频处理方法 将指定的图片通过Midjourney API转换为视频 */ @@ -17,4 +20,13 @@ export class VideoHandle { MJVideoExtendToVideo(task: TaskModal.Task) { return this.mjVideoService.MJVideoExtendToVideo(task) } + + /** 将静态图片通过可灵AI模型转换为动态视频的核心方法。 支持多种参数配置,包括模型选择、提示词、负面提示词、持续时间等。 */ + KlingImageToVideo(task: TaskModal.Task) { + return this.klingVideoService.KlingImageToVideo(task) + } + + KlingVideoExtend(task: TaskModal.Task) { + return this.klingVideoService.KlingVideoExtend(task) + } } diff --git a/src/main/service/video/klingVideo.ts b/src/main/service/video/klingVideo.ts new file mode 100644 index 0000000..ce5b9d0 --- /dev/null +++ b/src/main/service/video/klingVideo.ts @@ -0,0 +1,748 @@ +import { TaskModal } from "@/define/model/task"; +import { BookBasicHandle } from "../book/subBookHandle/bookBasicHandle"; +import { getInferenceSetting } from "../option/optionCommonService"; +import { t } from "@/i18n"; +import { cloneDeep, isEmpty } from "lodash"; +import { SettingModal } from "@/define/model/setting"; +import { ValidateJson } from "@/define/Tools/validate"; +import { BookTaskDetail } from "@/define/model/book/bookTaskDetail"; +import { KlingDuration, KlingMode, KlingModelName, VideoStatus } from "@/define/enum/video"; +import { GetImageBase64 } from "@/define/Tools/image"; +import axios from "axios"; +import { console } from "inspector"; +import { SendReturnMessage, successMessage } from "@/public/generalTools"; +import { ResponseMessageType } from "@/define/enum/softwareEnum"; +import { GeneralResponse } from "@/define/model/generalResponse"; +import { Book } from "@/define/model/book/book"; +import { BookBackTaskStatus, BookBackTaskType, BookTaskStatus } from "@/define/enum/bookEnum"; + +export class KlingVideoService extends BookBasicHandle { + inferenceSetting!: SettingModal.InferenceAISettingAndProvider + constructor() { + super(); + } + + private async InitApiSetting() { + // 加载推理设置中的数据 + const inferenceSetting = await getInferenceSetting(); + this.inferenceSetting = inferenceSetting; + // 判断一些数据是不是存在 + if (isEmpty(this.inferenceSetting.apiProviderItem.base_url)) { + throw new Error(t('未找到有效的API地址')); + } + + if (this.inferenceSetting.apiToken == null || isEmpty(this.inferenceSetting.apiToken)) { + throw new Error(t('请先配置AI推理的API密钥')); + } + + } + + //#region KlingImageToVideo + /** + * 可灵图转视频服务 + * + * 将静态图片通过可灵AI模型转换为动态视频的核心方法。 + * 支持多种参数配置,包括模型选择、提示词、负面提示词、持续时间等。 + * + * @param {TaskModal.Task} task - 任务对象,包含小说批次任务的详细信息 + * @returns {Promise} 返回成功消息或抛出错误 + * + * @throws {Error} 当API地址未配置时 + * @throws {Error} 当API密钥未配置时 + * @throws {Error} 当分镜数据的视频配置为空时 + * @throws {Error} 当可灵参数校验失败时 + * @throws {Error} 当图片地址为空时 + * + * @example + * ```typescript + * const klingService = new KlingVideoService(); + * await klingService.KlingImageToVideo(task); + * ``` + */ + async KlingImageToVideo(task: TaskModal.Task): Promise { + try { + // 初始化基础句柄和API设置 + await this.InitBookBasicHandle(); + await this.InitApiSetting(); + + let { klingOption, videoMessage, bookTaskDetail } = await this.GetKlingOptions(task.bookTaskDetailId as string) + + let imageUrl = videoMessage.imageUrl?.trim() || klingOption.image?.trim() + if (isEmpty(imageUrl)) { + throw new Error(t("当前分镜数据的图片地址为空,请检查")) + } + + let image_tail = klingOption.image_tail?.trim() + + let model_name = klingOption.model_name?.trim() || KlingModelName.KLING_V2_1; + + let prompt = klingOption.prompt?.trim(); + let negative_prompt = klingOption.negative_prompt?.trim(); + let cfg_scale = klingOption.cfg_scale || 0.5; + let mode = klingOption.mode?.trim() || KlingMode.STD; + let duration = klingOption.duration ?? KlingDuration.FIVE; + + let body: BookTaskDetail.klingOptions = { + model_name: model_name as KlingModelName, + image: await GetImageBase64(imageUrl as string, true), + } + + if (imageUrl.startsWith('http')) { + body.image = imageUrl + } else { + body.image = await GetImageBase64(imageUrl as string, true) + } + + if (!isEmpty(image_tail)) { + if (image_tail?.startsWith('http')) { + body.image_tail = image_tail + } else { + body.image_tail = await GetImageBase64(image_tail as string, true) + } + } + if (!isEmpty(prompt)) { + body.prompt = prompt + } + if (!isEmpty(negative_prompt)) { + body.negative_prompt = negative_prompt + } + if (cfg_scale != null) { + body.cfg_scale = cfg_scale + } + if (mode != null && !isEmpty(mode)) { + body.mode = mode as KlingMode + } + if (duration != null) { + body.duration = duration + } + + let url = this.inferenceSetting.apiProviderItem.base_url + '/kling/v1/videos/image2video' + let res = await axios.post(url, body, { + headers: { + Authorization: `Bearer ${this.inferenceSetting.apiToken}`, + 'Content-Type': 'application/json' + } + }) + console.log("Kling video res", res.data) + + let resData = res.data + + let taskId = resData.data.task_id + + // 修改Task, 将数据写入 + this.taskListService.UpdateBackTaskData(task.id as string, { + taskId: taskId as string, + taskMessage: JSON.stringify(resData) + }) + + // 修改videoMessage + videoMessage.taskId = taskId + videoMessage.status = VideoStatus.SUBMITTED + videoMessage.messageData = JSON.stringify(resData) + videoMessage.msg = '' + delete videoMessage.imageUrl // 不要修改原本的图片地址 + + this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage( + task.bookTaskDetailId as string, + videoMessage + ) + + // 添加任务成功 返回前端任务事件 + SendReturnMessage( + { + code: 1, + id: task.bookTaskDetailId as string, + message: t('已成功提交{type}图转视频任务,任务ID:{taskId}', { type: t("可灵"), taskId: taskId }), + type: ResponseMessageType.KLING_VIDEO, + data: JSON.stringify(videoMessage) + }, + task.messageName as string + ) + await this.FecthKlingImageToVideoResult(bookTaskDetail, task, taskId, false) + return successMessage( + t('Kling图转视频任务完成!'), + 'MJVideoService_MJImageToVideo' + ) + + } catch (error) { + throw new Error(t('可灵图转视频任务失败,失败信息:{error}', { error: (error as Error).message })); + } + } + + //#endregion + + //#region KlingVideoExtend + + /** + * 可灵视频延长服务 + * + * 对已生成的视频进行延长处理,通过可灵AI模型为现有视频添加更多内容。 + * 支持自定义提示词、负面提示词和CFG Scale参数,实现精确的视频延长控制。 + * + * @param {TaskModal.Task} task - 任务对象,包含小说批次任务的详细信息 + * @returns {Promise} 返回成功消息或抛出错误 + * + * @throws {Error} 当API地址未配置时 + * @throws {Error} 当API密钥未配置时 + * @throws {Error} 当视频ID为空时 + * @throws {Error} 当任务ID为空时 + * @throws {Error} 当配置数据无效时 + * + * @description + * 视频延长处理流程: + * 1. 初始化基础句柄和API设置 + * 2. 获取并验证可灵配置选项(包含视频ID、任务ID等) + * 3. 验证必需参数(video_id、task_id)的存在性 + * 4. 构建API请求体,包含延长所需的参数 + * 5. 调用可灵视频延长API接口 + * 6. 更新任务状态和视频消息数据 + * 7. 发送前端任务提交成功通知 + * 8. 启动轮询机制跟踪任务执行状态 + * + * @note + * - 视频延长基于已存在的视频内容进行扩展 + * - 需要提供原始视频的video_id和task_id + * - 支持自定义提示词控制延长内容的风格 + * - 延长后的视频会通过轮询机制获取结果 + * - 所有参数都会在前端界面进行配置和验证 + * + * @example + * ```typescript + * const klingService = new KlingVideoService(); + * const result = await klingService.KlingVideoExtend(task); + * console.log('视频延长任务已提交'); + * ``` + */ + async KlingVideoExtend(task: TaskModal.Task): Promise { + try { + // 初始化基础句柄和API设置 + await this.InitBookBasicHandle(); + await this.InitApiSetting(); + + let { klingOption, videoMessage, bookTaskDetail } = await this.GetKlingOptions(task.bookTaskDetailId as string) + + let video_id = klingOption.video_id?.trim() + if (isEmpty(video_id)) { + throw new Error(t("当前分镜数据的可灵视频ID为空,请检查")) + } + let task_id = klingOption.task_id?.trim() + if (isEmpty(task_id)) { + throw new Error(t("当前分镜数据的可灵任务ID为空,请检查")) + } + + let prompt = klingOption.prompt?.trim(); + let negative_prompt = klingOption.negative_prompt?.trim(); + let cfg_scale = klingOption.cfg_scale || 0.5; + + let body: any = { + video_id: video_id as string, + task_id: task_id as string + } + if (!isEmpty(prompt)) { + body.prompt = prompt + } + if (!isEmpty(negative_prompt)) { + body.negative_prompt = negative_prompt + } + if (cfg_scale != null) { + body.cfg_scale = cfg_scale + } + // 开始做请求 + let url = this.inferenceSetting.apiProviderItem.base_url + '/kling/v1/videos/video-extend'; + let res = await axios.post(url, body, { + headers: { + Authorization: `Bearer ${this.inferenceSetting.apiToken}`, + 'Content-Type': 'application/json' + } + }) + + console.log("Kling video extend res", res.data) + + let resData = res.data + + let taskId = resData.data.task_id + + // 修改Task, 将数据写入 + this.taskListService.UpdateBackTaskData(task.id as string, { + taskId: taskId as string, + taskMessage: JSON.stringify(resData) + }) + + // 修改videoMessage + videoMessage.taskId = taskId + videoMessage.status = VideoStatus.SUBMITTED + videoMessage.messageData = JSON.stringify(resData) + videoMessage.msg = '' + delete videoMessage.imageUrl // 不要修改原本的图片地址 + + this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage( + task.bookTaskDetailId as string, + videoMessage + ) + + // 添加任务成功 返回前端任务事件 + SendReturnMessage( + { + code: 1, + id: task.bookTaskDetailId as string, + message: t('已成功提交{type}图转视频任务,任务ID:{taskId}', { type: t("可灵"), taskId: taskId }), + type: ResponseMessageType.KLING_VIDEO_EXTEND, + data: JSON.stringify(videoMessage) + }, + task.messageName as string + ) + await this.FecthKlingImageToVideoResult(bookTaskDetail, task, taskId, false) + return successMessage( + t('可灵视频延长任务完成!'), + 'MJVideoService_MJImageToVideo' + ) + } catch (error) { + throw new Error(t('可灵视频延长任务失败,失败信息:{error}', { error: (error as Error).message })); + } + } + + //#endregion + + + //#region FecthKlingImageToVideoResult + /** + * 获取可灵图转视频任务结果 + * + * 通过轮询机制持续检查可灵视频生成任务的执行状态,直到任务完成(成功或失败)。 + * 根据不同的任务状态调用相应的处理方法,实现任务状态的实时跟踪和处理。 + * + * @param {Book.SelectBookTaskDetail} bookTaskDetail - 小说任务详情对象,包含分镜和视频配置信息 + * @param {TaskModal.Task} task - 当前执行的任务对象,包含任务类型和消息通道信息 + * @param {string} taskId - 可灵任务的唯一标识符,用于查询任务状态 + * @param {boolean} useTransfer - 是否使用转存服务的标志位(当前未使用,保留参数) + * + * @throws {Error} 当遇到未知任务状态时抛出错误 + * @throws {Error} 当API调用失败时抛出错误 + * + * @description + * 轮询处理流程: + * 1. 构建查询URL并发送GET请求获取任务状态 + * 2. 解析返回的任务状态信息 + * 3. 根据状态值进行分发处理: + * - "submitted" | "processing": 调用KlingTaskProcessing处理中间状态 + * - "succeed": 调用KlingTaskSuccessed处理成功状态并结束轮询 + * - "failed": 调用KlingTaskFailed处理失败状态并结束轮询 + * - 其他状态: 抛出未知状态错误 + * 4. 对于中间状态,等待20秒后继续下一次轮询 + * 5. 对于终态(成功/失败),退出轮询循环 + * + * @note + * - 这是一个无限循环的轮询方法,只有在任务完成时才会退出 + * - 每次轮询间隔为20秒(在KlingTaskProcessing中实现) + * - 支持图转视频和视频延长两种任务类型的状态跟踪 + * - 所有状态变更都会同步到数据库并通知前端 + * + * @example + * ```typescript + * await this.FecthKlingImageToVideoResult( + * bookTaskDetail, + * task, + * 'kling_task_123456', + * false + * ); + * ``` + */ + async FecthKlingImageToVideoResult( + bookTaskDetail: Book.SelectBookTaskDetail, + task: TaskModal.Task, + taskId: string, + useTransfer: boolean = false) { + console.log(useTransfer) + while (true) { + let fetchUrl = this.inferenceSetting.apiProviderItem.base_url + '/kling/v1/videos/image2video/' + taskId; + + let res = await axios.get(fetchUrl, { + headers: { + Authorization: `Bearer ${this.inferenceSetting.apiToken}`, + 'Content-Type': 'application/json' + } + }); + + let resData = res.data.data; + + let status = resData.task_status; + if (status === "submitted" || status === "processing") { + await this.KlingTaskProcessing(bookTaskDetail, task, taskId, resData) + } else if (status === "succeed") { + await this.KlingTaskSuccessed(bookTaskDetail, task, taskId, resData) + break + } else if (status === "failed") { + // 任务失败 + await this.KlingTaskFailed(bookTaskDetail, task, taskId, resData) + break + } else { + // 未知状态 + throw new Error(t("未知状态")); + } + } + } + + //#endregion + + + //#region fetck video extend res + + /** + * 获取可灵视频延长任务结果 + * + * 通过轮询机制持续检查可灵视频延长任务的执行状态,直到任务完成(成功或失败)。 + * 专门用于视频延长任务的状态跟踪,与图转视频任务使用不同的API端点。 + * + * @param {Book.SelectBookTaskDetail} bookTaskDetail - 小说任务详情对象,包含分镜和视频配置信息 + * @param {TaskModal.Task} task - 当前执行的任务对象,包含任务类型和消息通道信息 + * @param {string} taskId - 可灵视频延长任务的唯一标识符,用于查询任务状态 + * @param {boolean} useTransfer - 是否使用转存服务的标志位(当前未使用,保留参数) + * + * @throws {Error} 当遇到未知任务状态时抛出错误 + * @throws {Error} 当API调用失败时抛出错误 + * + * @description + * 视频延长任务轮询处理流程: + * 1. 构建视频延长任务查询URL(/kling/v1/videos/video-extend/{taskId}) + * 2. 发送GET请求获取任务状态信息 + * 3. 解析返回的任务状态数据 + * 4. 根据状态值进行分发处理: + * - "submitted" | "processing": 调用KlingTaskProcessing处理中间状态 + * - "succeed": 调用KlingTaskSuccessed处理成功状态并结束轮询 + * - "failed": 调用KlingTaskFailed处理失败状态并结束轮询 + * - 其他状态: 抛出未知状态错误 + * 5. 对于中间状态,等待20秒后继续下一次轮询 + * 6. 对于终态(成功/失败),退出轮询循环 + * + * @note + * - 这是一个无限循环的轮询方法,只有在任务完成时才会退出 + * - 每次轮询间隔为20秒(在KlingTaskProcessing中实现) + * - 专门用于视频延长任务,与图转视频任务使用不同的API路径 + * - 所有状态变更都会同步到数据库并通知前端 + * - 支持视频延长任务的完整生命周期管理 + * + * @example + * ```typescript + * await this.FecthKlingVideoExtendResult( + * bookTaskDetail, + * task, + * 'kling_extend_task_123456', + * false + * ); + * ``` + * + * @see KlingTaskProcessing - 处理中间状态 + * @see KlingTaskSuccessed - 处理成功状态 + * @see KlingTaskFailed - 处理失败状态 + */ + async FecthKlingVideoExtendResult( + bookTaskDetail: Book.SelectBookTaskDetail, + task: TaskModal.Task, + taskId: string, + useTransfer: boolean = false + ) { + console.log(useTransfer) + while (true) { + let fetchUrl = this.inferenceSetting.apiProviderItem.base_url + '/kling/v1/videos/video-extend/' + taskId; + + let res = await axios.get(fetchUrl, { + headers: { + Authorization: `Bearer ${this.inferenceSetting.apiToken}`, + 'Content-Type': 'application/json' + } + }); + + let resData = res.data.data; + + let status = resData.task_status; + if (status === "submitted" || status === "processing") { + await this.KlingTaskProcessing(bookTaskDetail, task, taskId, resData) + } else if (status === "succeed") { + await this.KlingTaskSuccessed(bookTaskDetail, task, taskId, resData) + break + } else if (status === "failed") { + // 任务失败 + await this.KlingTaskFailed(bookTaskDetail, task, taskId, resData) + break + } else { + // 未知状态 + throw new Error(t("未知状态")); + } + } + } + + //#endregion + + //#region get kling option + + /** + * 获取可灵视频配置选项 + * + * 从小说任务详情中提取和解析可灵视频生成相关的配置参数。 + * 包括验证配置数据的完整性和有效性,确保后续视频生成过程能够正常进行。 + * + * @param {string} bookTaskDetailId - 小说任务详情的唯一标识符 + * + * @returns {Promise<{klingOption: BookTaskDetail.klingOptions, videoMessage: BookTaskDetail.VideoMessage}>} + * 返回包含可灵配置选项和视频消息的对象 + * - klingOption: 解析后的可灵视频配置参数 + * - videoMessage: 完整的视频消息配置对象 + * + * @throws {Error} 当小说任务详情不存在时 + * @throws {Error} 当视频配置信息为空时 + * @throws {Error} 当可灵参数JSON格式无效时 + * + * @description + * 配置获取流程: + * 1. 根据任务详情ID查询数据库获取完整数据 + * 2. 验证视频消息配置是否存在 + * 3. 验证可灵选项字符串是否为有效的JSON格式 + * 4. 解析JSON字符串为可灵配置对象 + * 5. 返回配置选项和视频消息的组合对象 + * + * @note + * - 这个方法主要用于配置验证和数据准备阶段 + * - 确保了可灵视频生成所需的所有参数都已正确配置 + * - 为后续的视频生成API调用提供完整的参数基础 + * - 支持灵活的配置参数组合和验证 + * + * @example + * ```typescript + * const { klingOption, videoMessage } = await klingService.GetKlingOptions( + * 'task_detail_123456' + * ); + * console.log('模型名称:', klingOption.model_name); + * console.log('视频状态:', videoMessage.status); + * ``` + */ + async GetKlingOptions(bookTaskDetailId: string): Promise<{ bookTaskDetail: Book.SelectBookTaskDetail, klingOption: BookTaskDetail.klingOptions, videoMessage: BookTaskDetail.VideoMessage }> { + // 开始处理小说数据 + let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId, true); + + + // 获取视频配置信息 + let videoMessage = bookTaskDetail.videoMessage + if (videoMessage == null || videoMessage == undefined) { + throw new Error(t("小说批次任务的分镜数据的转视频配置为空,请检查")) + } + + // 获取 MJ Video 的options + let klingOptionsString = bookTaskDetail.videoMessage?.klingOptions as string + if (!ValidateJson(klingOptionsString)) { + throw new Error(t("当前分镜数据的可灵图转视频参数为空或参数校验失败,请检查")) + } + + let klingOptions = JSON.parse(klingOptionsString) as BookTaskDetail.klingOptions + return { bookTaskDetail: bookTaskDetail, klingOption: klingOptions, videoMessage: videoMessage } + } + + //#endregion + + //#region Kling Task Status Handle + + /** + * 处理可灵任务失败状态 + * + * 当可灵视频生成任务失败时,更新相关数据状态并通知前端。 + * 包括更新视频消息状态、任务状态,并发送失败通知消息。 + * + * @param {Book.SelectBookTaskDetail} bookTaskDetail - 小说任务详情对象 + * @param {TaskModal.Task} task - 当前执行的任务对象 + * @param {string} taskId - 可灵任务ID + * @param {any} resData - 从可灵API返回的响应数据,包含失败信息 + * + * @throws {Error} 抛出包含失败原因的错误信息 + * + * @description + * 处理流程: + * 1. 复制并更新视频消息状态为失败 + * 2. 记录失败原因和任务数据 + * 3. 更新数据库中的视频消息和任务状态 + * 4. 根据任务类型发送相应的失败通知 + * 5. 抛出包含具体失败信息的错误 + */ + async KlingTaskFailed(bookTaskDetail: Book.SelectBookTaskDetail, + task: TaskModal.Task, + taskId: string, + resData: any) { + // 修改小说分镜的 videoMessage + let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {} + + videoMessage.status = VideoStatus.FAIL + videoMessage.msg = resData.task_status_msg + videoMessage.taskId = taskId + videoMessage.messageData = JSON.stringify(resData) + delete videoMessage.imageUrl + + // 修改 videoMessage数据 + this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage( + bookTaskDetail.id as string, + videoMessage + ) + + // 修改TASK + this.taskListService.UpdateBackTaskData(task.id as string, { + taskId: taskId, + taskMessage: JSON.stringify(resData) + }) + + // 返回前端数据 + SendReturnMessage( + { + code: 0, + id: bookTaskDetail.id as string, + message: task.type == BookBackTaskType.KLING_VIDEO_EXTEND ? + t("可灵视频延长任务失败,失败信息:{error}", { + error: resData.task_status_msg + }) : + t('可灵图转视频任务失败,失败信息:{error}', { + error: resData.task_status_msg + }), + type: task.type == BookBackTaskType.KLING_VIDEO_EXTEND ? ResponseMessageType.KLING_VIDEO_EXTEND : ResponseMessageType.KLING_VIDEO, + data: JSON.stringify(videoMessage) + }, + task.messageName as string + ) + throw new Error(resData.task_status_msg) + } + + /** + * 处理可灵任务成功状态 + * + * 当可灵视频生成任务成功完成时,处理视频结果并下载到本地。 + * 包括提取视频URL、更新状态、下载视频文件,并通知前端任务完成。 + * + * @param {Book.SelectBookTaskDetail} bookTaskDetail - 小说任务详情对象 + * @param {TaskModal.Task} task - 当前执行的任务对象 + * @param {string} taskId - 可灵任务ID + * @param {any} resData - 从可灵API返回的成功响应数据,包含视频URLs + * + * @description + * 处理流程: + * 1. 复制并更新视频消息状态为成功 + * 2. 从响应数据中提取视频URL列表 + * 3. 更新数据库中的视频消息和任务状态 + * 4. 修改小说分镜状态为图转视频成功 + * 5. 调用下载方法将视频保存到本地 + * 6. 根据任务类型发送相应的成功通知 + * + * @note + * - 支持多个视频文件的处理 + * - 自动更新任务状态为完成 + * - 使用任务ID作为下载文件的前缀标识 + */ + async KlingTaskSuccessed(bookTaskDetail: Book.SelectBookTaskDetail, + task: TaskModal.Task, + taskId: string, + resData: any) { + // 任务成功 修改 videoMessage + let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {} + videoMessage.status = VideoStatus.SUCCESS + videoMessage.taskId = taskId + + let klingVideoIds: string[] = [] + + if (resData.task_result && resData.task_result.videos && resData.task_result.videos.length > 0) { + videoMessage.videoUrls = [] + resData.task_result.videos.forEach((item: any) => { + videoMessage.videoUrls?.push(item.url) + klingVideoIds.push(item.id); + }) + } + videoMessage.messageData = JSON.stringify(resData) + delete videoMessage.imageUrl + + this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage( + task.bookTaskDetailId as string, + videoMessage + ) + + // 修改小说分镜状态 + this.bookTaskDetailService.ModifyBookTaskDetailById(task.bookTaskDetailId as string, { + status: BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS + }) + + // 修改任务状态 + this.taskListService.UpdateBackTaskData(task.id as string, { + status: BookBackTaskStatus.DONE, + taskId: taskId, + taskMessage: JSON.stringify(resData) + }) + + let klingId = resData.task_id ?? new Date().getTime().toString() + + // 下载 视频 + await this.DownloadVideoUrls(videoMessage.videoUrls || [], task, bookTaskDetail, klingId, klingVideoIds) + + SendReturnMessage( + { + code: 1, + id: bookTaskDetail.id as string, + message: task.type == BookBackTaskType.KLING_VIDEO_EXTEND ? + t('可灵视频延长任务完成!') : t('Kling图转视频任务完成!'), + type: task.type == BookBackTaskType.KLING_VIDEO_EXTEND ? ResponseMessageType.KLING_VIDEO_EXTEND : ResponseMessageType.KLING_VIDEO, + data: JSON.stringify(videoMessage) + }, + task.messageName as string + ) + } + + /** + * 处理可灵任务执行中状态 + * + * 当可灵视频生成任务正在处理或刚提交时,更新任务状态并等待继续检查。 + * 用于轮询机制中的中间状态处理,保持任务状态同步并通知前端进度。 + * + * @param {Book.SelectBookTaskDetail} bookTaskDetail - 小说任务详情对象 + * @param {TaskModal.Task} task - 当前执行的任务对象 + * @param {string} taskId - 可灵任务ID + * @param {any} resData - 从可灵API返回的处理中状态数据 + * + * @description + * 处理流程: + * 1. 复制并更新视频消息状态为处理中 + * 2. 更新任务数据和消息内容 + * 3. 更新数据库中的视频消息状态 + * 4. 根据任务类型发送相应的处理中通知 + * 5. 等待20秒后返回,用于轮询间隔控制 + * + * @note + * - 这个方法会阻塞20秒作为轮询间隔 + * - 支持图转视频和视频延长两种任务类型 + * - 状态包括"submitted"(已提交)和"processing"(处理中) + */ + async KlingTaskProcessing( + bookTaskDetail: Book.SelectBookTaskDetail, + task: TaskModal.Task, + taskId: string, + resData: any) { + // 任务执行中或者是提交成功 + let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {} + videoMessage.status = VideoStatus.PROCESSING + videoMessage.taskId = taskId + videoMessage.messageData = JSON.stringify(resData) + delete videoMessage.imageUrl + this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage( + task.bookTaskDetailId as string, + videoMessage + ) + + SendReturnMessage( + { + code: 1, + id: bookTaskDetail.id as string, + message: task.type == BookBackTaskType.KLING_VIDEO_EXTEND ? + t('可灵视频延长任务正在执行中...') : t('可灵图转视频任务执行中...'), + type: task.type == BookBackTaskType.KLING_VIDEO_EXTEND ? ResponseMessageType.KLING_VIDEO_EXTEND : ResponseMessageType.KLING_VIDEO, + data: JSON.stringify(videoMessage) + }, + task.messageName as string + ) + + // 没有成功 等待二十秒后继续执行 + await new Promise((resolve) => setTimeout(resolve, 20000)) + } + + //#endregion + +} \ No newline at end of file diff --git a/src/main/service/video/mjVideo.ts b/src/main/service/video/mjVideo.ts index 224c926..d6d43a3 100644 --- a/src/main/service/video/mjVideo.ts +++ b/src/main/service/video/mjVideo.ts @@ -152,7 +152,7 @@ export class MJVideoService extends MJApiService { { code: 1, id: task.bookTaskDetailId as string, - message: t('已成功提交Midjourney图转视频任务,任务ID:{taskId}', { taskId: id }), + message: t('已成功提交{type}图转视频任务,任务ID:{taskId}', { type: 'Midjourney', taskId: id }), type: ResponseMessageType.MJ_VIDEO, data: JSON.stringify(videoMessage) }, @@ -275,7 +275,7 @@ export class MJVideoService extends MJApiService { { code: 1, id: task.bookTaskDetailId as string, - message: t('已成功提交Midjourney图转视频任务,任务ID:{taskId}', { taskId: id }), + message: t('已成功提交{type}图转视频任务,任务ID:{taskId}', { type: 'Midjourney Extend', taskId: id }), type: ResponseMessageType.MJ_VIDEO, data: JSON.stringify(videoMessage) }, diff --git a/src/renderer/components.d.ts b/src/renderer/components.d.ts index 059b7cc..cecb7a3 100644 --- a/src/renderer/components.d.ts +++ b/src/renderer/components.d.ts @@ -21,6 +21,7 @@ declare module 'vue' { ComfyUIAddWorkflow: typeof import('./src/components/Setting/ComfyUIAddWorkflow.vue')['default'] ComfyUISetting: typeof import('./src/components/Setting/ComfyUISetting.vue')['default'] CommonDialog: typeof import('./src/components/common/CommonDialog.vue')['default'] + ConfigOptionGroup: typeof import('./src/components/common/ConfigOptionGroup.vue')['default'] ContactDeveloper: typeof import('./src/components/SoftHome/ContactDeveloper.vue')['default'] CopyWritingCategoryMenu: typeof import('./src/components/CopyWriting/CopyWritingCategoryMenu.vue')['default'] CopyWritingContent: typeof import('./src/components/CopyWriting/CopyWritingContent.vue')['default'] @@ -34,6 +35,7 @@ declare module 'vue' { DatatableGenerateImageAction: typeof import('./src/components/Original/BookTaskDetail/DatatableGenerateImageAction.vue')['default'] DataTableGptPrompt: typeof import('./src/components/Original/BookTaskDetail/DataTableGptPrompt.vue')['default'] DatatableHeaderCharacter: typeof import('./src/components/Original/BookTaskDetail/DatatableHeaderCharacter.vue')['default'] + DatatableHeaderGptPrompt: typeof import('./src/components/Original/BookTaskDetail/DatatableHeaderGptPrompt.vue')['default'] DatatableHeaderImage: typeof import('./src/components/Original/BookTaskDetail/DatatableHeaderImage.vue')['default'] DisabledWrapper: typeof import('./src/components/common/DisabledWrapper.vue')['default'] DocHelp: typeof import('./src/components/DocHelp.vue')['default'] @@ -50,6 +52,8 @@ declare module 'vue' { InputDialogContent: typeof import('./src/components/common/InputDialogContent.vue')['default'] JianyingGenerateInformation: typeof import('./src/components/Original/BookTaskDetail/JianyingGenerateInformation.vue')['default'] JianyingKeyFrameSetting: typeof import('./src/components/Setting/JianyingKeyFrameSetting.vue')['default'] + KlingImageToVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/KlingImageToVideoInfo.vue')['default'] + KlingVideoExtendInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/KlingVideoExtendInfo.vue')['default'] LanguageSwitcher: typeof import('./src/components/common/LanguageSwitcher.vue')['default'] LoadingComponent: typeof import('./src/components/common/LoadingComponent.vue')['default'] ManageAISetting: typeof import('./src/components/CopyWriting/ManageAISetting.vue')['default'] @@ -57,15 +61,16 @@ declare module 'vue' { MediaToVideoInfoConfig: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue')['default'] MediaToVideoInfoEmptyState: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoEmptyState.vue')['default'] MediaToVideoInfoHome: typeof import('./src/components/MediaToVideo/MediaToVideoInfoHome.vue')['default'] + MediaToVideoInfoKlingVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/MediaToVideoInfoKlingVideoInfo.vue')['default'] MediaToVideoInfoMJVideoExtend: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue')['default'] MediaToVideoInfoMJVideoImageToVideo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoImageToVideo.vue')['default'] MediaToVideoInfoMJVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoInfo.vue')['default'] - MediaToVideoInfoMJVideoSelectParentTask: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoSelectParentTask.vue')['default'] MediaToVideoInfoTaskDetail: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskDetail.vue')['default'] MediaToVideoInfoTaskList: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskList.vue')['default'] MediaToVideoInfoTaskOptions: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoTaskOptions.vue')['default'] MediaToVideoInfoVideoConfig: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoConfig.vue')['default'] MediaToVideoInfoVideoListInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoVideoListInfo.vue')['default'] + MediaToVideoSelectParentTask: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoSelectParentTask.vue')['default'] MenuOpenRound: typeof import('./src/components/common/Icon/MenuOpenRound.vue')['default'] MessageAndProgress: typeof import('./src/components/Original/BookTaskDetail/MessageAndProgress.vue')['default'] MJAccountDialog: typeof import('./src/components/Setting/MJSetting/MJAccountDialog.vue')['default'] @@ -116,6 +121,8 @@ declare module 'vue' { NSpace: typeof import('naive-ui')['NSpace'] NSpin: typeof import('naive-ui')['NSpin'] NSwitch: typeof import('naive-ui')['NSwitch'] + NTabPane: typeof import('naive-ui')['NTabPane'] + NTabs: typeof import('naive-ui')['NTabs'] NTag: typeof import('naive-ui')['NTag'] NText: typeof import('naive-ui')['NText'] NTooltip: typeof import('naive-ui')['NTooltip'] @@ -132,6 +139,7 @@ declare module 'vue' { OriginalTaskList: typeof import('./src/components/Original/MainHome/OriginalTaskList.vue')['default'] OriginalViewBookInfo: typeof import('./src/components/Original/MainHome/OriginalViewBookInfo.vue')['default'] OriginalViewBookTaskInfo: typeof import('./src/components/Original/MainHome/OriginalViewBookTaskInfo.vue')['default'] + PointRightIcon: typeof import('./src/components/common/Icon/PointRightIcon.vue')['default'] PresetShowCard: typeof import('./src/components/Preset/PresetShowCard.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue index a90df63..d6b1b0a 100644 --- a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue @@ -139,77 +139,7 @@
-
-
-
- - - - - - 模型 -
-
- {{ klingOptions?.model || 'kling-v1' }} -
-
- -
-
- - - - - - 模式 -
-
- - {{ klingOptions?.mode === 'pro' ? '高表现' : '高性能' }} - -
-
- -
-
- - - - - - 时长 -
-
- {{ klingOptions?.duration || 5 }}秒 -
-
- -
-
- - - - - - 提示词相关性 -
-
- {{ klingOptions.cfg_scale }} -
-
-
+
diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/KlingImageToVideoInfo.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/KlingImageToVideoInfo.vue new file mode 100644 index 0000000..2675b14 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/KlingImageToVideoInfo.vue @@ -0,0 +1,184 @@ + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/KlingVideoExtendInfo.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/KlingVideoExtendInfo.vue new file mode 100644 index 0000000..b0e4b3c --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/KlingVideoExtendInfo.vue @@ -0,0 +1,118 @@ + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/MediaToVideoInfoKlingVideoInfo.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/MediaToVideoInfoKlingVideoInfo.vue new file mode 100644 index 0000000..88f9238 --- /dev/null +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/MediaToVideoInfoKlingVideoInfo.vue @@ -0,0 +1,288 @@ + + + diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue index a759549..2a8deb6 100644 --- a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue @@ -443,16 +443,43 @@ - - - {{ t('执行视频拓展') }} - +
+ + + {{ t('应用设置') }} + + + + + {{ t('执行视频拓展') }} + +
@@ -469,8 +496,6 @@ import { NTooltip, NSwitch, NAlert, - NImage, - NText, useMessage } from 'naive-ui' @@ -511,13 +536,24 @@ const props = defineProps({ }) // 定义 emits -const emit = defineEmits(['video-message-change', 'extend', 'select-parent-task', 'image-upload']) +const emit = defineEmits([ + 'video-message-change', + 'extend', + 'select-parent-task', + 'image-upload', + 'batch-settings' +]) // 修改 videoMessage 的通用函数 function handleVideoMessageChange(key, value = undefined) { emit('video-message-change', key, value) } +// 处理批量设置 +async function handleBatchSettings() { + emit('batch-settings', 'extend') +} + // 处理视频拓展 async function handleExtend() { console.log('执行视频拓展', props.videoMessage, props.task) diff --git a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoImageToVideo.vue b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoImageToVideo.vue index 21ccc0d..debb8b6 100644 --- a/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoImageToVideo.vue +++ b/src/renderer/src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoImageToVideo.vue @@ -8,7 +8,6 @@ :placeholder="t('请输入图片链接')" @change="handleVideoMessageChange('imageUrl')" size="small" - :disabled="loading" class="image-input" >