This commit is contained in:
root
2025-07-11 14:19:45 +08:00
parent dcc2b6684f
commit c2e219c125
9 changed files with 1541 additions and 1 deletions

256
docs/COPY_FEATURE_DEMO.md Normal file
View 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不可用
- ✅ 网络错误情况
---
通过这个复制功能,用户可以轻松地复制和分享模板导入日志,大大提升了调试和问题解决的效率。

View 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环境的一体化体验。

View 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
View 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
View 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()

View 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

View 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
View 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 ""

View File

@@ -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<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
#[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<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
#[allow(dead_code)]
pub struct PythonCommandBuilder {