📁 项目方案:UVR-Pro - 专业级便携式人声消除工具

核心原则:一切都在程序根目录下

原则1:零写入系统盘(C盘)
原则2:所有文件自包含
原则3:目录结构清晰分离
原则4:用户数据与程序分离但同目录

一、最终运行时的目录结构

UVR-Pro/                              ← 用户解压或安装到此目录
├── UVR-Pro.exe                       ← 主程序(单文件或入口)
├── README.txt                        ← 使用说明
│
├── core/                             ← 核心引擎(不可写)
│   ├── uvrcore.dll/.so              ← C++加速库(如有)
│   ├── audio_engine.pyd             ← 音频处理引擎
│   ├── model_loader.pyd             ← 模型加载器
│   └── config/                       ← 引擎配置文件
│       ├── default.yaml
│       └── performance.yaml
│
├── models/                           ← 模型库目录(只读)
│   ├── README.md                     ← 模型说明
│   ├── vr_standard/                  ← 预设模型1
│   │   ├── model.pth                 ← 模型文件
│   │   ├── config.json               ← 模型配置
│   │   ├── signature.md5             ← 文件校验
│   │   └── preview.mp3               ← 效果预览
│   ├── dsp_basic/                    ← 预设模型2
│   │   ├── algorithm.py              ← 算法文件
│   │   └── config.json
│   └── models_index.json             ← 模型索引文件
│
├── frontend/                         ← 前端资源(只读)
│   ├── index.html                    ← 如果是Web前端
│   ├── assets/
│   │   ├── css/
│   │   ├── js/
│   │   └── icons/
│   └── electron/                     ← 如果是Electron
│       ├── main.js
│       └── preload.js
│
├── runtime/                          ← 运行时目录(可写)
│   ├── temp/                         ← 临时文件(程序退出可清空)
│   │   ├── audio_cache/
│   │   ├── processing_temp/
│   │   └── model_cache/              ← 模型解压缓存
│   ├── logs/                         ← 日志文件
│   │   ├── app.log
│   │   ├── error.log
│   │   └── processing_history.log
│   └── user_data/                    ← 用户数据
│       ├── settings.json             ← 用户设置
│       ├── recent_files.json         ← 最近文件
│       └── custom_presets/           ← 用户自定义预设
│
├── plugins/                          ← 插件目录(可扩展)
│   ├── system/                       ← 系统插件
│   │   ├── format_converter/
│   │   └── batch_processor/
│   ├── community/                    ← 社区插件(预留)
│   └── plugins_config.json           ← 插件配置
│
└── docs/                             ← 文档
    ├── user_guide.pdf
    ├── quick_start.txt
    └── license.txt

二、前后端完全分离架构

前端(Frontend)

  • 技术栈:PyQt5 或 Tkinter(纯Python GUI)
  • 职责

    • 用户交互界面
    • 文件选择对话框
    • 参数设置面板
    • 进度显示和日志
  • 特点:不包含任何业务逻辑,仅负责UI展示和事件转发

后端(Backend)

  • 技术栈:Python + C扩展(可选)
  • 职责

    • 音频文件读取/写入
    • 模型加载和推理
    • 音频信号处理
    • 任务队列管理
  • 特点:无UI依赖,可独立运行

通信方式

前端 (UI Thread) ↔ IPC/Queue ↔ 后端 (Worker Thread)
       ↓                           ↓
   用户交互                     音频处理
   进度显示                     模型推理
   错误提示                     文件I/O

三、编译打包方案

编译策略

开发时源代码结构 ≠ 编译后运行结构

编译流程

1. 源代码开发阶段
   uvrcode/
   ├── frontend_src/
   ├── backend_src/
   └── resources/

2. 编译构建阶段
   build/
   ├── compile_frontend.py   → 生成 frontend/ 目录
   ├── compile_backend.py    → 生成 core/ 目录
   └── package_models.py     → 打包 models/ 目录

3. 最终分发阶段
   dist/
   └── UVR-Pro/             ← 完整运行时目录

PyInstaller配置关键点

# build.spec 关键配置
import sys
import os

# 禁止写入C盘
if sys.platform == 'win32':
    # 设置临时目录到程序所在目录
    os.environ['TEMP'] = os.path.join(sys._MEIPASS, 'runtime', 'temp')
    os.environ['TMP'] = os.path.join(sys._MEIPASS, 'runtime', 'temp')

a = Analysis(
    ['main.py'],
    pathex=[],  # 不依赖系统PATH
    binaries=[],
    datas=[
        # 显式指定每个目录
        ('src/frontend', 'frontend'),
        ('src/backend/compiled', 'core'),
        ('models/packaged', 'models'),
        ('resources', 'resources'),
    ],
    hiddenimports=[],
    hookspath=['hooks'],
    runtime_hooks=['runtime_hooks.py'],  # 运行时hook禁止写C盘
    excludes=['test', 'unittest', 'pydoc'],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=None,
    noarchive=False,
)

