From c2e219c1254c9c696c874351376faacab3040eba Mon Sep 17 00:00:00 2001 From: root Date: Fri, 11 Jul 2025 14:19:45 +0800 Subject: [PATCH] fix --- docs/COPY_FEATURE_DEMO.md | 256 ++++++++++++++++++++++++++++ docs/PYTHON_CORE_DISTRIBUTION.md | 280 +++++++++++++++++++++++++++++++ docs/PYTHON_EMBEDDED_SOLUTION.md | 133 +++++++++++++++ python_core/build.spec | 161 ++++++++++++++++++ python_core/main.py | 208 +++++++++++++++++++++++ scripts/build_python_core.bat | 46 +++++ scripts/build_python_core.py | 257 ++++++++++++++++++++++++++++ scripts/build_python_core.sh | 53 ++++++ src-tauri/src/python_executor.rs | 148 +++++++++++++++- 9 files changed, 1541 insertions(+), 1 deletion(-) create mode 100644 docs/COPY_FEATURE_DEMO.md create mode 100644 docs/PYTHON_CORE_DISTRIBUTION.md create mode 100644 docs/PYTHON_EMBEDDED_SOLUTION.md create mode 100644 python_core/build.spec create mode 100644 python_core/main.py create mode 100644 scripts/build_python_core.bat create mode 100644 scripts/build_python_core.py create mode 100755 scripts/build_python_core.sh diff --git a/docs/COPY_FEATURE_DEMO.md b/docs/COPY_FEATURE_DEMO.md new file mode 100644 index 0000000..49e9170 --- /dev/null +++ b/docs/COPY_FEATURE_DEMO.md @@ -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不可用 +- ✅ 网络错误情况 + +--- + +通过这个复制功能,用户可以轻松地复制和分享模板导入日志,大大提升了调试和问题解决的效率。 diff --git a/docs/PYTHON_CORE_DISTRIBUTION.md b/docs/PYTHON_CORE_DISTRIBUTION.md new file mode 100644 index 0000000..96113a3 --- /dev/null +++ b/docs/PYTHON_CORE_DISTRIBUTION.md @@ -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 { + 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环境的一体化体验。 diff --git a/docs/PYTHON_EMBEDDED_SOLUTION.md b/docs/PYTHON_EMBEDDED_SOLUTION.md new file mode 100644 index 0000000..c407aa8 --- /dev/null +++ b/docs/PYTHON_EMBEDDED_SOLUTION.md @@ -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 { + 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 { + 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方案 + +根据项目需求和技术栈选择最适合的方案。 diff --git a/python_core/build.spec b/python_core/build.spec new file mode 100644 index 0000000..ce6b5c2 --- /dev/null +++ b/python_core/build.spec @@ -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' +# ) diff --git a/python_core/main.py b/python_core/main.py new file mode 100644 index 0000000..070c270 --- /dev/null +++ b/python_core/main.py @@ -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() diff --git a/scripts/build_python_core.bat b/scripts/build_python_core.bat new file mode 100644 index 0000000..ae0eeeb --- /dev/null +++ b/scripts/build_python_core.bat @@ -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 diff --git a/scripts/build_python_core.py b/scripts/build_python_core.py new file mode 100644 index 0000000..2fe5a4e --- /dev/null +++ b/scripts/build_python_core.py @@ -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() diff --git a/scripts/build_python_core.sh b/scripts/build_python_core.sh new file mode 100755 index 0000000..663b74d --- /dev/null +++ b/scripts/build_python_core.sh @@ -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 "" diff --git a/src-tauri/src/python_executor.rs b/src-tauri/src/python_executor.rs index b10662a..3714dbb 100644 --- a/src-tauri/src/python_executor.rs +++ b/src-tauri/src/python_executor.rs @@ -5,9 +5,112 @@ use std::thread; use std::sync::{Arc, Mutex}; use std::sync::mpsc; use serde::{Deserialize, Serialize}; -use tauri::{AppHandle, Emitter}; +use tauri::{AppHandle, Emitter, Manager}; 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 { + 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, +) -> Result { + 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 #[derive(Debug, Serialize, Deserialize, Clone)] 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 module action with automatic sidecar/system Python selection +pub async fn execute_python_module_action( + app: tauri::AppHandle, + module: &str, + action: &str, + params: Option, + config: Option, +) -> Result { + // 检查是否应该使用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 #[allow(dead_code)] pub struct PythonCommandBuilder {