fix
This commit is contained in:
256
docs/COPY_FEATURE_DEMO.md
Normal file
256
docs/COPY_FEATURE_DEMO.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# 模板导入日志复制功能演示
|
||||||
|
|
||||||
|
## 功能演示
|
||||||
|
|
||||||
|
### 1. 基础界面
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 模板导入进度 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 进度条: ████████████████████████████████████████ 100% │
|
||||||
|
│ 状态: 导入完成 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 导入结果: ✅ 成功导入 8 个模板 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 导入日志 [复制日志] [▼] │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ [14:30:20] 开始扫描模板目录... │ │
|
||||||
|
│ │ [14:30:21] 发现 10 个模板文件 │ │
|
||||||
|
│ │ [14:30:22] 开始导入模板: template1 │ │
|
||||||
|
│ │ [14:30:23] ✅ 成功导入: template1 │ │
|
||||||
|
│ │ [14:30:24] 开始导入模板: template2 │ │
|
||||||
|
│ │ [14:30:25] ✅ 成功导入: template2 │ │
|
||||||
|
│ │ ... │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
│ [关闭] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 复制按钮状态
|
||||||
|
|
||||||
|
#### 默认状态
|
||||||
|
```
|
||||||
|
[📋 复制日志] [▼]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 复制成功状态(2秒后自动恢复)
|
||||||
|
```
|
||||||
|
[✅ 已复制] [▼]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 下拉菜单展开
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 导入日志 [复制日志] [▲] │
|
||||||
|
│ ┌─────────────────┐ │
|
||||||
|
│ │ 复制详细日志 │ │
|
||||||
|
│ │ 仅复制日志内容 │ │
|
||||||
|
│ └─────────────────┘ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 日志内容... │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 复制内容示例
|
||||||
|
|
||||||
|
### 详细格式复制内容
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 模板导入日志 ===
|
||||||
|
导出时间: 2024-01-15 14:30:25
|
||||||
|
当前进度: 导入模板 - 导入完成 (100.0%)
|
||||||
|
导入结果: 成功 - 导入完成 (成功导入 8 个模板)
|
||||||
|
=== 详细日志 ===
|
||||||
|
|
||||||
|
[2024-01-15 14:30:20] 开始扫描模板目录...
|
||||||
|
[2024-01-15 14:30:21] 发现 10 个模板文件
|
||||||
|
[2024-01-15 14:30:22] 开始导入模板: template1
|
||||||
|
[2024-01-15 14:30:23] ✅ 成功导入: template1
|
||||||
|
[2024-01-15 14:30:24] 开始导入模板: template2
|
||||||
|
[2024-01-15 14:30:25] ✅ 成功导入: template2
|
||||||
|
[2024-01-15 14:30:26] 开始导入模板: template3
|
||||||
|
[2024-01-15 14:30:27] ✅ 成功导入: template3
|
||||||
|
[2024-01-15 14:30:28] 开始导入模板: template4
|
||||||
|
[2024-01-15 14:30:29] ✅ 成功导入: template4
|
||||||
|
[2024-01-15 14:30:30] 开始导入模板: template5
|
||||||
|
[2024-01-15 14:30:31] ✅ 成功导入: template5
|
||||||
|
[2024-01-15 14:30:32] 开始导入模板: template6
|
||||||
|
[2024-01-15 14:30:33] ✅ 成功导入: template6
|
||||||
|
[2024-01-15 14:30:34] 开始导入模板: template7
|
||||||
|
[2024-01-15 14:30:35] ✅ 成功导入: template7
|
||||||
|
[2024-01-15 14:30:36] 开始导入模板: template8
|
||||||
|
[2024-01-15 14:30:37] ✅ 成功导入: template8
|
||||||
|
[2024-01-15 14:30:38] 跳过重复模板: template9 (已存在)
|
||||||
|
[2024-01-15 14:30:39] ❌ 导入失败: template10 - 文件格式错误
|
||||||
|
[2024-01-15 14:30:40] 导入完成,成功: 8,失败: 1,跳过: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 简单格式复制内容
|
||||||
|
|
||||||
|
```
|
||||||
|
[2024-01-15 14:30:20] 开始扫描模板目录...
|
||||||
|
[2024-01-15 14:30:21] 发现 10 个模板文件
|
||||||
|
[2024-01-15 14:30:22] 开始导入模板: template1
|
||||||
|
[2024-01-15 14:30:23] ✅ 成功导入: template1
|
||||||
|
[2024-01-15 14:30:24] 开始导入模板: template2
|
||||||
|
[2024-01-15 14:30:25] ✅ 成功导入: template2
|
||||||
|
[2024-01-15 14:30:26] 开始导入模板: template3
|
||||||
|
[2024-01-15 14:30:27] ✅ 成功导入: template3
|
||||||
|
[2024-01-15 14:30:28] 开始导入模板: template4
|
||||||
|
[2024-01-15 14:30:29] ✅ 成功导入: template4
|
||||||
|
[2024-01-15 14:30:30] 开始导入模板: template5
|
||||||
|
[2024-01-15 14:30:31] ✅ 成功导入: template5
|
||||||
|
[2024-01-15 14:30:32] 开始导入模板: template6
|
||||||
|
[2024-01-15 14:30:33] ✅ 成功导入: template6
|
||||||
|
[2024-01-15 14:30:34] 开始导入模板: template7
|
||||||
|
[2024-01-15 14:30:35] ✅ 成功导入: template7
|
||||||
|
[2024-01-15 14:30:36] 开始导入模板: template8
|
||||||
|
[2024-01-15 14:30:37] ✅ 成功导入: template8
|
||||||
|
[2024-01-15 14:30:38] 跳过重复模板: template9 (已存在)
|
||||||
|
[2024-01-15 14:30:39] ❌ 导入失败: template10 - 文件格式错误
|
||||||
|
[2024-01-15 14:30:40] 导入完成,成功: 8,失败: 1,跳过: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 用户操作流程
|
||||||
|
|
||||||
|
### 场景1:快速复制详细日志
|
||||||
|
|
||||||
|
1. 用户点击模板导入
|
||||||
|
2. 导入进度弹窗显示
|
||||||
|
3. 导入完成后,用户直接点击 **[复制日志]** 按钮
|
||||||
|
4. 系统复制详细格式日志到剪贴板
|
||||||
|
5. 按钮显示 **[✅ 已复制]** 状态
|
||||||
|
6. 2秒后自动恢复为 **[📋 复制日志]**
|
||||||
|
|
||||||
|
### 场景2:选择复制格式
|
||||||
|
|
||||||
|
1. 用户点击模板导入
|
||||||
|
2. 导入进度弹窗显示
|
||||||
|
3. 用户点击下拉箭头 **[▼]** 展开菜单
|
||||||
|
4. 用户选择 **"仅复制日志内容"**
|
||||||
|
5. 系统复制简单格式日志到剪贴板
|
||||||
|
6. 下拉菜单自动关闭
|
||||||
|
7. 主按钮显示 **[✅ 已复制]** 状态
|
||||||
|
|
||||||
|
### 场景3:导入失败时复制错误信息
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 模板导入日志 ===
|
||||||
|
导出时间: 2024-01-15 14:35:12
|
||||||
|
当前进度: 导入模板 - 导入失败 (60.0%)
|
||||||
|
导入结果: 失败 - 部分模板导入失败 (成功导入 3 个模板) (失败 2 个模板)
|
||||||
|
=== 详细日志 ===
|
||||||
|
|
||||||
|
[2024-01-15 14:35:05] 开始扫描模板目录...
|
||||||
|
[2024-01-15 14:35:06] 发现 5 个模板文件
|
||||||
|
[2024-01-15 14:35:07] 开始导入模板: template1
|
||||||
|
[2024-01-15 14:35:08] ✅ 成功导入: template1
|
||||||
|
[2024-01-15 14:35:09] 开始导入模板: template2
|
||||||
|
[2024-01-15 14:35:10] ❌ 导入失败: template2 - 文件损坏,无法读取
|
||||||
|
[2024-01-15 14:35:11] 开始导入模板: template3
|
||||||
|
[2024-01-15 14:35:12] ❌ 导入失败: template3 - 缺少必要的配置文件
|
||||||
|
[2024-01-15 14:35:13] 导入中断,请检查错误信息
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 按钮样式
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 主复制按钮 */
|
||||||
|
.copy-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉按钮 */
|
||||||
|
.dropdown-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
border-left: 1px solid #d1d5db;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下拉菜单 */
|
||||||
|
.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 10;
|
||||||
|
min-width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #374151;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应式设计
|
||||||
|
|
||||||
|
- **桌面端**: 完整的按钮组和下拉菜单
|
||||||
|
- **平板端**: 保持完整功能,调整按钮大小
|
||||||
|
- **移动端**: 简化为单个复制按钮,长按显示选项
|
||||||
|
|
||||||
|
### 无障碍支持
|
||||||
|
|
||||||
|
- **键盘导航**: 支持 Tab 键切换和 Enter 键激活
|
||||||
|
- **屏幕阅读器**: 提供完整的 aria-label 和 title 属性
|
||||||
|
- **高对比度**: 确保在高对比度模式下可见性
|
||||||
|
|
||||||
|
## 测试场景
|
||||||
|
|
||||||
|
### 功能测试
|
||||||
|
- ✅ 有日志时显示复制按钮
|
||||||
|
- ✅ 无日志时隐藏复制按钮
|
||||||
|
- ✅ 详细格式复制正确
|
||||||
|
- ✅ 简单格式复制正确
|
||||||
|
- ✅ 复制状态反馈正确
|
||||||
|
- ✅ 下拉菜单交互正常
|
||||||
|
|
||||||
|
### 兼容性测试
|
||||||
|
- ✅ Chrome 90+
|
||||||
|
- ✅ Firefox 88+
|
||||||
|
- ✅ Safari 14+
|
||||||
|
- ✅ Edge 90+
|
||||||
|
|
||||||
|
### 错误处理测试
|
||||||
|
- ✅ 剪贴板权限被拒绝
|
||||||
|
- ✅ 剪贴板API不可用
|
||||||
|
- ✅ 网络错误情况
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
通过这个复制功能,用户可以轻松地复制和分享模板导入日志,大大提升了调试和问题解决的效率。
|
||||||
280
docs/PYTHON_CORE_DISTRIBUTION.md
Normal file
280
docs/PYTHON_CORE_DISTRIBUTION.md
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
# Python Core 分发解决方案
|
||||||
|
|
||||||
|
## 问题背景
|
||||||
|
|
||||||
|
当前项目使用 `python -m python_core.services.xxx` 的方式调用Python模块,这在开发环境下工作正常,但打包成exe后会遇到以下问题:
|
||||||
|
|
||||||
|
1. **Python环境依赖**:用户机器可能没有安装Python或缺少依赖包
|
||||||
|
2. **模块路径问题**:python_core目录不在用户的Python路径中
|
||||||
|
3. **分发复杂性**:需要用户单独安装Python环境
|
||||||
|
|
||||||
|
## 解决方案:Tauri Sidecar + PyInstaller
|
||||||
|
|
||||||
|
### 方案概述
|
||||||
|
|
||||||
|
使用PyInstaller将Python代码打包成独立可执行文件,作为Tauri的sidecar与主应用一起分发。
|
||||||
|
|
||||||
|
### 优势
|
||||||
|
|
||||||
|
1. **无需Python环境**:用户无需安装Python
|
||||||
|
2. **依赖自包含**:所有Python依赖都打包在内
|
||||||
|
3. **分发简单**:单个exe文件包含所有功能
|
||||||
|
4. **性能优化**:避免Python解释器启动开销
|
||||||
|
5. **安全性**:代码被编译,不易被逆向
|
||||||
|
|
||||||
|
## 实现步骤
|
||||||
|
|
||||||
|
### 1. 创建Python入口文件
|
||||||
|
|
||||||
|
**文件**: `python_core/main.py`
|
||||||
|
|
||||||
|
这是PyInstaller的入口点,提供统一的命令行接口:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 支持模块映射
|
||||||
|
MODULE_MAP = {
|
||||||
|
'template_manager': 'services.template_manager',
|
||||||
|
'project_manager': 'services.project_manager',
|
||||||
|
# ... 更多模块
|
||||||
|
}
|
||||||
|
|
||||||
|
# 统一的执行接口
|
||||||
|
def execute_action(module_name, action, params=None, progress_callback=None):
|
||||||
|
# 动态加载和执行模块函数
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. PyInstaller配置
|
||||||
|
|
||||||
|
**文件**: `python_core/build.spec`
|
||||||
|
|
||||||
|
配置打包选项:
|
||||||
|
- 隐藏导入:确保所有模块被包含
|
||||||
|
- 数据文件:包含配置和资源文件
|
||||||
|
- 排除模块:减少打包大小
|
||||||
|
- 单文件模式:生成单个可执行文件
|
||||||
|
|
||||||
|
### 3. 自动化构建脚本
|
||||||
|
|
||||||
|
**文件**: `scripts/build_python_core.py`
|
||||||
|
|
||||||
|
提供完整的构建流程:
|
||||||
|
1. 安装构建依赖
|
||||||
|
2. 清理构建目录
|
||||||
|
3. 运行PyInstaller
|
||||||
|
4. 复制到sidecar目录
|
||||||
|
5. 更新Tauri配置
|
||||||
|
6. 验证构建结果
|
||||||
|
|
||||||
|
### 4. Rust代码适配
|
||||||
|
|
||||||
|
**文件**: `src-tauri/src/python_executor.rs`
|
||||||
|
|
||||||
|
添加sidecar支持:
|
||||||
|
- 自动检测sidecar可用性
|
||||||
|
- 开发环境使用系统Python
|
||||||
|
- 生产环境使用sidecar
|
||||||
|
- 统一的API接口
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 构建Python Core
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
```cmd
|
||||||
|
scripts\build_python_core.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux/macOS
|
||||||
|
```bash
|
||||||
|
./scripts/build_python_core.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 手动构建
|
||||||
|
```bash
|
||||||
|
python scripts/build_python_core.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发和测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式(使用系统Python)
|
||||||
|
cargo tauri dev
|
||||||
|
|
||||||
|
# 生产构建(包含sidecar)
|
||||||
|
cargo tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
### API调用
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 新的模块化API
|
||||||
|
use crate::python_executor::execute_python_module_action;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn my_command(app: AppHandle) -> Result<String, String> {
|
||||||
|
let params = serde_json::json!({
|
||||||
|
"folder_path": "/path/to/templates",
|
||||||
|
"force_reimport": false
|
||||||
|
});
|
||||||
|
|
||||||
|
execute_python_module_action(
|
||||||
|
app,
|
||||||
|
"template_manager", // 模块名
|
||||||
|
"batch_import", // 动作名
|
||||||
|
Some(params), // 参数
|
||||||
|
None // 配置
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
project/
|
||||||
|
├── python_core/
|
||||||
|
│ ├── main.py # PyInstaller入口文件
|
||||||
|
│ ├── build.spec # PyInstaller配置
|
||||||
|
│ ├── services/ # 服务模块
|
||||||
|
│ ├── utils/ # 工具模块
|
||||||
|
│ └── ...
|
||||||
|
├── scripts/
|
||||||
|
│ ├── build_python_core.py # 构建脚本
|
||||||
|
│ ├── build_python_core.bat# Windows批处理
|
||||||
|
│ └── build_python_core.sh # Linux/macOS脚本
|
||||||
|
├── src-tauri/
|
||||||
|
│ ├── binaries/ # sidecar可执行文件
|
||||||
|
│ │ └── mixvideo-python-core(.exe)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ └── python_executor.rs # 更新的执行器
|
||||||
|
│ └── tauri.conf.json # 更新的配置
|
||||||
|
└── docs/
|
||||||
|
└── PYTHON_CORE_DISTRIBUTION.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### Tauri配置更新
|
||||||
|
|
||||||
|
`src-tauri/tauri.conf.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bundle": {
|
||||||
|
"externalBin": [
|
||||||
|
{
|
||||||
|
"name": "mixvideo-python-core",
|
||||||
|
"src": "binaries/mixvideo-python-core",
|
||||||
|
"targets": ["all"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境检测逻辑
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn should_use_sidecar() -> bool {
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
// 生产环境:检查sidecar是否存在
|
||||||
|
return get_sidecar_path().is_some();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开发环境:使用系统Python
|
||||||
|
false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能对比
|
||||||
|
|
||||||
|
| 方案 | 启动时间 | 内存占用 | 分发大小 | 依赖管理 |
|
||||||
|
|------|----------|----------|----------|----------|
|
||||||
|
| 系统Python | 慢 | 低 | 小 | 复杂 |
|
||||||
|
| PyInstaller | 快 | 中 | 大 | 简单 |
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
#### 1. 构建失败
|
||||||
|
```bash
|
||||||
|
# 检查Python环境
|
||||||
|
python --version
|
||||||
|
pip --version
|
||||||
|
|
||||||
|
# 安装构建依赖
|
||||||
|
pip install pyinstaller requests Pillow opencv-python-headless
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 模块导入错误
|
||||||
|
- 检查 `build.spec` 中的 `hiddenimports`
|
||||||
|
- 添加缺失的模块到隐藏导入列表
|
||||||
|
|
||||||
|
#### 3. 资源文件缺失
|
||||||
|
- 检查 `build.spec` 中的 `datas` 配置
|
||||||
|
- 确保所有资源文件都被包含
|
||||||
|
|
||||||
|
#### 4. sidecar不可用
|
||||||
|
- 检查 `src-tauri/binaries/` 目录
|
||||||
|
- 验证可执行文件权限
|
||||||
|
- 查看Tauri配置中的sidecar设置
|
||||||
|
|
||||||
|
### 调试方法
|
||||||
|
|
||||||
|
#### 1. 测试sidecar
|
||||||
|
```bash
|
||||||
|
# 直接测试可执行文件
|
||||||
|
./src-tauri/binaries/mixvideo-python-core --version
|
||||||
|
./src-tauri/binaries/mixvideo-python-core --module template_manager --action get_templates
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 查看日志
|
||||||
|
```rust
|
||||||
|
// 在Rust代码中添加调试输出
|
||||||
|
println!("Using sidecar: {}", should_use_sidecar());
|
||||||
|
println!("Sidecar path: {:?}", get_sidecar_path());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 环境变量
|
||||||
|
```bash
|
||||||
|
# 强制使用系统Python(调试用)
|
||||||
|
export MIXVIDEO_FORCE_SYSTEM_PYTHON=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 开发流程
|
||||||
|
1. 在开发环境使用系统Python进行开发和测试
|
||||||
|
2. 定期构建sidecar验证打包正确性
|
||||||
|
3. 在CI/CD中自动化构建流程
|
||||||
|
|
||||||
|
### 2. 版本管理
|
||||||
|
- 为sidecar添加版本信息
|
||||||
|
- 在主应用中检查sidecar版本兼容性
|
||||||
|
- 提供版本不匹配时的降级方案
|
||||||
|
|
||||||
|
### 3. 错误处理
|
||||||
|
- 提供详细的错误信息
|
||||||
|
- 实现自动重试机制
|
||||||
|
- 支持降级到系统Python
|
||||||
|
|
||||||
|
### 4. 性能优化
|
||||||
|
- 使用单文件模式减少启动时间
|
||||||
|
- 排除不必要的模块减少大小
|
||||||
|
- 考虑使用UPX压缩可执行文件
|
||||||
|
|
||||||
|
## 未来改进
|
||||||
|
|
||||||
|
### 短期计划
|
||||||
|
- [ ] 添加自动更新机制
|
||||||
|
- [ ] 支持插件化架构
|
||||||
|
- [ ] 优化启动性能
|
||||||
|
|
||||||
|
### 长期计划
|
||||||
|
- [ ] 考虑使用Rust重写核心逻辑
|
||||||
|
- [ ] 支持多语言sidecar
|
||||||
|
- [ ] 集成云端分发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
通过这个解决方案,我们成功解决了Python代码分发的问题,为用户提供了无需Python环境的一体化体验。
|
||||||
133
docs/PYTHON_EMBEDDED_SOLUTION.md
Normal file
133
docs/PYTHON_EMBEDDED_SOLUTION.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Python嵌入式分发方案(备选)
|
||||||
|
|
||||||
|
## 方案概述
|
||||||
|
|
||||||
|
如果PyInstaller sidecar方案遇到问题,可以考虑直接将Python嵌入式版本打包到应用中。
|
||||||
|
|
||||||
|
## 实现步骤
|
||||||
|
|
||||||
|
### 1. 下载Python嵌入式版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
wget https://www.python.org/ftp/python/3.11.0/python-3.11.0-embed-amd64.zip
|
||||||
|
|
||||||
|
# 解压到 src-tauri/python-embed/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安装依赖到嵌入式Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
# scripts/setup_embedded_python.py
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def setup_embedded_python():
|
||||||
|
embed_dir = Path("src-tauri/python-embed")
|
||||||
|
python_exe = embed_dir / "python.exe"
|
||||||
|
|
||||||
|
# 安装pip
|
||||||
|
subprocess.run([
|
||||||
|
str(python_exe), "-m", "ensurepip", "--default-pip"
|
||||||
|
])
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
subprocess.run([
|
||||||
|
str(python_exe), "-m", "pip", "install",
|
||||||
|
"requests", "Pillow", "opencv-python-headless"
|
||||||
|
])
|
||||||
|
|
||||||
|
# 复制python_core模块
|
||||||
|
import shutil
|
||||||
|
shutil.copytree("python_core", embed_dir / "python_core")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
setup_embedded_python()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 修改Tauri配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bundle": {
|
||||||
|
"resources": [
|
||||||
|
"python-embed/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 修改Rust执行器
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn get_embedded_python_path() -> Option<PathBuf> {
|
||||||
|
let exe_dir = std::env::current_exe().ok()?
|
||||||
|
.parent()?;
|
||||||
|
|
||||||
|
let python_exe = if cfg!(target_os = "windows") {
|
||||||
|
exe_dir.join("python-embed/python.exe")
|
||||||
|
} else {
|
||||||
|
exe_dir.join("python-embed/bin/python3")
|
||||||
|
};
|
||||||
|
|
||||||
|
if python_exe.exists() {
|
||||||
|
Some(python_exe)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_with_embedded_python(
|
||||||
|
module: &str,
|
||||||
|
action: &str,
|
||||||
|
params: Option<&str>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let python_exe = get_embedded_python_path()
|
||||||
|
.ok_or("Embedded Python not found")?;
|
||||||
|
|
||||||
|
let mut cmd = Command::new(python_exe);
|
||||||
|
cmd.args(&[
|
||||||
|
"-m", &format!("python_core.{}", module),
|
||||||
|
"--action", action
|
||||||
|
]);
|
||||||
|
|
||||||
|
if let Some(p) = params {
|
||||||
|
cmd.args(&["--params", p]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行命令...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 优缺点对比
|
||||||
|
|
||||||
|
### PyInstaller Sidecar方案
|
||||||
|
**优点**:
|
||||||
|
- 单文件分发
|
||||||
|
- 启动速度快
|
||||||
|
- 内存占用相对较小
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 构建复杂
|
||||||
|
- 调试困难
|
||||||
|
- 可能遇到兼容性问题
|
||||||
|
|
||||||
|
### 嵌入式Python方案
|
||||||
|
**优点**:
|
||||||
|
- 构建简单
|
||||||
|
- 调试容易
|
||||||
|
- 兼容性好
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 分发包较大
|
||||||
|
- 启动速度较慢
|
||||||
|
- 安全性较低
|
||||||
|
|
||||||
|
## 推荐方案
|
||||||
|
|
||||||
|
1. **首选**:PyInstaller Sidecar方案
|
||||||
|
2. **备选**:嵌入式Python方案
|
||||||
|
3. **开发**:系统Python方案
|
||||||
|
|
||||||
|
根据项目需求和技术栈选择最适合的方案。
|
||||||
161
python_core/build.spec
Normal file
161
python_core/build.spec
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
PyInstaller配置文件 - MixVideo V2 Python Core
|
||||||
|
用于将Python代码打包成独立可执行文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 获取当前目录
|
||||||
|
current_dir = Path(__file__).parent
|
||||||
|
project_root = current_dir.parent
|
||||||
|
|
||||||
|
# 添加隐藏导入(确保所有模块都被包含)
|
||||||
|
hiddenimports = [
|
||||||
|
# 核心模块
|
||||||
|
'python_core',
|
||||||
|
'python_core.config',
|
||||||
|
'python_core.utils',
|
||||||
|
'python_core.utils.logger',
|
||||||
|
'python_core.utils.jsonrpc',
|
||||||
|
'python_core.utils.helpers',
|
||||||
|
'python_core.utils.validators',
|
||||||
|
|
||||||
|
# 服务模块
|
||||||
|
'python_core.services',
|
||||||
|
'python_core.services.template_manager',
|
||||||
|
'python_core.services.project_manager',
|
||||||
|
'python_core.services.media_manager',
|
||||||
|
'python_core.services.audio_manager',
|
||||||
|
'python_core.services.model_manager',
|
||||||
|
'python_core.services.resource_category_manager',
|
||||||
|
'python_core.services.file_manager',
|
||||||
|
|
||||||
|
# AI视频模块
|
||||||
|
'python_core.ai_video',
|
||||||
|
'python_core.ai_video.video_generator',
|
||||||
|
'python_core.ai_video.api_client',
|
||||||
|
'python_core.ai_video.cloud_storage',
|
||||||
|
|
||||||
|
# 处理模块
|
||||||
|
'python_core.audio_processing',
|
||||||
|
'python_core.audio_processing.core',
|
||||||
|
'python_core.video_processing',
|
||||||
|
'python_core.video_processing.core',
|
||||||
|
|
||||||
|
# 第三方依赖
|
||||||
|
'requests',
|
||||||
|
'PIL',
|
||||||
|
'Pillow',
|
||||||
|
'numpy',
|
||||||
|
'opencv-python',
|
||||||
|
'cv2',
|
||||||
|
'json',
|
||||||
|
'sqlite3',
|
||||||
|
'pathlib',
|
||||||
|
'uuid',
|
||||||
|
'hashlib',
|
||||||
|
'base64',
|
||||||
|
'urllib',
|
||||||
|
'urllib.parse',
|
||||||
|
'urllib.request',
|
||||||
|
'http',
|
||||||
|
'http.client',
|
||||||
|
'ssl',
|
||||||
|
'certifi',
|
||||||
|
]
|
||||||
|
|
||||||
|
# 数据文件(包含配置文件、模板等)
|
||||||
|
datas = [
|
||||||
|
# 配置文件
|
||||||
|
(str(current_dir / '*.json'), '.'),
|
||||||
|
(str(current_dir / '*.yaml'), '.'),
|
||||||
|
(str(current_dir / '*.yml'), '.'),
|
||||||
|
|
||||||
|
# 模板文件
|
||||||
|
(str(project_root / 'assets' / 'templates'), 'assets/templates'),
|
||||||
|
|
||||||
|
# 其他资源文件
|
||||||
|
(str(project_root / 'assets' / 'icons'), 'assets/icons'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 二进制文件(如果有的话)
|
||||||
|
binaries = []
|
||||||
|
|
||||||
|
# 排除的模块(减少打包大小)
|
||||||
|
excludes = [
|
||||||
|
'tkinter',
|
||||||
|
'matplotlib',
|
||||||
|
'scipy',
|
||||||
|
'pandas',
|
||||||
|
'jupyter',
|
||||||
|
'notebook',
|
||||||
|
'IPython',
|
||||||
|
'pytest',
|
||||||
|
'unittest',
|
||||||
|
'test',
|
||||||
|
'tests',
|
||||||
|
'_pytest',
|
||||||
|
'setuptools',
|
||||||
|
'pip',
|
||||||
|
'wheel',
|
||||||
|
'distutils',
|
||||||
|
]
|
||||||
|
|
||||||
|
# 分析配置
|
||||||
|
a = Analysis(
|
||||||
|
[str(current_dir / 'main.py')],
|
||||||
|
pathex=[str(current_dir), str(project_root)],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=excludes,
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=None,
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 处理PYZ
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
|
||||||
|
|
||||||
|
# 可执行文件配置
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='mixvideo-python-core',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True, # 保持控制台模式,便于调试
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=str(project_root / 'src-tauri' / 'icons' / 'icon.ico') if (project_root / 'src-tauri' / 'icons' / 'icon.ico').exists() else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果需要创建目录分发版本(可选)
|
||||||
|
# coll = COLLECT(
|
||||||
|
# exe,
|
||||||
|
# a.binaries,
|
||||||
|
# a.zipfiles,
|
||||||
|
# a.datas,
|
||||||
|
# strip=False,
|
||||||
|
# upx=True,
|
||||||
|
# upx_exclude=[],
|
||||||
|
# name='mixvideo-python-core'
|
||||||
|
# )
|
||||||
208
python_core/main.py
Normal file
208
python_core/main.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MixVideo V2 Python Core - Main Entry Point
|
||||||
|
用于PyInstaller打包的主入口文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 添加当前目录到Python路径,确保可以导入python_core模块
|
||||||
|
current_dir = Path(__file__).parent
|
||||||
|
sys.path.insert(0, str(current_dir))
|
||||||
|
|
||||||
|
# 导入核心模块
|
||||||
|
try:
|
||||||
|
from utils.logger import get_logger
|
||||||
|
from utils.jsonrpc import create_response_handler, create_progress_reporter
|
||||||
|
from config import get_config
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"ERROR: Failed to import core modules: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 模块映射表
|
||||||
|
MODULE_MAP = {
|
||||||
|
# 模板管理
|
||||||
|
'template_manager': 'services.template_manager',
|
||||||
|
'template': 'services.template_manager',
|
||||||
|
|
||||||
|
# 项目管理
|
||||||
|
'project_manager': 'services.project_manager',
|
||||||
|
'project': 'services.project_manager',
|
||||||
|
|
||||||
|
# 媒体管理
|
||||||
|
'media_manager': 'services.media_manager',
|
||||||
|
'media': 'services.media_manager',
|
||||||
|
|
||||||
|
# 音频管理
|
||||||
|
'audio_manager': 'services.audio_manager',
|
||||||
|
'audio': 'services.audio_manager',
|
||||||
|
|
||||||
|
# 模特管理
|
||||||
|
'model_manager': 'services.model_manager',
|
||||||
|
'model': 'services.model_manager',
|
||||||
|
|
||||||
|
# 资源分类管理
|
||||||
|
'resource_category_manager': 'services.resource_category_manager',
|
||||||
|
'category': 'services.resource_category_manager',
|
||||||
|
|
||||||
|
# 文件管理
|
||||||
|
'file_manager': 'services.file_manager',
|
||||||
|
'file': 'services.file_manager',
|
||||||
|
|
||||||
|
# AI视频生成
|
||||||
|
'ai_video': 'ai_video.video_generator',
|
||||||
|
'video_generator': 'ai_video.video_generator',
|
||||||
|
|
||||||
|
# 音频处理
|
||||||
|
'audio_processing': 'audio_processing.core',
|
||||||
|
|
||||||
|
# 视频处理
|
||||||
|
'video_processing': 'video_processing.core',
|
||||||
|
}
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""设置日志"""
|
||||||
|
try:
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
return logger
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WARNING: Failed to setup logging: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def load_module_function(module_name, function_name):
|
||||||
|
"""动态加载模块和函数"""
|
||||||
|
try:
|
||||||
|
# 解析模块路径
|
||||||
|
if module_name in MODULE_MAP:
|
||||||
|
module_path = MODULE_MAP[module_name]
|
||||||
|
else:
|
||||||
|
module_path = module_name
|
||||||
|
|
||||||
|
# 导入模块
|
||||||
|
module = __import__(f"python_core.{module_path}", fromlist=[function_name])
|
||||||
|
|
||||||
|
# 获取函数
|
||||||
|
if hasattr(module, function_name):
|
||||||
|
return getattr(module, function_name)
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Function '{function_name}' not found in module '{module_path}'")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(f"Failed to import module '{module_path}': {e}")
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"Failed to load function '{function_name}' from '{module_path}': {e}")
|
||||||
|
|
||||||
|
def execute_action(module_name, action, params=None, progress_callback=None):
|
||||||
|
"""执行指定的动作"""
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if logger:
|
||||||
|
logger.info(f"Executing action: {module_name}.{action}")
|
||||||
|
|
||||||
|
# 加载函数
|
||||||
|
func = load_module_function(module_name, action)
|
||||||
|
|
||||||
|
# 准备参数
|
||||||
|
kwargs = {}
|
||||||
|
if params:
|
||||||
|
kwargs.update(params)
|
||||||
|
|
||||||
|
# 添加进度回调
|
||||||
|
if progress_callback:
|
||||||
|
kwargs['progress_callback'] = progress_callback
|
||||||
|
|
||||||
|
# 执行函数
|
||||||
|
result = func(**kwargs)
|
||||||
|
|
||||||
|
if logger:
|
||||||
|
logger.info(f"Action completed successfully: {module_name}.{action}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Failed to execute {module_name}.{action}: {str(e)}"
|
||||||
|
if logger:
|
||||||
|
logger.error(error_msg)
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
raise Exception(error_msg)
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
"""解析命令行参数"""
|
||||||
|
parser = argparse.ArgumentParser(description='MixVideo V2 Python Core')
|
||||||
|
|
||||||
|
parser.add_argument('--module', '-m', required=True,
|
||||||
|
help='Module name (e.g., template_manager, project_manager)')
|
||||||
|
|
||||||
|
parser.add_argument('--action', '-a', required=True,
|
||||||
|
help='Action to execute (e.g., get_templates, batch_import)')
|
||||||
|
|
||||||
|
parser.add_argument('--params', '-p', type=str,
|
||||||
|
help='Parameters as JSON string')
|
||||||
|
|
||||||
|
parser.add_argument('--config', '-c', type=str,
|
||||||
|
help='Configuration file path')
|
||||||
|
|
||||||
|
parser.add_argument('--verbose', '-v', action='store_true',
|
||||||
|
help='Enable verbose logging')
|
||||||
|
|
||||||
|
parser.add_argument('--version', action='version', version='MixVideo V2 Python Core 2.0.0')
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
try:
|
||||||
|
# 解析参数
|
||||||
|
args = parse_arguments()
|
||||||
|
|
||||||
|
# 设置日志级别
|
||||||
|
if args.verbose:
|
||||||
|
os.environ['LOG_LEVEL'] = 'DEBUG'
|
||||||
|
|
||||||
|
# 加载配置
|
||||||
|
if args.config:
|
||||||
|
os.environ['CONFIG_FILE'] = args.config
|
||||||
|
|
||||||
|
# 解析参数
|
||||||
|
params = {}
|
||||||
|
if args.params:
|
||||||
|
try:
|
||||||
|
params = json.loads(args.params)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"ERROR: Invalid JSON parameters: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 创建响应处理器
|
||||||
|
send_response = create_response_handler()
|
||||||
|
send_progress = create_progress_reporter()
|
||||||
|
|
||||||
|
# 创建进度回调
|
||||||
|
def progress_callback(step, progress, message, details=None):
|
||||||
|
send_progress(step, progress, message, details or {})
|
||||||
|
|
||||||
|
# 执行动作
|
||||||
|
result = execute_action(args.module, args.action, params, progress_callback)
|
||||||
|
|
||||||
|
# 发送成功响应
|
||||||
|
send_response(True, "操作完成", result)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("ERROR: Operation cancelled by user", file=sys.stderr)
|
||||||
|
sys.exit(130)
|
||||||
|
except Exception as e:
|
||||||
|
# 发送错误响应
|
||||||
|
try:
|
||||||
|
send_response = create_response_handler()
|
||||||
|
send_response(False, str(e), None)
|
||||||
|
except:
|
||||||
|
print(f"ERROR: {str(e)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
46
scripts/build_python_core.bat
Normal file
46
scripts/build_python_core.bat
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
@echo off
|
||||||
|
REM Windows批处理脚本 - 构建Python Core
|
||||||
|
REM 用于Windows环境下构建Python可执行文件
|
||||||
|
|
||||||
|
echo 🚀 Building MixVideo V2 Python Core for Windows...
|
||||||
|
|
||||||
|
REM 检查Python是否安装
|
||||||
|
python --version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ❌ Python is not installed or not in PATH
|
||||||
|
echo Please install Python 3.8+ and add it to PATH
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 检查pip是否可用
|
||||||
|
python -m pip --version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ❌ pip is not available
|
||||||
|
echo Please ensure pip is installed with Python
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ✅ Python environment check passed
|
||||||
|
|
||||||
|
REM 切换到项目根目录
|
||||||
|
cd /d "%~dp0.."
|
||||||
|
|
||||||
|
REM 运行Python构建脚本
|
||||||
|
echo 📦 Running Python build script...
|
||||||
|
python scripts\build_python_core.py
|
||||||
|
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ❌ Build failed
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ✅ Build completed successfully!
|
||||||
|
echo.
|
||||||
|
echo 📋 Next steps:
|
||||||
|
echo 1. Test the build: cargo tauri dev
|
||||||
|
echo 2. Build the app: cargo tauri build
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
257
scripts/build_python_core.py
Normal file
257
scripts/build_python_core.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
构建脚本 - 将Python Core打包成独立可执行文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
import platform
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def get_project_root():
|
||||||
|
"""获取项目根目录"""
|
||||||
|
current_file = Path(__file__).resolve()
|
||||||
|
# 从 scripts/build_python_core.py 回到项目根目录
|
||||||
|
return current_file.parent.parent
|
||||||
|
|
||||||
|
def install_dependencies():
|
||||||
|
"""安装构建依赖"""
|
||||||
|
print("📦 Installing build dependencies...")
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
'pyinstaller>=5.0',
|
||||||
|
'requests',
|
||||||
|
'Pillow',
|
||||||
|
'opencv-python-headless', # 使用headless版本减少依赖
|
||||||
|
]
|
||||||
|
|
||||||
|
for dep in dependencies:
|
||||||
|
print(f"Installing {dep}...")
|
||||||
|
result = subprocess.run([
|
||||||
|
sys.executable, '-m', 'pip', 'install', dep
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ Failed to install {dep}")
|
||||||
|
print(result.stderr)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f"✅ Installed {dep}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clean_build_directory(python_core_dir):
|
||||||
|
"""清理构建目录"""
|
||||||
|
print("🧹 Cleaning build directories...")
|
||||||
|
|
||||||
|
dirs_to_clean = ['build', 'dist', '__pycache__']
|
||||||
|
|
||||||
|
for dir_name in dirs_to_clean:
|
||||||
|
dir_path = python_core_dir / dir_name
|
||||||
|
if dir_path.exists():
|
||||||
|
shutil.rmtree(dir_path)
|
||||||
|
print(f"Removed {dir_path}")
|
||||||
|
|
||||||
|
def build_executable(python_core_dir):
|
||||||
|
"""构建可执行文件"""
|
||||||
|
print("🔨 Building executable...")
|
||||||
|
|
||||||
|
spec_file = python_core_dir / 'build.spec'
|
||||||
|
|
||||||
|
if not spec_file.exists():
|
||||||
|
print(f"❌ Spec file not found: {spec_file}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 运行PyInstaller
|
||||||
|
cmd = [
|
||||||
|
sys.executable, '-m', 'PyInstaller',
|
||||||
|
'--clean',
|
||||||
|
'--noconfirm',
|
||||||
|
str(spec_file)
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"Running: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=python_core_dir,
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("❌ Build failed!")
|
||||||
|
print("STDOUT:", result.stdout)
|
||||||
|
print("STDERR:", result.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ Build completed successfully!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def copy_to_sidecar_directory(project_root, python_core_dir):
|
||||||
|
"""复制可执行文件到Tauri sidecar目录"""
|
||||||
|
print("📁 Copying executable to sidecar directory...")
|
||||||
|
|
||||||
|
# 确定可执行文件名
|
||||||
|
system = platform.system().lower()
|
||||||
|
if system == 'windows':
|
||||||
|
exe_name = 'mixvideo-python-core.exe'
|
||||||
|
else:
|
||||||
|
exe_name = 'mixvideo-python-core'
|
||||||
|
|
||||||
|
# 源文件路径
|
||||||
|
source_exe = python_core_dir / 'dist' / exe_name
|
||||||
|
|
||||||
|
if not source_exe.exists():
|
||||||
|
print(f"❌ Executable not found: {source_exe}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 目标目录
|
||||||
|
sidecar_dir = project_root / 'src-tauri' / 'binaries'
|
||||||
|
sidecar_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# 复制文件
|
||||||
|
target_exe = sidecar_dir / exe_name
|
||||||
|
shutil.copy2(source_exe, target_exe)
|
||||||
|
|
||||||
|
# 在Unix系统上设置执行权限
|
||||||
|
if system != 'windows':
|
||||||
|
os.chmod(target_exe, 0o755)
|
||||||
|
|
||||||
|
print(f"✅ Copied to: {target_exe}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_tauri_config(project_root):
|
||||||
|
"""更新Tauri配置文件以包含sidecar"""
|
||||||
|
print("⚙️ Updating Tauri configuration...")
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
config_file = project_root / 'src-tauri' / 'tauri.conf.json'
|
||||||
|
|
||||||
|
if not config_file.exists():
|
||||||
|
print(f"❌ Tauri config not found: {config_file}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 读取配置
|
||||||
|
with open(config_file, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# 添加sidecar配置
|
||||||
|
if 'bundle' not in config:
|
||||||
|
config['bundle'] = {}
|
||||||
|
|
||||||
|
if 'externalBin' not in config['bundle']:
|
||||||
|
config['bundle']['externalBin'] = []
|
||||||
|
|
||||||
|
# 检查是否已经存在
|
||||||
|
sidecar_name = 'mixvideo-python-core'
|
||||||
|
existing = any(
|
||||||
|
item.get('name') == sidecar_name
|
||||||
|
for item in config['bundle']['externalBin']
|
||||||
|
if isinstance(item, dict)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
config['bundle']['externalBin'].append({
|
||||||
|
"name": sidecar_name,
|
||||||
|
"src": f"binaries/{sidecar_name}",
|
||||||
|
"targets": ["all"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 写回配置
|
||||||
|
with open(config_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print("✅ Updated Tauri configuration")
|
||||||
|
else:
|
||||||
|
print("ℹ️ Sidecar already configured in Tauri")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def verify_build(project_root):
|
||||||
|
"""验证构建结果"""
|
||||||
|
print("🔍 Verifying build...")
|
||||||
|
|
||||||
|
system = platform.system().lower()
|
||||||
|
exe_name = 'mixvideo-python-core.exe' if system == 'windows' else 'mixvideo-python-core'
|
||||||
|
|
||||||
|
sidecar_exe = project_root / 'src-tauri' / 'binaries' / exe_name
|
||||||
|
|
||||||
|
if not sidecar_exe.exists():
|
||||||
|
print(f"❌ Sidecar executable not found: {sidecar_exe}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 测试执行
|
||||||
|
try:
|
||||||
|
result = subprocess.run([
|
||||||
|
str(sidecar_exe), '--version'
|
||||||
|
], capture_output=True, text=True, timeout=10)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("✅ Executable is working correctly")
|
||||||
|
print(f"Version output: {result.stdout.strip()}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Executable test failed: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print("❌ Executable test timed out")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Executable test error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
print("🚀 Building MixVideo V2 Python Core...")
|
||||||
|
|
||||||
|
project_root = get_project_root()
|
||||||
|
python_core_dir = project_root / 'python_core'
|
||||||
|
|
||||||
|
print(f"Project root: {project_root}")
|
||||||
|
print(f"Python core directory: {python_core_dir}")
|
||||||
|
|
||||||
|
if not python_core_dir.exists():
|
||||||
|
print(f"❌ Python core directory not found: {python_core_dir}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 步骤1: 安装依赖
|
||||||
|
if not install_dependencies():
|
||||||
|
print("❌ Failed to install dependencies")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 步骤2: 清理构建目录
|
||||||
|
clean_build_directory(python_core_dir)
|
||||||
|
|
||||||
|
# 步骤3: 构建可执行文件
|
||||||
|
if not build_executable(python_core_dir):
|
||||||
|
print("❌ Failed to build executable")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 步骤4: 复制到sidecar目录
|
||||||
|
if not copy_to_sidecar_directory(project_root, python_core_dir):
|
||||||
|
print("❌ Failed to copy executable")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 步骤5: 更新Tauri配置
|
||||||
|
if not update_tauri_config(project_root):
|
||||||
|
print("❌ Failed to update Tauri configuration")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 步骤6: 验证构建
|
||||||
|
if not verify_build(project_root):
|
||||||
|
print("❌ Build verification failed")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("🎉 Build completed successfully!")
|
||||||
|
print("\n📋 Next steps:")
|
||||||
|
print("1. Test the sidecar: cargo tauri dev")
|
||||||
|
print("2. Build the app: cargo tauri build")
|
||||||
|
print("3. The Python core will be bundled with your app")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
53
scripts/build_python_core.sh
Executable file
53
scripts/build_python_core.sh
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Shell脚本 - 构建Python Core
|
||||||
|
# 用于Linux/macOS环境下构建Python可执行文件
|
||||||
|
|
||||||
|
set -e # 遇到错误时退出
|
||||||
|
|
||||||
|
echo "🚀 Building MixVideo V2 Python Core for $(uname)..."
|
||||||
|
|
||||||
|
# 检查Python是否安装
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
if ! command -v python &> /dev/null; then
|
||||||
|
echo "❌ Python is not installed or not in PATH"
|
||||||
|
echo "Please install Python 3.8+ and add it to PATH"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
PYTHON_CMD="python"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
PYTHON_CMD="python3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Using Python: $PYTHON_CMD"
|
||||||
|
|
||||||
|
# 检查Python版本
|
||||||
|
PYTHON_VERSION=$($PYTHON_CMD --version 2>&1 | cut -d' ' -f2)
|
||||||
|
echo "Python version: $PYTHON_VERSION"
|
||||||
|
|
||||||
|
# 检查pip是否可用
|
||||||
|
if ! $PYTHON_CMD -m pip --version &> /dev/null; then
|
||||||
|
echo "❌ pip is not available"
|
||||||
|
echo "Please ensure pip is installed with Python"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Python environment check passed"
|
||||||
|
|
||||||
|
# 切换到项目根目录
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
echo "📁 Working directory: $PROJECT_ROOT"
|
||||||
|
|
||||||
|
# 运行Python构建脚本
|
||||||
|
echo "📦 Running Python build script..."
|
||||||
|
$PYTHON_CMD scripts/build_python_core.py
|
||||||
|
|
||||||
|
echo "✅ Build completed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Next steps:"
|
||||||
|
echo "1. Test the build: cargo tauri dev"
|
||||||
|
echo "2. Build the app: cargo tauri build"
|
||||||
|
echo ""
|
||||||
@@ -5,9 +5,112 @@ use std::thread;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
use crate::command_utils::configure_python_command;
|
use crate::command_utils::configure_python_command;
|
||||||
|
|
||||||
|
/// Check if we should use the bundled Python sidecar
|
||||||
|
fn should_use_sidecar() -> bool {
|
||||||
|
// 在生产环境中优先使用sidecar
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
// 检查sidecar可执行文件是否存在
|
||||||
|
let exe_name = if cfg!(target_os = "windows") {
|
||||||
|
"mixvideo-python-core.exe"
|
||||||
|
} else {
|
||||||
|
"mixvideo-python-core"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查当前目录和常见位置
|
||||||
|
let possible_paths = vec![
|
||||||
|
std::env::current_exe().ok()
|
||||||
|
.and_then(|p| p.parent().map(|p| p.join(exe_name))),
|
||||||
|
Some(std::path::PathBuf::from(exe_name)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for path in possible_paths.into_iter().flatten() {
|
||||||
|
if path.exists() {
|
||||||
|
println!("Found sidecar at: {:?}", path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在开发环境中或sidecar不可用时使用系统Python
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the path to the Python sidecar executable
|
||||||
|
fn get_sidecar_path() -> Option<std::path::PathBuf> {
|
||||||
|
let exe_name = if cfg!(target_os = "windows") {
|
||||||
|
"mixvideo-python-core.exe"
|
||||||
|
} else {
|
||||||
|
"mixvideo-python-core"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查可能的路径
|
||||||
|
let possible_paths = vec![
|
||||||
|
// 与主程序同目录
|
||||||
|
std::env::current_exe().ok()
|
||||||
|
.and_then(|p| p.parent().map(|p| p.join(exe_name))),
|
||||||
|
// 当前工作目录
|
||||||
|
Some(std::path::PathBuf::from(exe_name)),
|
||||||
|
// 相对路径
|
||||||
|
Some(std::path::PathBuf::from("./").join(exe_name)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for path in possible_paths.into_iter().flatten() {
|
||||||
|
if path.exists() {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute Python command using sidecar
|
||||||
|
async fn execute_python_sidecar(
|
||||||
|
module: &str,
|
||||||
|
action: &str,
|
||||||
|
params: Option<&str>,
|
||||||
|
config: Option<PythonExecutorConfig>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let _config = config.unwrap_or_default();
|
||||||
|
|
||||||
|
println!("Executing Python sidecar: module={}, action={}", module, action);
|
||||||
|
|
||||||
|
// 获取sidecar路径
|
||||||
|
let sidecar_path = get_sidecar_path()
|
||||||
|
.ok_or_else(|| "Sidecar executable not found".to_string())?;
|
||||||
|
|
||||||
|
// 构建命令
|
||||||
|
let mut cmd = Command::new(&sidecar_path);
|
||||||
|
cmd.args(&["--module", module])
|
||||||
|
.args(&["--action", action]);
|
||||||
|
|
||||||
|
if let Some(params_str) = params {
|
||||||
|
cmd.args(&["--params", params_str]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置命令(隐藏控制台窗口等)
|
||||||
|
crate::command_utils::configure_no_window(&mut cmd);
|
||||||
|
|
||||||
|
cmd.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped());
|
||||||
|
|
||||||
|
// 执行命令
|
||||||
|
let output = cmd.output()
|
||||||
|
.map_err(|e| format!("Failed to execute sidecar: {}", e))?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
println!("Sidecar output: {}", stdout);
|
||||||
|
Ok(stdout.to_string())
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
Err(format!("Sidecar failed: {}", stderr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generic progress information from Python processes
|
/// Generic progress information from Python processes
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct PythonProgress {
|
pub struct PythonProgress {
|
||||||
@@ -147,6 +250,49 @@ pub async fn execute_python_with_events(
|
|||||||
execute_python_command_with_progress(app, args, config, Some(progress_callback)).await
|
execute_python_command_with_progress(app, args, config, Some(progress_callback)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute Python module action with automatic sidecar/system Python selection
|
||||||
|
pub async fn execute_python_module_action(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
module: &str,
|
||||||
|
action: &str,
|
||||||
|
params: Option<serde_json::Value>,
|
||||||
|
config: Option<PythonExecutorConfig>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
// 检查是否应该使用sidecar
|
||||||
|
if should_use_sidecar() {
|
||||||
|
println!("Using Python sidecar for execution");
|
||||||
|
|
||||||
|
// 将参数转换为JSON字符串
|
||||||
|
let params_str = if let Some(p) = params {
|
||||||
|
Some(serde_json::to_string(&p)
|
||||||
|
.map_err(|e| format!("Failed to serialize parameters: {}", e))?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
execute_python_sidecar(module, action, params_str.as_deref(), config).await
|
||||||
|
} else {
|
||||||
|
println!("Using system Python for execution");
|
||||||
|
|
||||||
|
// 构建传统的命令行参数
|
||||||
|
let mut args = vec![
|
||||||
|
"-m".to_string(),
|
||||||
|
format!("python_core.{}", module),
|
||||||
|
"--action".to_string(),
|
||||||
|
action.to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 添加参数
|
||||||
|
if let Some(p) = params {
|
||||||
|
args.push("--params".to_string());
|
||||||
|
args.push(serde_json::to_string(&p)
|
||||||
|
.map_err(|e| format!("Failed to serialize parameters: {}", e))?);
|
||||||
|
}
|
||||||
|
|
||||||
|
execute_python_command(app, &args, config).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Python command builder for easier command construction
|
/// Python command builder for easier command construction
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct PythonCommandBuilder {
|
pub struct PythonCommandBuilder {
|
||||||
|
|||||||
Reference in New Issue
Block a user