四、路径访问规范

绝对禁止的行为

# ❌ 禁止这样写
os.path.expanduser('~')  # 可能指向C:\Users
os.environ['APPDATA']    # 系统目录
'C:\\'                   # 硬编码系统盘

# ✅ 必须这样写
import sys
import os

def get_app_root():
    """获取应用程序根目录"""
    if getattr(sys, 'frozen', False):
        # 打包后:_MEIPASS目录
        return sys._MEIPASS
    else:
        # 开发时:项目根目录
        return os.path.dirname(os.path.abspath(__file__))

def get_runtime_path(subdir=''):
    """获取运行时目录(可写)"""
    root = get_app_root()
    runtime_dir = os.path.join(root, 'runtime')
    
    # 自动创建子目录
    if subdir:
        target = os.path.join(runtime_dir, subdir)
        os.makedirs(target, exist_ok=True)
        return target
    return runtime_dir

def get_model_path(model_name):
    """获取模型路径"""
    root = get_app_root()
    return os.path.join(root, 'models', model_name)

五、模型管理策略

模型存放规则

  1. 原始模型models/[model_name]/ (只读)
  2. 缓存模型runtime/temp/model_cache/ (可写,临时)
  3. 用户模型runtime/user_data/custom_models/ (可写,持久)

模型加载流程

class ModelManager:
    def __init__(self):
        self.app_root = get_app_root()
        self.model_base = os.path.join(self.app_root, 'models')
        self.cache_dir = os.path.join(self.app_root, 'runtime', 'temp', 'model_cache')
        
    def load_model(self, model_id):
        """加载模型,优先使用缓存"""
        model_dir = os.path.join(self.model_base, model_id)
        
        # 检查缓存
        cache_file = os.path.join(self.cache_dir, f"{model_id}.cache")
        if os.path.exists(cache_file):
            return self._load_from_cache(cache_file)
        
        # 从原始目录加载并创建缓存
        model = self._load_original(model_dir)
        self._create_cache(model, cache_file)
        return model
    
    def _load_original(self, model_dir):
        """从原始目录加载,绝不涉及系统目录"""
        # 验证所有文件都在model_dir内
        for root, dirs, files in os.walk(model_dir):
            for file in files:
                full_path = os.path.join(root, file)
                # 安全检查:确保路径在允许范围内
                if not full_path.startswith(self.model_base):
                    raise SecurityError(f"模型文件路径越界: {full_path}")
        # 加载模型...

六、配置文件策略

配置文件位置

CONFIG_PATHS = {
    # 系统默认配置(只读)
    'defaults': os.path.join(get_app_root(), 'core', 'config', 'default.yaml'),
    
    # 用户配置(可写,在runtime目录)
    'user': os.path.join(get_app_root(), 'runtime', 'user_data', 'settings.json'),
    
    # 运行时临时配置(可写)
    'session': os.path.join(get_app_root(), 'runtime', 'temp', 'session_config.json')
}

七、编译构建脚本示例

build.py

#!/usr/bin/env python3
"""
编译构建脚本 - 确保所有文件都在正确位置
"""
import os
import shutil
import py_compile
from pathlib import Path

