This commit is contained in:
2026-06-30 15:02:20 +08:00
commit 3948b5a48a
306 changed files with 77275 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
release/
frontweb-dist/
backend-app/
*.log
+3
View File
@@ -0,0 +1,3 @@
registry=https://registry.npmmirror.com
strict-ssl=false
better_sqlite3_binary_host_mirror=https://npmmirror.com/mirrors/better-sqlite3
+131
View File
@@ -0,0 +1,131 @@
# LocalMiniDrama 桌面客户端
基于 Electron 的本地桌面应用,内嵌 `backend-node``frontweb`,打包为 Windows exe / macOS dmg 后可直接运行。当前版本:**v1.2.7**
---
## 主要功能(v1.2.7
| 模块 | 功能 |
|------|------|
| 首页(项目列表) | 创建/打开剧集项目;素材库(角色/场景/道具全局复用);AI 配置;明暗主题切换 |
| 剧集管理页 | 管理剧集信息(标题/风格/比例);分集列表(新增/删除/预览剧本);本剧资源库(角色/场景/道具按剧过滤);从素材库导入资源 |
| 制作页(分集) | 剧本编辑、角色/场景/道具 AI 生成与图片管理;分镜脚本生成与逐镜编辑(图片提示词、视频提示词) |
| 分镜全能模式 | 分镜可在**经典**与**全能模式**间切换;全能模式中间为**片段描述**(`@图片1`… 多图参考),配合 AI 配置中 **`volcengine_omni`Seedance 2.0** 或 **`kling_omni`(可灵 Omni)**;生视频前校验模型匹配;支持「根据分镜生成提示词」 |
| 尾帧衔接 / 导出分镜表 | **尾帧衔接**:提取本镜视频末帧设为下一镜首帧;**导出分镜表**:HTML 表格导出当前集全部镜头字段 |
| 生成任务进度 | 角色 / 场景 / 道具 / 分镜图 / 视频任务统一轮询与恢复(`generationTaskStore` |
| 分镜图生成 | **相机角度视角**:仰视/俯视/侧面/背面角度自动影响背景透视;**四宫格序列图**:一键生成 2×2 四帧序列参考图,自动拆分面板,随时切换主分镜图 |
| 一键流水线 | **一键生成视频**:全流程自动执行;**补全并生成**:仅生成缺失内容,自动跳过已有 |
| 图片/视频生成 | 支持 DashScope、Volcengine、Gemini 等多种 API;生成失败自动重试 3 次;错误信息持久显示 |
| 合成视频 | 将所有分镜视频合成为完整剧集 |
| 主题 | 支持暗色模式(默认)与浅色模式,偏好持久保存 |
---
## 开发运行
1. 确保已构建前端(否则窗口内会显示「请先构建前端」提示):
```bash
cd ../frontweb && npm install && npm run build
```
2. 安装依赖并启动 Electron
```bash
cd desktop
npm install
npm start
```
开发时后端工作目录为 `backend-node/`,配置与数据使用仓库内路径。
---
## 打包为 exe
在 `desktop` 目录下执行:
```bash
cd desktop
npm install
npm run dist
```
**国内网络**:若从 GitHub 下载 Electron 或 winCodeSign 超时,使用国内镜像:
```bash
npm run dist:cn
```
本目录下的 `.npmrc` 已配置 `registry=https://registry.npmmirror.com``npm install` 会使用国内源;`dist:cn` 脚本会将 Electron 与 electron-builder 的二进制下载也切换到 npmmirror 镜像。
产物在 `desktop/release/` 下:
| 文件 | 说明 |
|------|------|
| `LocalMiniDrama Setup x.x.x.exe` | NSIS 安装包(有安装引导,可选安装目录) |
| `LocalMiniDrama x.x.x.exe` | 便携版(单文件,无需安装,双击即用) |
首次运行时,会在用户数据目录(如 `%APPDATA%/LocalMiniDrama`)下生成 `backend/`,包含 `configs/config.yaml`(从 example 复制)和 `data/`(数据库与文件存储),按需修改配置即可。
---
## 脚本说明
| 脚本 | 说明 |
|------|------|
| `npm start` | 启动 Electron(开发模式) |
| `npm run build:front` | 仅构建前端(frontweb |
| `npm run copy-front` | 将 frontweb/dist 复制到 desktop/frontweb-dist(打包前置步骤) |
| `npm run pack` | 构建前端 + 复制 + 打出未压缩目录(便于检查打包内容) |
| `npm run dist` | 构建前端 + 复制 + 打出 Windows 安装包与便携 exe |
| `npm run dist:cn` | 同上,使用国内镜像(Electron、electron-builder 二进制) |
| `npm run prepare-backend` | 将 backend-node 复制到 backend-app(打包前置步骤) |
| `bash dist-mac.sh` | macOS 一键打包(完整版 + 纯净版 DMG,含国内镜像加速) |
---
## 打包后如何看日志 / 调试
### 1. 查看后端日志文件(推荐)
双击运行 exe 时,后端日志会自动写入:
```
%APPDATA%\LocalMiniDrama\backend\logs\app.log
```
用记事本或 VS Code 打开后,点击「AI 生成角色」等按钮,查看是否有对应请求行、报错信息,便于判断是请求未发出、AI 超时还是配置有误。
### 2. 从命令行运行(实时日志)
```powershell
& "D:\path\to\release\LocalMiniDrama 1.2.7.exe"
```
日志会直接打印在终端,操作软件时可实时看到所有输出。
### 3. 打开前端开发者工具
```powershell
$env:LOCALMINIDRAMA_DEVTOOLS=1
& "D:\path\to\release\LocalMiniDrama 1.2.7.exe"
```
在 Network 面板查看各 API 请求(如 `POST /api/v1/generation/characters`)是否正常发出和返回。
### 4. 确认配置与网络
配置文件位于:
```
%APPDATA%\LocalMiniDrama\backend\configs\config.yaml
```
AI 相关配置需在软件「AI 配置」弹窗中填写并保存(会写入上述 yaml 文件);本机网络需能访问对应 API(如 dashscope、volcengine 等)。
---
## 依赖
- Node.js >= 18
- 本仓库中的 `backend-node`(打包时通过 `prepare-backend` 复制到 `backend-app`
- 前端需先在 `frontweb` 目录执行 `npm run build`,再打包或开发运行
+4
View File
@@ -0,0 +1,4 @@
@echo off
cd /d "%~dp0"
npm run dist:cn
pause
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# macOS 打包脚本(完整版 + 纯净版 DMG)
# 用法:在 desktop/ 目录下执行 bash dist-mac.sh
# 或先授权:chmod +x dist-mac.sh && ./dist-mac.sh
set -e
# 使用国内镜像加速 Electron 下载
export ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
export ELECTRON_BUILDER_BINARIES_MIRROR="https://cdn.npmmirror.com/binaries/electron-builder-binaries/"
# 禁用 macOS 代码签名(无证书时跳过签名流程)
export CSC_IDENTITY_AUTO_DISCOVERY=false
# 切换到 desktop 目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo ""
echo "========== [1/2] 构建完整版(含示例资源)=========="
echo ""
# 准备后端 + 编译前端 + 复制前端产物 + electron-builder 打包
npm run prepare-backend
npm run build:front
npm run copy-front
npx electron-builder --mac --config electron-builder-mac.json
echo ""
echo "========== [2/2] 构建纯净版(不含示例资源)=========="
echo ""
# 前端/后端已准备好,直接再打一次 lite 包
npx electron-builder --mac --config electron-builder-mac-lite.json
echo ""
echo "========== 全部构建完成 =========="
echo "输出目录:release/"
echo " 完整版(Intel):LocalMiniDrama-x.x.x-mac-x64.dmg"
echo " 完整版(ARM LocalMiniDrama-x.x.x-mac-arm64.dmg"
echo " 纯净版(Intel):LocalMiniDrama-Lite-x.x.x-mac-x64.dmg"
echo " 纯净版(ARM LocalMiniDrama-Lite-x.x.x-mac-arm64.dmg"
echo ""
+16
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
directories:
output: dist
buildResources: build
artifactName: LocalMiniDrama-${buildVersion}.${ext}
files: []
electronVersion: 28.3.3
+41
View File
@@ -0,0 +1,41 @@
{
"appId": "com.localminidrama.desktop",
"productName": "LocalMiniDrama",
"directories": {
"output": "release"
},
"files": [
"main.js",
"package.json",
"backend-app/**/*",
"node_modules/**/*"
],
"asarUnpack": [
"node_modules/better-sqlite3/**",
"node_modules/sharp/**",
"backend-app/tools/**"
],
"extraResources": [
{
"from": "frontweb-dist",
"to": "frontweb/dist",
"filter": ["**/*"]
},
{
"from": "../backend-node/tools/ffmpeg",
"to": "ffmpeg",
"filter": ["**/*"]
}
],
"win": {
"target": ["nsis", "portable"],
"icon": null,
"signAndEditExecutable": false,
"artifactName": "${productName}-Lite-${version}.${ext}"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"artifactName": "${productName}-Lite-Setup-${version}.${ext}"
}
}
+41
View File
@@ -0,0 +1,41 @@
{
"appId": "com.localminidrama.desktop",
"productName": "LocalMiniDrama",
"directories": {
"output": "release"
},
"files": [
"main.js",
"package.json",
"backend-app/**/*",
"node_modules/**/*"
],
"asarUnpack": [
"node_modules/better-sqlite3/**",
"backend-app/tools/**"
],
"extraResources": [
{
"from": "frontweb-dist",
"to": "frontweb/dist",
"filter": ["**/*"]
},
{
"from": "ffmpeg-mac",
"to": "ffmpeg",
"filter": ["**/*"]
}
],
"mac": {
"target": [
{ "target": "dmg", "arch": ["x64", "arm64"] }
],
"category": "public.app-category.entertainment",
"icon": null,
"identity": null
},
"dmg": {
"title": "${productName} Lite ${version}",
"artifactName": "${productName}-Lite-${version}-mac-${arch}.dmg"
}
}
+46
View File
@@ -0,0 +1,46 @@
{
"appId": "com.localminidrama.desktop",
"productName": "LocalMiniDrama",
"directories": {
"output": "release"
},
"files": [
"main.js",
"package.json",
"backend-app/**/*",
"node_modules/**/*"
],
"asarUnpack": [
"node_modules/better-sqlite3/**",
"backend-app/tools/**"
],
"extraResources": [
{
"from": "frontweb-dist",
"to": "frontweb/dist",
"filter": ["**/*"]
},
{
"from": "../example_drama",
"to": "example_drama",
"filter": ["**/*"]
},
{
"from": "ffmpeg-mac",
"to": "ffmpeg",
"filter": ["**/*"]
}
],
"mac": {
"target": [
{ "target": "dmg", "arch": ["x64", "arm64"] }
],
"category": "public.app-category.entertainment",
"icon": null,
"identity": null
},
"dmg": {
"title": "${productName} ${version}",
"artifactName": "${productName}-${version}-mac-${arch}.dmg"
}
}
+9
View File
@@ -0,0 +1,9 @@
将 macOS 版 ffmpeg 可执行文件放在本目录:
ffmpeg-mac/ffmpeg
ffmpeg-mac/ffprobe (可选,建议一并放入)
推荐使用 evermeet.cx 的静态构建版本,解压后放入此目录,注意需要有可执行权限:
chmod +x ffmpeg ffprobe
构建 dmg 后,这两个文件会随安装包分发;用户首次启动时自动复制到:
~/Library/Application Support/localminidrama-desktop/backend/tools/ffmpeg/
+273
View File
@@ -0,0 +1,273 @@
const { app, BrowserWindow, Menu } = require('electron');
const path = require('path');
const fs = require('fs');
// 显式固定 userData 目录,使开发模式与打包 exe 路径完全一致,防止 productName 变更导致路径漂移
const USERDATA_DIR = path.join(app.getPath('appData'), 'localminidrama-desktop');
app.setPath('userData', USERDATA_DIR);
const MAIN_STARTUP_LOG = path.join(USERDATA_DIR, 'main-startup.log');
function writeMainLog(msg) {
const line = `${new Date().toISOString()} ${msg}\n`;
try {
if (!fs.existsSync(USERDATA_DIR)) fs.mkdirSync(USERDATA_DIR, { recursive: true });
fs.appendFileSync(MAIN_STARTUP_LOG, line);
} catch (_) {}
}
process.on('uncaughtException', (err) => {
writeMainLog(`uncaughtException: ${err && err.stack ? err.stack : err}`);
});
process.on('unhandledRejection', (reason) => {
const text = reason instanceof Error ? reason.stack : String(reason);
writeMainLog(`unhandledRejection: ${text}`);
});
writeMainLog(`main.js loaded packaged=${app.isPackaged} exec=${process.execPath}`);
// 兼容迁移:若旧路径 LocalMiniDrama 有数据而新路径为空,自动迁移
;(function migrateOldUserData() {
const oldPath = path.join(app.getPath('appData'), 'LocalMiniDrama');
if (fs.existsSync(oldPath) && !fs.existsSync(USERDATA_DIR)) {
try {
fs.renameSync(oldPath, USERDATA_DIR);
} catch (e) {
// rename 跨驱动器时会失败,此时静默忽略,用户数据仍可手动迁移
}
}
})();
const BACKEND_APP_PATH = path.join(__dirname, 'backend-app');
const BACKEND_NODE_PATH = path.join(__dirname, '..', 'backend-node');
const DEFAULT_PORT = 5679;
let serverInstance = null;
/** 开发模式用 backend-node(改代码即生效);打包后用 backend-app */
function getBackendModulePath() {
if (app.isPackaged) return BACKEND_APP_PATH;
// Electron 开发模式必须用 backend-apprequire 会向上解析到 desktop/node_modules
// 其中 better-sqlite3 已由 postinstall 的 electron-rebuild 对准当前 Electron ABI。
// 若直接用 backend-node,则会加载 backend-node/node_modules(多为本机 Node 编的 ABI,必炸)。
if (process.versions.electron && fs.existsSync(path.join(BACKEND_APP_PATH, 'src', 'app.js'))) {
return BACKEND_APP_PATH;
}
return fs.existsSync(BACKEND_NODE_PATH) ? BACKEND_NODE_PATH : BACKEND_APP_PATH;
}
function getBackendCwd() {
if (app.isPackaged) {
return path.join(app.getPath('userData'), 'backend');
}
return getBackendModulePath();
}
function ensureBackendCwd(backendCwd) {
if (!fs.existsSync(backendCwd)) {
fs.mkdirSync(backendCwd, { recursive: true });
}
const configsDir = path.join(backendCwd, 'configs');
const dataDir = path.join(backendCwd, 'data');
const logsDir = path.join(backendCwd, 'logs');
const configPath = path.join(configsDir, 'config.yaml');
if (!fs.existsSync(configsDir)) fs.mkdirSync(configsDir, { recursive: true });
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
// 首次安装时,从打包内置的 config.yaml 复制到用户数据目录
const bundledConfig = path.join(getBackendModulePath(), 'configs', 'config.yaml');
if (!fs.existsSync(configPath) && fs.existsSync(bundledConfig)) {
fs.copyFileSync(bundledConfig, configPath);
}
// 每次启动时,将内置 config.yaml 中的 vendor_lock 节强制同步到用户 config.yaml
// 确保打包时配置的锁定策略对所有用户生效,不受首次安装后遗留旧配置影响。
if (fs.existsSync(bundledConfig) && fs.existsSync(configPath)) {
try {
const yaml = require('js-yaml');
const userCfg = yaml.load(fs.readFileSync(configPath, 'utf8')) || {};
const bundledCfg = yaml.load(fs.readFileSync(bundledConfig, 'utf8')) || {};
if (bundledCfg.vendor_lock !== undefined) {
userCfg.vendor_lock = bundledCfg.vendor_lock;
fs.writeFileSync(configPath, yaml.dump(userCfg, { lineWidth: -1 }), 'utf8');
}
} catch (e) {
console.warn('[config] Failed to sync vendor_lock from bundled config:', e.message);
}
}
}
/**
* 首次启动时,将打包内置的 ffmpeg 自动复制到 userData/backend/tools/ffmpeg/。
* 来源:process.resourcesPath/ffmpeg/(由 electron-builder extraResources 写入)。
* 已存在则跳过,不会重复覆盖,也不影响用户手动替换版本。
*/
function ensureFfmpeg(backendCwd) {
if (!app.isPackaged) return;
const isWin = process.platform === 'win32';
const ffmpegName = isWin ? 'ffmpeg.exe' : 'ffmpeg';
const ffprobeName = isWin ? 'ffprobe.exe' : 'ffprobe';
const destDir = path.join(backendCwd, 'tools', 'ffmpeg');
const destFfmpeg = path.join(destDir, ffmpegName);
// 已存在则跳过(支持用户手动替换)
if (fs.existsSync(destFfmpeg)) {
console.log('[ffmpeg] Already exists at', destFfmpeg);
return;
}
const srcDir = path.join(process.resourcesPath, 'ffmpeg');
const srcFfmpeg = path.join(srcDir, ffmpegName);
if (!fs.existsSync(srcFfmpeg)) {
console.warn(
'[ffmpeg] Bundled ffmpeg not found, skipping auto-extract. Expected:',
srcFfmpeg,
'(打包前请将 ffmpeg.exe 放入 backend-node/tools/ffmpeg,并确保 package.json 的 extraResources 包含该目录)'
);
return;
}
try {
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
fs.copyFileSync(srcFfmpeg, destFfmpeg);
if (!isWin) fs.chmodSync(destFfmpeg, 0o755);
const srcFfprobe = path.join(srcDir, ffprobeName);
if (fs.existsSync(srcFfprobe)) {
const destFfprobe = path.join(destDir, ffprobeName);
fs.copyFileSync(srcFfprobe, destFfprobe);
if (!isWin) fs.chmodSync(destFfprobe, 0o755);
}
console.log('[ffmpeg] Auto-extracted to', destDir);
} catch (e) {
console.warn('[ffmpeg] Auto-extract failed:', e.message);
}
}
function getWebDistPath() {
if (app.isPackaged) {
return path.join(process.resourcesPath, 'frontweb', 'dist');
}
return path.join(__dirname, '..', 'frontweb', 'dist');
}
/**
* 探测端口是否空闲:优先使用 preferredPort,被占用时让 OS 分配一个随机空闲端口。
* 返回最终可用的端口号。
*/
function findFreePort(preferredPort) {
const net = require('net');
return new Promise((resolve) => {
const probe = net.createServer();
probe.once('error', () => {
// 首选端口被占,让 OS 随机分配
const fallback = net.createServer();
fallback.listen(0, '127.0.0.1', () => {
const port = fallback.address().port;
fallback.close(() => resolve(port));
});
});
probe.listen(preferredPort, '127.0.0.1', () => {
probe.close(() => resolve(preferredPort));
});
});
}
function createWindow(port) {
Menu.setApplicationMenu(null);
const win = new BrowserWindow({
width: 1280,
height: 800,
webPreferences: { nodeIntegration: false, contextIsolation: true },
show: false,
});
win.once('ready-to-show', () => {
win.show();
writeMainLog('window ready-to-show');
});
// 若页面长期不触发 ready-to-show,避免用户误以为“点了没反应”
setTimeout(() => {
if (!win.isDestroyed() && !win.isVisible()) {
win.show();
writeMainLog('window shown (fallback timeout, check page load)');
}
}, 8000);
win.webContents.on('did-fail-load', (_e, code, desc, url) => {
writeMainLog(`did-fail-load code=${code} desc=${desc} url=${url}`);
});
writeMainLog(`createWindow loadURL http://127.0.0.1:${port}`);
win.loadURL(`http://127.0.0.1:${port}`);
win.on('closed', () => app.quit());
if (process.env.LOCALMINIDRAMA_DEVTOOLS === '1') {
win.webContents.openDevTools();
}
}
/** 后端始终在主进程内运行(打包用子进程会重复启动 exe 导致大量进程,故取消) */
async function startBackend() {
const backendCwd = getBackendCwd();
ensureBackendCwd(backendCwd);
ensureFfmpeg(backendCwd);
process.env.WEB_DIST_PATH = getWebDistPath();
if (app.isPackaged) {
process.env.LOG_FILE = path.join(backendCwd, 'logs', 'app.log');
process.env.EXAMPLE_DRAMA_PATH = path.join(process.resourcesPath, 'example_drama');
} else {
process.env.EXAMPLE_DRAMA_PATH = path.join(__dirname, '..', 'example_drama');
}
process.chdir(backendCwd);
const backendModulePath = getBackendModulePath();
try {
require(path.join(backendModulePath, 'src', 'db', 'migrate.js'));
} catch (err) {
console.warn('Migration warning:', err.message);
}
const { createApp } = require(path.join(backendModulePath, 'src', 'app.js'));
const { createServer } = require('http');
const { app: expressApp, config } = createApp();
const preferredPort = config.server?.port || DEFAULT_PORT;
// 自动探测空闲端口:优先默认端口,被占时由 OS 分配,支持多实例同时运行
const port = await findFreePort(preferredPort);
if (port !== preferredPort) {
console.log(`Port ${preferredPort} in use, using ${port}`);
}
return new Promise((resolve, reject) => {
const server = createServer(expressApp);
serverInstance = server;
server.on('error', reject);
server.listen(port, '127.0.0.1', () => {
console.log('Backend listening on', port);
resolve(port);
});
});
}
app.whenReady().then(async () => {
writeMainLog('app.whenReady');
let port;
try {
port = await startBackend();
writeMainLog(`startBackend ok port=${port}`);
} catch (err) {
const stack = err && err.stack ? err.stack : String(err);
writeMainLog(`Failed to start backend\n${stack}`);
console.error('Failed to start backend', err);
app.quit();
return;
}
// startBackend 的 Promise 在 listen 回调中 resolve,服务器此时已就绪,直接建窗口
createWindow(port);
});
app.on('before-quit', () => {
if (serverInstance) {
serverInstance.close();
serverInstance = null;
}
});
+6732
View File
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
{
"name": "localminidrama-desktop",
"version": "1.2.7",
"description": "LocalMiniDrama 本地桌面客户端",
"main": "main.js",
"author": "LocalMiniDrama",
"license": "MIT",
"scripts": {
"prestart": "node scripts/copy-backend.js",
"start": "electron .",
"rebuild:backend-native": "electron-rebuild -f --only better-sqlite3 --project-dir ../backend-node",
"build:front": "cd ../frontweb && npm run build",
"copy-front": "node scripts/copy-front.js",
"pack": "npm run prepare-backend && npm run build:front && npm run copy-front && electron-builder --dir",
"dist": "npm run prepare-backend && npm run build:front && npm run copy-front && electron-builder --win",
"dist:cn": "node scripts/dist-cn.js",
"dist:mac": "bash dist-mac.sh",
"postinstall": "node scripts/copy-backend.js && electron-rebuild && electron-rebuild -f --only better-sqlite3 --project-dir ../backend-node",
"prepare-backend": "node scripts/copy-backend.js"
},
"dependencies": {
"adm-zip": "^0.5.16",
"better-sqlite3": "^11.6.0",
"cors": "^2.8.5",
"express": "^4.21.0",
"js-yaml": "^4.1.0",
"jsonrepair": "^3.13.3",
"jsonwebtoken": "^9.0.3",
"multer": "^1.4.5-lts.1",
"sharp": "^0.34.5",
"uuid": "^10.0.0"
},
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.1",
"electron-rebuild": "^3.2.9"
},
"build": {
"appId": "com.localminidrama.desktop",
"productName": "本地短剧助手",
"artifactName": "LocalMiniDrama ${version}.${ext}",
"directories": {
"output": "release"
},
"files": [
"main.js",
"package.json",
"backend-app/**/*",
"node_modules/**/*"
],
"asarUnpack": [
"node_modules/better-sqlite3/**",
"node_modules/sharp/**"
],
"extraResources": [
{
"from": "frontweb-dist",
"to": "frontweb/dist",
"filter": ["**/*"]
},
{
"from": "../example_drama",
"to": "example_drama",
"filter": ["**/*"]
},
{
"from": "../backend-node/tools/ffmpeg",
"to": "ffmpeg",
"filter": ["**/*"]
}
],
"win": {
"target": ["nsis", "portable"],
"icon": null,
"signAndEditExecutable": false
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
}
}
+39
View File
@@ -0,0 +1,39 @@
const fs = require('fs');
const path = require('path');
const repoRoot = path.join(__dirname, '..', '..');
const src = path.join(repoRoot, 'backend-node');
const dest = path.join(__dirname, '..', 'backend-app');
const dirsToCopy = ['src', 'configs', 'scripts', 'migrations'];
if (!fs.existsSync(src)) {
console.error('backend-node not found at', src);
process.exit(1);
}
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true });
fs.mkdirSync(dest, { recursive: true });
for (const dir of dirsToCopy) {
const from = path.join(src, dir);
const to = path.join(dest, dir);
if (fs.existsSync(from)) {
fs.cpSync(from, to, { recursive: true });
}
}
// 合并 desktop 自带的初始迁移(保证 01_init、02_add_default_model 等存在)
const migrationsDest = path.join(dest, 'migrations');
const initialMigrations = path.join(__dirname, 'initial-migrations');
if (!fs.existsSync(migrationsDest)) fs.mkdirSync(migrationsDest, { recursive: true });
if (fs.existsSync(initialMigrations)) {
for (const f of fs.readdirSync(initialMigrations)) {
if (f.endsWith('.sql')) {
fs.copyFileSync(path.join(initialMigrations, f), path.join(migrationsDest, f));
}
}
console.log('Merged initial-migrations -> desktop/backend-app/migrations');
}
console.log('Copied backend-node (src, configs, scripts, migrations) -> desktop/backend-app');
+14
View File
@@ -0,0 +1,14 @@
const fs = require('fs');
const path = require('path');
const repoRoot = path.join(__dirname, '..', '..');
const src = path.join(repoRoot, 'frontweb', 'dist');
const dest = path.join(__dirname, '..', 'frontweb-dist');
if (!fs.existsSync(src)) {
console.error('frontweb/dist not found. Run: cd frontweb && npm run build');
process.exit(1);
}
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true });
fs.cpSync(src, dest, { recursive: true });
console.log('Copied frontweb/dist -> desktop/frontweb-dist');
+43
View File
@@ -0,0 +1,43 @@
process.env.ELECTRON_MIRROR = 'https://npmmirror.com/mirrors/electron/';
process.env.ELECTRON_BUILDER_BINARIES_MIRROR = 'https://cdn.npmmirror.com/binaries/electron-builder-binaries/';
const { spawnSync } = require('child_process');
const path = require('path');
const isWin = process.platform === 'win32';
const cwd = path.join(__dirname, '..');
// 第一步:完整构建(含示例资源),前端/后端同时准备
console.log('\n========== [1/2] 构建完整版(含示例资源)==========\n');
const full = spawnSync(isWin ? 'npm.cmd' : 'npm', ['run', 'dist'], {
stdio: 'inherit',
shell: isWin,
cwd,
});
if (full.status !== 0) {
console.error('完整版构建失败,终止。');
process.exit(full.status || 1);
}
// 第二步:纯净版构建(不含示例资源),前端/后端已准备好,直接调 electron-builder
console.log('\n========== [2/2] 构建纯净版(不含示例资源)==========\n');
const lite = spawnSync(
isWin ? 'npx.cmd' : 'npx',
['electron-builder', '--win', '--config', 'electron-builder-lite.json'],
{
stdio: 'inherit',
shell: isWin,
cwd,
}
);
if (lite.status !== 0) {
console.error('纯净版构建失败。');
process.exit(lite.status || 1);
}
console.log('\n========== 全部构建完成 ==========');
console.log('输出目录:release/');
console.log(' 完整版安装包:LocalMiniDrama Setup x.x.x.exe');
console.log(' 完整版便携版:LocalMiniDrama x.x.x.exe');
console.log(' 纯净版安装包:LocalMiniDrama-Lite-Setup-x.x.x.exe');
console.log(' 纯净版便携版:LocalMiniDrama-Lite-x.x.x.exe\n');
process.exit(0);
@@ -0,0 +1,294 @@
-- 最小初始表结构,与 backend-node 业务代码对齐(若无 backend-node/migrations 则使用本文件)
CREATE TABLE IF NOT EXISTS dramas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT '',
description TEXT,
genre TEXT,
style TEXT DEFAULT 'realistic',
tags TEXT,
thumbnail TEXT,
total_episodes INTEGER DEFAULT 1,
total_duration INTEGER DEFAULT 0,
status TEXT DEFAULT 'draft',
metadata TEXT,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS episodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
drama_id INTEGER NOT NULL,
episode_number INTEGER DEFAULT 0,
title TEXT DEFAULT '',
script_content TEXT,
description TEXT,
duration INTEGER DEFAULT 0,
video_url TEXT,
thumbnail TEXT,
status TEXT DEFAULT 'draft',
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS storyboards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
episode_id INTEGER NOT NULL,
scene_id INTEGER,
storyboard_number INTEGER DEFAULT 0,
title TEXT,
description TEXT,
location TEXT,
time TEXT,
duration REAL,
dialogue TEXT,
action TEXT,
atmosphere TEXT,
image_prompt TEXT,
video_prompt TEXT,
characters TEXT,
shot_type TEXT,
angle TEXT,
movement TEXT,
video_url TEXT,
status TEXT DEFAULT 'draft',
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS characters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
drama_id INTEGER NOT NULL,
name TEXT NOT NULL DEFAULT '',
role TEXT,
description TEXT,
personality TEXT,
appearance TEXT,
image_url TEXT,
local_path TEXT,
voice_style TEXT,
sort_order INTEGER DEFAULT 0,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS episode_characters (
episode_id INTEGER NOT NULL,
character_id INTEGER NOT NULL,
PRIMARY KEY (episode_id, character_id)
);
CREATE TABLE IF NOT EXISTS scenes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
drama_id INTEGER NOT NULL,
episode_id INTEGER,
location TEXT,
time TEXT,
prompt TEXT,
image_url TEXT,
local_path TEXT,
storyboard_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'draft',
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS props (
id INTEGER PRIMARY KEY AUTOINCREMENT,
drama_id INTEGER NOT NULL,
name TEXT NOT NULL DEFAULT '',
type TEXT,
description TEXT,
prompt TEXT,
image_url TEXT,
local_path TEXT,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS storyboard_props (
storyboard_id INTEGER NOT NULL,
prop_id INTEGER NOT NULL,
PRIMARY KEY (storyboard_id, prop_id)
);
CREATE TABLE IF NOT EXISTS frame_prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
storyboard_id INTEGER NOT NULL,
frame_type TEXT,
prompt TEXT,
description TEXT,
layout TEXT,
created_at TEXT,
updated_at TEXT
);
CREATE TABLE IF NOT EXISTS ai_service_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service_type TEXT NOT NULL,
provider TEXT DEFAULT '',
name TEXT DEFAULT '',
base_url TEXT DEFAULT '',
api_key TEXT,
model TEXT,
default_model TEXT,
endpoint TEXT,
query_endpoint TEXT,
priority INTEGER DEFAULT 0,
is_default INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
settings TEXT,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS async_tasks (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
status TEXT NOT NULL,
progress INTEGER DEFAULT 0,
message TEXT,
resource_id TEXT,
created_at TEXT,
updated_at TEXT,
completed_at TEXT,
error TEXT,
result TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS image_generations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
storyboard_id INTEGER,
drama_id INTEGER,
scene_id INTEGER,
character_id INTEGER,
provider TEXT,
prompt TEXT,
negative_prompt TEXT,
model TEXT,
frame_type TEXT,
reference_images TEXT,
size TEXT,
quality TEXT,
image_url TEXT,
local_path TEXT,
status TEXT,
task_id TEXT,
completed_at TEXT,
error_msg TEXT,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS video_generations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
drama_id INTEGER,
storyboard_id INTEGER,
provider TEXT,
prompt TEXT,
model TEXT,
duration REAL,
aspect_ratio TEXT,
image_url TEXT,
first_frame_url TEXT,
last_frame_url TEXT,
reference_image_urls TEXT,
video_url TEXT,
local_path TEXT,
status TEXT,
task_id TEXT,
scene_id INTEGER,
completed_at TEXT,
error_msg TEXT,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS video_merges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
episode_id INTEGER,
drama_id INTEGER,
title TEXT,
provider TEXT,
model TEXT,
status TEXT,
scenes TEXT,
task_id TEXT,
created_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS character_libraries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '',
category TEXT,
image_url TEXT,
local_path TEXT,
description TEXT,
tags TEXT,
source_type TEXT,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS scene_libraries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location TEXT NOT NULL DEFAULT '',
time TEXT,
prompt TEXT,
description TEXT,
image_url TEXT,
local_path TEXT,
category TEXT,
tags TEXT,
source_type TEXT,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS prop_libraries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '',
description TEXT,
prompt TEXT,
image_url TEXT,
local_path TEXT,
category TEXT,
tags TEXT,
source_type TEXT,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
CREATE TABLE IF NOT EXISTS assets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
drama_id INTEGER,
name TEXT,
type TEXT,
category TEXT,
url TEXT,
local_path TEXT,
file_size INTEGER,
mime_type TEXT,
width INTEGER,
height INTEGER,
duration REAL,
image_gen_id INTEGER,
video_gen_id INTEGER,
created_at TEXT,
updated_at TEXT,
deleted_at TEXT
);
@@ -0,0 +1,2 @@
-- 为已有库增加 default_model 列(新建库 01 已包含则跳过)
ALTER TABLE ai_service_configs ADD COLUMN default_model TEXT;
@@ -0,0 +1,2 @@
-- 道具归属集:从某集剧本提取的道具记入该集,本集资源列表会展示
ALTER TABLE props ADD COLUMN episode_id INTEGER;
@@ -0,0 +1,4 @@
-- async_tasks 缺少 completed_at、error、result 时补上(与 taskService 一致)
ALTER TABLE async_tasks ADD COLUMN completed_at TEXT;
ALTER TABLE async_tasks ADD COLUMN error TEXT;
ALTER TABLE async_tasks ADD COLUMN result TEXT;
@@ -0,0 +1,3 @@
-- image_generations 缺少 completed_at / error_msg 时补上
ALTER TABLE image_generations ADD COLUMN completed_at TEXT;
ALTER TABLE image_generations ADD COLUMN error_msg TEXT;
@@ -0,0 +1,2 @@
-- characters 表缺少 local_path 时补上
ALTER TABLE characters ADD COLUMN local_path TEXT;
@@ -0,0 +1,3 @@
-- scenes 表缺少 image_url / local_path 时补上
ALTER TABLE scenes ADD COLUMN image_url TEXT;
ALTER TABLE scenes ADD COLUMN local_path TEXT;