class Builder:
    def __init__(self):
        self.project_root = Path(__file__).parent
        self.build_dir = self.project_root / "build"
        self.dist_dir = self.project_root / "dist"
        
    def clean(self):
        """清理构建目录"""
        if self.build_dir.exists():
            shutil.rmtree(self.build_dir)
        if self.dist_dir.exists():
            shutil.rmtree(self.dist_dir)
            
    def prepare_directory_structure(self):
        """创建标准的目录结构"""
        structure = [
            "dist/UVR-Pro",
            "dist/UVR-Pro/core",
            "dist/UVR-Pro/models",
            "dist/UVR-Pro/frontend",
            "dist/UVR-Pro/runtime/temp",
            "dist/UVR-Pro/runtime/logs",
            "dist/UVR-Pro/runtime/user_data",
            "dist/UVR-Pro/plugins",
            "dist/UVR-Pro/docs"
        ]
        
        for dir_path in structure:
            Path(dir_path).mkdir(parents=True, exist_ok=True)
            
    def compile_backend(self):
        """编译后端代码到core目录"""
        backend_src = self.project_root / "src" / "backend"
        core_target = self.dist_dir / "UVR-Pro" / "core"
        
        # 编译Python文件为.pyc
        for py_file in backend_src.rglob("*.py"):
            relative = py_file.relative_to(backend_src)
            target_file = core_target / relative.with_suffix(".pyc")
            target_file.parent.mkdir(parents=True, exist_ok=True)
            py_compile.compile(py_file, cfile=str(target_file))
            
    def copy_models(self):
        """复制模型文件到models目录"""
        models_src = self.project_root / "models"
        models_target = self.dist_dir / "UVR-Pro" / "models"
        
        shutil.copytree(models_src, models_target, dirs_exist_ok=True)
        
    def create_spec_file(self):
        """生成PyInstaller spec文件"""
        spec_content = f'''
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
    ['main_entry.py'],
    pathex=[],
    binaries=[],
    datas=[
        ('{self.dist_dir}/UVR-Pro/frontend', 'frontend'),
        ('{self.dist_dir}/UVR-Pro/core', 'core'),
        ('{self.dist_dir}/UVR-Pro/models', 'models'),
        ('{self.dist_dir}/UVR-Pro/docs', 'docs'),
    ],
    hiddenimports=[],
    hookspath=[],
    runtime_hooks=['runtime_hooks.py'],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

# 禁止写入C盘的runtime hook
runtime_hooks = ['no_cdisk_write.py']

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='UVR-Pro',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon='app_icon.ico'
)
'''
        spec_file = self.build_dir / "UVR-Pro.spec"
        spec_file.write_text(spec_content)
        
    def build(self):
        """执行完整构建流程"""
        print("1. 清理旧构建...")
        self.clean()
        
        print("2. 创建目录结构...")
        self.prepare_directory_structure()
        
        print("3. 编译后端代码...")
        self.compile_backend()
        
        print("4. 复制模型文件...")
        self.copy_models()
        
        print("5. 生成spec文件...")
        self.create_spec_file()
        
        print("6. 运行PyInstaller...")
        os.system(f"pyinstaller {self.build_dir/'UVR-Pro.spec'}")
        
        print("构建完成!输出目录: dist/UVR-Pro/")

if __name__ == "__main__":
    builder = Builder()
    builder.build()

八、运行时保护钩子

no_cdisk_write.py (运行时钩子)

"""
运行时钩子 - 禁止程序写入C盘
"""
import os
import sys

def block_cdisk_access():
    """阻止对C盘的写入"""
    def safe_join(path, *paths):
        """安全的路径连接,检查是否指向C盘"""
        full_path = os.path.join(path, *paths)
        full_path = os.path.abspath(full_path)
        
        # 检查是否试图访问C盘系统目录
        if sys.platform == 'win32':
            if full_path.startswith(('C:\\', 'c:\\')):
                # 重定向到程序目录下的runtime目录
                if getattr(sys, 'frozen', False):
                    base = sys._MEIPASS
                else:
                    base = os.path.dirname(os.path.abspath(__file__))
                
                # 提取相对路径部分
                rel_path = full_path[3:]  # 去掉"C:\"
                # 重定向到runtime目录
                safe_path = os.path.join(base, 'runtime', 'temp', rel_path.lstrip('\\/'))
                os.makedirs(os.path.dirname(safe_path), exist_ok=True)
                return safe_path
        
        return full_path
    
    # 替换os.path.join为安全版本
    os.path.join = safe_join
    
    # 设置环境变量,强制使用程序目录
    if getattr(sys, 'frozen', False):
        app_dir = sys._MEIPASS
        runtime_dir = os.path.join(app_dir, 'runtime')
        
        os.environ['TEMP'] = os.path.join(runtime_dir, 'temp')
        os.environ['TMP'] = os.path.join(runtime_dir, 'temp')
        os.environ['APPDATA'] = runtime_dir  # 重定向

# 应用保护
block_cdisk_access()

九、用户安装和使用流程

安装流程

用户操作:
1. 下载 UVR-Pro.zip
2. 解压到任意位置(如 D:\AudioTools\UVR-Pro\)
3. 直接运行 UVR-Pro.exe

系统变化:
✅ 不会写入注册表
✅ 不会在C盘创建文件
✅ 不会添加系统路径
✅ 所有文件都在解压目录内

卸载流程

用户操作:
1. 关闭程序
2. 删除整个 UVR-Pro 文件夹

系统变化:
✅ 无残留注册表项
✅ 无系统目录文件
✅ 完全绿色卸载

十、质量保证清单

  • [ ] 目录分离:前端/后端/模型/运行时完全分离
  • [ ] 路径安全:所有路径都基于程序根目录
  • [ ] 无C盘写入:运行时钩子保护
  • [ ] 完整包含:所有依赖打包在目录内
  • [ ] 相对路径:代码中只使用相对路径
  • [ ] 环境隔离:不依赖系统Python环境
  • [ ] 用户友好:解压即用,无需配置

这个方案确保

  1. 程序绝对绿色,不污染系统
  2. 所有文件都在一个根目录下
  3. 前后端物理分离但逻辑统一
  4. 用户数据与程序数据分离但同目录
  5. 专业级项目结构,便于维护和扩展

标签: none

添加新评论