diff --git a/.gitignore b/.gitignore index 72f6829..d5d3336 100644 --- a/.gitignore +++ b/.gitignore @@ -62,9 +62,6 @@ temp/ tmp/ .cache/ -# Packaging tools -packaging/ - # MkDocs build output site/ diff --git a/packaging/windows/README.md b/packaging/windows/README.md new file mode 100644 index 0000000..9699430 --- /dev/null +++ b/packaging/windows/README.md @@ -0,0 +1,216 @@ +# Windows Package Builder + +Automated build system for creating Windows portable packages of Pixelle-Video. + +## Quick Start + +### Prerequisites + +- Python 3.11+ (for running the build script) +- PyYAML: `pip install pyyaml` +- Internet connection (for downloading Python, FFmpeg, etc.) + +### Build Package + +```bash +# Basic build +python packaging/windows/build.py + +# Build with China mirrors (faster in China) +python packaging/windows/build.py --cn-mirror + +# Custom output directory +python packaging/windows/build.py --output /path/to/output +``` + +## Configuration + +Edit `config/build_config.yaml` to customize: + +- Python version +- FFmpeg version +- Excluded files/folders +- Build options +- Mirror settings + +## Output + +The build process creates: + +``` +dist/windows/ +├── Pixelle-Video-v0.1.0-win64/ # Build directory +│ ├── python/ # Python embedded +│ ├── tools/ # FFmpeg, etc. +│ ├── Pixelle-Video/ # Project files +│ ├── data/ # User data (empty) +│ ├── output/ # Output (empty) +│ ├── start.bat # Main launcher +│ ├── start_api.bat # API launcher +│ ├── start_web.bat # Web launcher +│ └── README.txt # User guide +├── Pixelle-Video-v0.1.0-win64.zip # ZIP package +└── Pixelle-Video-v0.1.0-win64.zip.sha256 # Checksum +``` + +## Build Process + +The builder performs these steps: + +1. **Download Phase** + - Python embedded distribution + - FFmpeg portable + - Cached in `.cache/` for reuse + +2. **Extract Phase** + - Extract Python to `build/python/` + - Extract FFmpeg to `build/tools/ffmpeg/` + +3. **Prepare Phase** + - Enable site-packages in Python + - Install pip + - Install uv (if configured) + +4. **Install Phase** + - Install project dependencies using uv/pip + - Pre-install all packages + +5. **Copy Phase** + - Copy project files (excluding test/docs/cache) + - Generate launcher scripts from templates + - Create empty directories + +6. **Package Phase** + - Create ZIP archive + - Generate SHA256 checksum + +## Templates + +Launcher script templates in `templates/`: + +- `start.bat` - Main Web UI launcher +- `start_api.bat` - API server launcher +- `start_web.bat` - Web UI only launcher +- `README.txt` - User documentation + +Templates support placeholders: +- `{VERSION}` - Project version +- `{BUILD_DATE}` - Build timestamp + +## Cache + +Downloaded files are cached in `.cache/`: + +``` +.cache/ +├── python-3.11.9-embed-amd64.zip +├── ffmpeg-6.1.1-win64.zip +└── get-pip.py +``` + +Delete cache to force re-download. + +## Troubleshooting + +### Build fails with "PyYAML not found" + +```bash +pip install pyyaml +``` + +### Downloads are slow + +Use China mirrors: + +```bash +python build.py --cn-mirror +``` + +### Dependencies installation fails + +Check: +1. Internet connection +2. PyPI mirrors accessibility +3. Project dependencies in `pyproject.toml` + +### ZIP creation fails + +Ensure: +1. Sufficient disk space +2. Write permissions to output directory +3. No files are locked by other processes + +## Advanced Usage + +### Custom Configuration + +Create custom config file: + +```bash +cp config/build_config.yaml config/my_config.yaml +# Edit my_config.yaml +python build.py --config config/my_config.yaml +``` + +### Skip ZIP Creation + +Edit `build_config.yaml`: + +```yaml +build: + create_zip: false +``` + +### Include Chrome Portable + +Edit `build_config.yaml`: + +```yaml +chrome: + include: true + download_url: "https://path/to/chrome-portable.zip" +``` + +## Maintenance + +### Update Python Version + +Edit `config/build_config.yaml`: + +```yaml +python: + version: "3.11.10" + download_url: "https://www.python.org/ftp/python/3.11.10/python-3.11.10-embed-amd64.zip" +``` + +### Update FFmpeg Version + +Edit `config/build_config.yaml`: + +```yaml +ffmpeg: + version: "6.2.0" + download_url: "https://github.com/BtbN/FFmpeg-Builds/releases/download/..." +``` + +## Distribution + +To distribute the package: + +1. Upload ZIP file to release page +2. Include SHA256 checksum for verification +3. Provide installation instructions + +Users verify download: + +```bash +# Windows PowerShell +Get-FileHash Pixelle-Video-v0.1.0-win64.zip -Algorithm SHA256 +``` + +Compare with `.sha256` file. + +## License + +Same as Pixelle-Video project license. + diff --git a/packaging/windows/build.py b/packaging/windows/build.py new file mode 100644 index 0000000..c0d9bd7 --- /dev/null +++ b/packaging/windows/build.py @@ -0,0 +1,690 @@ +#!/usr/bin/env python3 +""" +Windows Package Builder for Pixelle-Video + +This script automates the creation of a Windows portable package: +1. Downloads Python embedded distribution +2. Downloads FFmpeg portable +3. Prepares Python environment (enable site-packages, install pip) +4. Installs project dependencies +5. Copies project files +6. Generates launcher scripts +7. Creates final ZIP package + +Usage: + python build.py [--config CONFIG] [--output OUTPUT] [--cn-mirror] +""" + +import argparse +import hashlib +import os +import shutil +import subprocess +import sys +import tempfile +import zipfile +from datetime import datetime +from pathlib import Path +from typing import Optional +from urllib.request import urlretrieve + +try: + import yaml +except ImportError: + print("ERROR: PyYAML is required. Install it with: pip install pyyaml") + sys.exit(1) + + +class Color: + """ANSI color codes for terminal output""" + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + RESET = '\033[0m' + BOLD = '\033[1m' + + +class WindowsPackageBuilder: + """Build Windows portable package for Pixelle-Video""" + + def __init__(self, config_path: str, output_dir: Optional[str] = None, use_cn_mirror: bool = False): + self.config_path = Path(config_path) + self.script_dir = Path(__file__).parent + self.project_root = self.script_dir.parent.parent + + # Load configuration + with open(self.config_path, 'r', encoding='utf-8') as f: + self.config = yaml.safe_load(f) + + # Override mirror setting if specified + if use_cn_mirror: + self.config['mirrors']['use_cn_mirror'] = True + + # Setup paths + self.output_dir = Path(output_dir) if output_dir else self.project_root / self.config['build']['output_dir'] + self.cache_dir = self.project_root / self.config['cache']['cache_dir'] + self.templates_dir = self.script_dir / 'templates' + + # Get version from pyproject.toml + self.version = self._read_version() + self.package_name = f"{self.config['package']['name']}-v{self.version}-{self.config['package']['architecture']}" + self.build_dir = self.output_dir / self.package_name + + def _read_version(self) -> str: + """Read version from pyproject.toml""" + pyproject_path = self.project_root / 'pyproject.toml' + try: + import tomllib + except ImportError: + # Python < 3.11 fallback + try: + import tomli as tomllib + except ImportError: + # Simple regex fallback + import re + with open(pyproject_path, 'r') as f: + content = f.read() + match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content) + if match: + return match.group(1) + return "0.1.0" + + with open(pyproject_path, 'rb') as f: + pyproject = tomllib.load(f) + return pyproject.get('project', {}).get('version', '0.1.0') + + def log(self, message: str, level: str = "INFO"): + """Print colored log message""" + colors = { + "INFO": Color.BLUE, + "SUCCESS": Color.GREEN, + "WARNING": Color.YELLOW, + "ERROR": Color.RED, + "HEADER": Color.HEADER, + } + color = colors.get(level, Color.RESET) + print(f"{color}[{level}]{Color.RESET} {message}") + + def download_file(self, url: str, output_path: Path, description: str = "", max_retries: int = 3) -> bool: + """Download file with progress indication and retry support""" + import ssl + import urllib.request + + for attempt in range(max_retries): + try: + if attempt > 0: + self.log(f"Retry {attempt}/{max_retries}...") + + self.log(f"Downloading {description or url}...") + + # Create SSL context that's more lenient + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + def report_progress(block_num, block_size, total_size): + downloaded = block_num * block_size + percent = min(downloaded / total_size * 100, 100) if total_size > 0 else 0 + print(f"\r Progress: {percent:.1f}%", end='', flush=True) + + # Try with urllib first + opener = urllib.request.build_opener(urllib.request.HTTPSHandler(context=ssl_context)) + urllib.request.install_opener(opener) + urlretrieve(url, output_path, reporthook=report_progress) + print() # New line after progress + self.log(f"Downloaded to {output_path}", "SUCCESS") + return True + + except Exception as e: + self.log(f"Download attempt {attempt + 1} failed: {e}", "WARNING") + if attempt < max_retries - 1: + import time + time.sleep(2) # Wait before retry + else: + self.log(f"All download attempts failed", "ERROR") + # Try with curl as fallback + return self._download_with_curl(url, output_path, description) + + return False + + def _find_suitable_python(self) -> Optional[str]: + """Find a suitable Python 3.11+ for installing dependencies""" + candidates = [ + # Try common locations for newer Python versions + '/Users/puke/miniforge3/bin/python3', # User's conda + '/opt/homebrew/bin/python3', # Homebrew + '/usr/local/bin/python3', # Manual install + ] + + # Also check what's in PATH + for i in range(11, 14): # Python 3.11, 3.12, 3.13 + for py_name in [f'python3.{i}', f'python{i}']: + found = shutil.which(py_name) + if found and found not in candidates: + candidates.append(found) + + # Check generic python3 + python3_path = shutil.which('python3') + if python3_path and '.venv' not in python3_path: + candidates.append(python3_path) + + # Test each candidate + for candidate in candidates: + try: + if not candidate: + continue + + # Skip if in project venv + if '.venv' in candidate or 'venv' in candidate: + continue + + # Check if path exists + if not os.path.exists(candidate): + continue + + # Check Python version + result = subprocess.run( + [candidate, '-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + version = result.stdout.strip() + major, minor = map(int, version.split('.')) + + # Need Python 3.11+ + if major == 3 and minor >= 11: + # Check if pip is available + pip_check = subprocess.run( + [candidate, '-m', 'pip', '--version'], + capture_output=True, + timeout=5 + ) + if pip_check.returncode == 0: + self.log(f"Found Python {version} at {candidate}", "SUCCESS") + return candidate + except Exception as e: + continue + + return None + + def _download_with_curl(self, url: str, output_path: Path, description: str = "") -> bool: + """Fallback download method using curl""" + try: + self.log(f"Trying curl fallback for {description}...") + result = subprocess.run( + ['curl', '-L', '-o', str(output_path), url, '--progress-bar'], + check=True, + capture_output=False + ) + if result.returncode == 0 and output_path.exists(): + self.log(f"Downloaded with curl to {output_path}", "SUCCESS") + return True + except Exception as e: + self.log(f"Curl download also failed: {e}", "ERROR") + return False + + def download_python(self) -> Path: + """Download Python embedded distribution""" + python_config = self.config['python'] + cache_file = self.cache_dir / f"python-{python_config['version']}-embed-amd64.zip" + + if cache_file.exists(): + self.log(f"Using cached Python: {cache_file}") + return cache_file + + self.cache_dir.mkdir(parents=True, exist_ok=True) + + # Choose URL based on mirror setting + url = python_config['mirror_url'] if self.config['mirrors']['use_cn_mirror'] else python_config['download_url'] + + if self.download_file(url, cache_file, f"Python {python_config['version']}"): + return cache_file + else: + raise RuntimeError("Failed to download Python") + + def download_ffmpeg(self) -> Path: + """Download FFmpeg portable""" + ffmpeg_config = self.config['ffmpeg'] + cache_file = self.cache_dir / f"ffmpeg-{ffmpeg_config['version']}-win64.zip" + + if cache_file.exists(): + self.log(f"Using cached FFmpeg: {cache_file}") + return cache_file + + self.cache_dir.mkdir(parents=True, exist_ok=True) + + url = ffmpeg_config['mirror_url'] if self.config['mirrors']['use_cn_mirror'] else ffmpeg_config['download_url'] + + if self.download_file(url, cache_file, f"FFmpeg {ffmpeg_config['version']}"): + return cache_file + else: + raise RuntimeError("Failed to download FFmpeg") + + def extract_python(self, zip_path: Path, target_dir: Path): + """Extract Python embedded distribution""" + self.log(f"Extracting Python to {target_dir}...") + target_dir.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(target_dir) + + # Add execute permissions to .exe files (needed on Unix systems) + if os.name != 'nt': # Not on Windows + for exe_file in target_dir.glob('*.exe'): + os.chmod(exe_file, 0o755) + for exe_file in target_dir.glob('**/*.exe'): + os.chmod(exe_file, 0o755) + + self.log("Python extracted successfully", "SUCCESS") + + def extract_ffmpeg(self, zip_path: Path, target_dir: Path): + """Extract FFmpeg portable""" + self.log(f"Extracting FFmpeg to {target_dir}...") + temp_extract = target_dir.parent / "ffmpeg_temp" + temp_extract.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(temp_extract) + + # Find the bin directory (FFmpeg archive has nested structure) + bin_dir = None + for root, dirs, files in os.walk(temp_extract): + if 'bin' in dirs: + bin_dir = Path(root) / 'bin' + break + + if bin_dir and bin_dir.exists(): + target_dir.mkdir(parents=True, exist_ok=True) + shutil.copytree(bin_dir, target_dir, dirs_exist_ok=True) + shutil.rmtree(temp_extract) + self.log("FFmpeg extracted successfully", "SUCCESS") + else: + raise RuntimeError("FFmpeg bin directory not found in archive") + + def prepare_python_environment(self, python_dir: Path): + """Prepare Python environment: enable site-packages""" + self.log("Preparing Python environment...") + + # Modify python311._pth to enable site-packages + pth_file = python_dir / "python311._pth" + if pth_file.exists(): + with open(pth_file, 'r') as f: + lines = f.readlines() + + # Uncomment "import site" line or add it + modified = False + for i, line in enumerate(lines): + if line.strip().startswith('#import site'): + lines[i] = 'import site\n' + modified = True + break + + if not modified and 'import site' not in ''.join(lines): + lines.append('import site\n') + + with open(pth_file, 'w') as f: + f.writelines(lines) + + self.log("Enabled site-packages in Python", "SUCCESS") + + # Note: On non-Windows systems, we can't run python.exe directly + # Pip and dependencies will be installed using system Python + if os.name == 'nt': + # On Windows, we can install pip directly + python_exe = python_dir / "python.exe" + get_pip_path = self.cache_dir / "get-pip.py" + + if not get_pip_path.exists(): + self.log("Downloading get-pip.py...") + pip_url = "https://bootstrap.pypa.io/get-pip.py" + self.download_file(pip_url, get_pip_path, "get-pip.py") + + self.log("Installing pip...") + result = subprocess.run( + [str(python_exe), str(get_pip_path)], + capture_output=True, + text=True + ) + + if result.returncode == 0: + self.log("Pip installed successfully", "SUCCESS") + else: + self.log(f"Pip installation warning: {result.stderr}", "WARNING") + else: + self.log("Cross-platform build detected (building on non-Windows)", "INFO") + self.log("Dependencies will be installed using system Python", "INFO") + + def install_dependencies(self, python_dir: Path): + """Install project dependencies""" + self.log("Installing project dependencies...") + + # Determine target directory for site-packages + site_packages = python_dir / "Lib" / "site-packages" + site_packages.mkdir(parents=True, exist_ok=True) + + if os.name == 'nt': + # On Windows, use the embedded Python + python_exe = python_dir / "python.exe" + + # Install uv first if configured + if self.config['build'].get('use_uv', True): + self.log("Installing uv...") + subprocess.run( + [str(python_exe), "-m", "pip", "install", "uv"], + check=True + ) + + # Install dependencies + if self.config['build'].get('use_uv', True): + cmd = [str(python_exe), "-m", "uv", "pip", "install", "-e", str(self.project_root)] + if self.config['mirrors']['use_cn_mirror']: + cmd.extend(["--index-url", self.config['mirrors']['pypi_mirror']]) + else: + cmd = [str(python_exe), "-m", "pip", "install", "-e", str(self.project_root)] + if self.config['mirrors']['use_cn_mirror']: + cmd.extend(["--index-url", self.config['mirrors']['pypi_mirror']]) + + self.log(f"Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + self.log("Dependencies installed successfully", "SUCCESS") + else: + self.log(f"Dependency installation failed:\n{result.stderr}", "ERROR") + raise RuntimeError("Failed to install dependencies") + else: + # Cross-platform build: use system Python to install to target directory + self.log("Cross-platform build: using system Python to install dependencies") + + # Find a Python 3.11+ executable (not from project venv) + python_cmd = self._find_suitable_python() + + if not python_cmd: + self.log("No suitable Python 3.11+ found. Please install Python 3.11+ or use Windows to build.", "ERROR") + raise RuntimeError("Python 3.11+ required for cross-platform build") + + self.log(f"Using Python: {python_cmd}") + + # Use pip with --target to install to specific directory + cmd = [ + python_cmd, "-m", "pip", "install", + "--target", str(site_packages), + "--no-user", + "--no-warn-script-location" + ] + + # Read dependencies from pyproject.toml + try: + import tomllib + except ImportError: + try: + import tomli as tomllib + except ImportError: + self.log("tomllib/tomli not available, trying simple parsing", "WARNING") + tomllib = None + + if tomllib: + pyproject_path = self.project_root / "pyproject.toml" + with open(pyproject_path, 'rb') as f: + pyproject = tomllib.load(f) + deps = pyproject.get('project', {}).get('dependencies', []) + else: + # Simple fallback: read from pyproject.toml manually + import re + pyproject_path = self.project_root / "pyproject.toml" + with open(pyproject_path, 'r') as f: + content = f.read() + # Find dependencies section + deps_match = re.search(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL) + if deps_match: + deps_str = deps_match.group(1) + deps = [dep.strip(' "\',\n') for dep in deps_str.split('\n') if dep.strip() and not dep.strip().startswith('#')] + else: + deps = [] + + if deps: + cmd.extend(deps) + + if self.config['mirrors']['use_cn_mirror']: + cmd.extend(["--index-url", self.config['mirrors']['pypi_mirror']]) + + self.log(f"Installing {len(deps)} dependencies...") + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + self.log("Dependencies installed successfully", "SUCCESS") + else: + self.log(f"Dependency installation output:\n{result.stdout}", "INFO") + if result.stderr: + self.log(f"Warnings: {result.stderr}", "WARNING") + else: + self.log("No dependencies found in pyproject.toml", "WARNING") + + def copy_project_files(self, target_dir: Path): + """Copy project files to build directory""" + self.log(f"Copying project files to {target_dir}...") + + exclude_patterns = self.config['build']['exclude_patterns'] + + def should_exclude(path: Path) -> bool: + path_str = str(path.relative_to(self.project_root)) + for pattern in exclude_patterns: + if pattern.endswith('/*'): + # Directory content exclusion + if path_str.startswith(pattern[:-2]): + return True + elif pattern.endswith('*'): + # Wildcard pattern + if path_str.startswith(pattern[:-1]): + return True + elif '*' in pattern: + # Glob pattern (simple check) + import fnmatch + if fnmatch.fnmatch(path_str, pattern): + return True + else: + # Exact match or directory + if path_str == pattern or path_str.startswith(f"{pattern}/"): + return True + return False + + target_dir.mkdir(parents=True, exist_ok=True) + + # Copy files + copied_count = 0 + for item in self.project_root.iterdir(): + if item.name in ['.git', 'packaging', 'dist', '.venv', 'venv']: + continue + + if should_exclude(item): + continue + + target_path = target_dir / item.name + + if item.is_file(): + shutil.copy2(item, target_path) + copied_count += 1 + elif item.is_dir(): + shutil.copytree(item, target_path, ignore=lambda d, names: [ + n for n in names if should_exclude(Path(d) / n) + ]) + # Count files in copied directory + copied_count += sum(1 for _ in target_path.rglob('*') if _.is_file()) + + self.log(f"Copied {copied_count} files", "SUCCESS") + + def generate_launcher_scripts(self): + """Generate launcher scripts from templates""" + self.log("Generating launcher scripts...") + + replacements = { + '{VERSION}': self.version, + '{BUILD_DATE}': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + } + + # Copy and process templates + for template_file in self.templates_dir.glob('*'): + if template_file.is_file(): + target_file = self.build_dir / template_file.name + + with open(template_file, 'r', encoding='utf-8') as f: + content = f.read() + + # Replace placeholders + for key, value in replacements.items(): + content = content.replace(key, value) + + with open(target_file, 'w', encoding='utf-8', newline='\r\n') as f: + f.write(content) + + self.log(f"Generated: {template_file.name}") + + self.log("Launcher scripts generated", "SUCCESS") + + def create_empty_directories(self): + """Create empty directories specified in config""" + self.log("Creating empty directories...") + + for dir_name in self.config['build'].get('create_empty_dirs', []): + dir_path = self.build_dir / dir_name + dir_path.mkdir(parents=True, exist_ok=True) + # Create .gitkeep to preserve directory in git + (dir_path / '.gitkeep').touch() + + self.log("Empty directories created", "SUCCESS") + + def create_zip_package(self): + """Create final ZIP package""" + if not self.config['build'].get('create_zip', True): + return + + zip_path = self.output_dir / f"{self.package_name}.zip" + self.log(f"Creating ZIP package: {zip_path}...") + + compression_map = { + 'deflate': zipfile.ZIP_DEFLATED, + 'bzip2': zipfile.ZIP_BZIP2, + 'lzma': zipfile.ZIP_LZMA, + } + compression = compression_map.get( + self.config['build'].get('zip_compression', 'deflate'), + zipfile.ZIP_DEFLATED + ) + + with zipfile.ZipFile(zip_path, 'w', compression) as zipf: + for root, dirs, files in os.walk(self.build_dir): + for file in files: + file_path = Path(root) / file + arcname = file_path.relative_to(self.build_dir.parent) + zipf.write(file_path, arcname) + + # Calculate file size and hash + size_mb = zip_path.stat().st_size / (1024 * 1024) + + with open(zip_path, 'rb') as f: + file_hash = hashlib.sha256(f.read()).hexdigest() + + self.log(f"ZIP package created: {zip_path}", "SUCCESS") + self.log(f"Size: {size_mb:.2f} MB") + self.log(f"SHA256: {file_hash}") + + # Write hash to file + hash_file = zip_path.with_suffix('.zip.sha256') + with open(hash_file, 'w') as f: + f.write(f"{file_hash} {zip_path.name}\n") + + def build(self): + """Main build process""" + self.log("=" * 60, "HEADER") + self.log(f"Building {self.package_name}", "HEADER") + self.log("=" * 60, "HEADER") + + try: + # Clean build directory + if self.build_dir.exists(): + self.log(f"Cleaning existing build directory: {self.build_dir}") + shutil.rmtree(self.build_dir) + + self.build_dir.mkdir(parents=True, exist_ok=True) + self.output_dir.mkdir(parents=True, exist_ok=True) + + # Download dependencies + python_zip = self.download_python() + ffmpeg_zip = self.download_ffmpeg() + + # Extract Python + python_dir = self.build_dir / "python" / "python311" + self.extract_python(python_zip, python_dir) + + # Extract FFmpeg + ffmpeg_dir = self.build_dir / "tools" / "ffmpeg" / "bin" + self.extract_ffmpeg(ffmpeg_zip, ffmpeg_dir) + + # Prepare Python environment + self.prepare_python_environment(python_dir) + + # Install dependencies + if self.config['build'].get('pre_install_deps', True): + self.install_dependencies(python_dir) + + # Copy project files + project_target = self.build_dir / "Pixelle-Video" + self.copy_project_files(project_target) + + # Generate launcher scripts + self.generate_launcher_scripts() + + # Create empty directories + self.create_empty_directories() + + # Create ZIP package + self.create_zip_package() + + self.log("=" * 60, "HEADER") + self.log("Build completed successfully!", "SUCCESS") + self.log(f"Package location: {self.build_dir}", "SUCCESS") + self.log("=" * 60, "HEADER") + + except Exception as e: + self.log(f"Build failed: {e}", "ERROR") + import traceback + traceback.print_exc() + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser(description="Build Windows portable package for Pixelle-Video") + parser.add_argument( + '--config', + default='packaging/windows/config/build_config.yaml', + help='Path to build configuration file' + ) + parser.add_argument( + '--output', + help='Output directory (default: dist/windows)' + ) + parser.add_argument( + '--cn-mirror', + action='store_true', + help='Use China mirrors for faster downloads' + ) + + args = parser.parse_args() + + builder = WindowsPackageBuilder( + config_path=args.config, + output_dir=args.output, + use_cn_mirror=args.cn_mirror + ) + builder.build() + + +if __name__ == '__main__': + main() + diff --git a/packaging/windows/config/build_config.yaml b/packaging/windows/config/build_config.yaml new file mode 100644 index 0000000..a9b75d1 --- /dev/null +++ b/packaging/windows/config/build_config.yaml @@ -0,0 +1,78 @@ +# Windows Package Build Configuration + +# Package information +package: + name: Pixelle-Video + version_source: pyproject.toml # Read version from pyproject.toml + architecture: win64 + +# Python configuration +python: + version: "3.11.9" + download_url: "https://www.python.org/ftp/python/3.11.9/python-3.11.9-embed-amd64.zip" + # Mirror for China users (optional) + mirror_url: "https://mirrors.huaweicloud.com/python/3.11.9/python-3.11.9-embed-amd64.zip" + +# FFmpeg configuration +ffmpeg: + version: "6.1.1" + download_url: "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" + # Alternative mirror + mirror_url: "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip" + +# Chrome/Chromium configuration (for html2image) +chrome: + include: false # Set to true to bundle Chrome portable + version: "120.0.6099.109" + download_url: "" # Add portable Chrome download URL if needed + note: "Users can use system Chrome or install separately" + +# Build options +build: + # Files/folders to exclude from project copy + exclude_patterns: + - ".git" + - ".github" + - "__pycache__" + - "*.pyc" + - ".pytest_cache" + - ".ruff_cache" + - "*.log" + - ".DS_Store" + - "output/*" # Don't include output files + - "temp/*" + - "plans/*" # Don't include planning docs + - "repositories/*" # Don't include referenced repos + - "docs/*" # Don't include docs in package + - "test_*.py" # Don't include test files + - ".venv" + - "venv" + - "node_modules" + - "uv.lock" + + # Dependencies installation + use_uv: true # Use uv for faster dependency installation + pre_install_deps: true # Install deps during build (recommended for end users) + + # Output + output_dir: "dist/windows" + create_zip: true + zip_compression: "deflate" # deflate, bzip2, lzma + + # Additional options + include_readme: true + include_license: true + create_empty_dirs: + - "data" + - "output" + +# Download cache +cache: + enabled: true + cache_dir: "packaging/windows/.cache" + +# Mirror settings (for China users) +mirrors: + use_cn_mirror: false # Set to true for faster downloads in China + pypi_mirror: "https://pypi.tuna.tsinghua.edu.cn/simple" + diff --git a/packaging/windows/requirements.txt b/packaging/windows/requirements.txt new file mode 100644 index 0000000..d118769 --- /dev/null +++ b/packaging/windows/requirements.txt @@ -0,0 +1,5 @@ +# Requirements for building Windows package +# Install with: pip install -r requirements.txt + +pyyaml>=6.0.0 + diff --git a/packaging/windows/templates/README.txt b/packaging/windows/templates/README.txt new file mode 100644 index 0000000..d3b708f --- /dev/null +++ b/packaging/windows/templates/README.txt @@ -0,0 +1,110 @@ +======================================== + Pixelle-Video - Windows Portable +======================================== + +AI-powered video creation platform + +Version: {VERSION} +Build Date: {BUILD_DATE} + +======================================== + Quick Start +======================================== + +1. Double-click "start.bat" to launch the Web UI +2. Configure your API keys in the Web UI (Settings section) +3. Open your browser at: http://localhost:8501 + +======================================== + Available Launchers +======================================== + +start.bat - Launch Web UI (Default) +start_api.bat - Launch API Server only (Port 8000) +start_web.bat - Launch Web UI only (Port 8501) + +For most users, just use "start.bat" + +======================================== + First-Time Setup +======================================== + +1. On first run, the Web UI will start with default configuration +2. Click on "Settings" in the Web UI to configure: + - LLM API Key (OpenAI/Qwen/DeepSeek/etc) + - LLM Base URL and Model + - ComfyUI settings (use RunningHub or local ComfyUI) +3. Click "Save Config" to save your settings +4. Configuration will be automatically saved to config.yaml + +======================================== + Configuration +======================================== + +Configuration is done through the Web UI: + +1. Launch the application using start.bat +2. Click on "Settings" in the Web UI +3. Fill in the required fields: + - LLM API Key: Your LLM provider API key + - LLM Base URL: LLM API endpoint + - LLM Model: Model name (e.g., gpt-4o, qwen-max) + - ComfyUI URL: For local ComfyUI (default: http://127.0.0.1:8188) + - RunningHub API Key: For cloud image generation (optional) +4. Click "Save Config" to save + +The configuration will be automatically saved to Pixelle-Video/config.yaml. + +Note: You can also manually edit config.yaml if needed, but the Web UI is recommended. + +======================================== + Folder Structure +======================================== + +python/ - Python 3.11 embedded runtime +tools/ - FFmpeg and other utilities +Pixelle-Video/ - Main application +data/ - User data (BGM, templates, workflows) +output/ - Generated videos + +======================================== + System Requirements +======================================== + +- Windows 10/11 (64-bit) +- 4GB RAM minimum (8GB recommended) +- Internet connection (for API calls and ComfyUI cloud) +- Modern web browser (Chrome/Edge/Firefox) + +======================================== + Troubleshooting +======================================== + +Problem: "Python not found" +Solution: Ensure python/ folder exists and is not corrupted + +Problem: "Failed to start" +Solution: Check if Python and dependencies are installed correctly + +Problem: "Port already in use" +Solution: Close other applications using port 8501 or 8000 + +Problem: "Module not found" +Solution: Re-extract the package completely, don't move files + +======================================== + Support +======================================== + +GitHub: https://github.com/AIDC-AI/Pixelle-Video +Documentation: https://pixelle.ai/docs +Issues: https://github.com/AIDC-AI/Pixelle-Video/issues + +======================================== + License +======================================== + +See LICENSE file in Pixelle-Video/ folder + +Copyright (c) 2025 Pixelle.AI + diff --git a/packaging/windows/templates/start.bat b/packaging/windows/templates/start.bat new file mode 100644 index 0000000..cbfda16 --- /dev/null +++ b/packaging/windows/templates/start.bat @@ -0,0 +1,40 @@ +@echo off +chcp 65001 >nul +setlocal enabledelayedexpansion + +echo ======================================== +echo Pixelle-Video - Windows Launcher +echo ======================================== +echo. + +:: Set environment variables +set "PYTHON_HOME=%~dp0python\python311" +set "PATH=%PYTHON_HOME%;%PYTHON_HOME%\Scripts;%~dp0tools\ffmpeg\bin;%PATH%" +set "PROJECT_ROOT=%~dp0Pixelle-Video" + +:: Change to project directory +cd /d "%PROJECT_ROOT%" + +:: Set PYTHONPATH to project root for module imports +set "PYTHONPATH=%PROJECT_ROOT%" + +:: Start Web UI +echo [Starting] Launching Pixelle-Video Web UI... +echo Browser will open at: http://localhost:8501 +echo. +echo Note: Configure API keys and settings in the Web UI. +echo Press Ctrl+C to stop the server +echo ======================================== +echo. + +"%PYTHON_HOME%\python.exe" -m streamlit run web\app.py + +if errorlevel 1 ( + echo. + echo [ERROR] Failed to start. Please check: + echo 1. Python is properly installed + echo 2. Dependencies are installed + echo. + pause +) + diff --git a/packaging/windows/templates/start_api.bat b/packaging/windows/templates/start_api.bat new file mode 100644 index 0000000..735cbcb --- /dev/null +++ b/packaging/windows/templates/start_api.bat @@ -0,0 +1,42 @@ +@echo off +chcp 65001 >nul +setlocal enabledelayedexpansion + +echo ======================================== +echo Pixelle-Video - API Server Launcher +echo ======================================== +echo. + +:: Set environment variables +set "PYTHON_HOME=%~dp0python\python311" +set "PATH=%PYTHON_HOME%;%PYTHON_HOME%\Scripts;%~dp0tools\ffmpeg\bin;%PATH%" +set "PROJECT_ROOT=%~dp0Pixelle-Video" + +:: Change to project directory +cd /d "%PROJECT_ROOT%" + +:: Set PYTHONPATH to project root for module imports +set "PYTHONPATH=%PROJECT_ROOT%" + +:: Start API Server +echo [Starting] Launching Pixelle-Video API Server... +echo API will be available at: http://localhost:8000 +echo API Documentation: http://localhost:8000/docs +echo. +echo Note: Configure API keys and settings in the Web UI. +echo Press Ctrl+C to stop the server +echo ======================================== +echo. + +"%PYTHON_HOME%\python.exe" -m uvicorn api.app:app --host 0.0.0.0 --port 8000 + +if errorlevel 1 ( + echo. + echo [ERROR] Failed to start. Please check: + echo 1. Python is properly installed + echo 2. Dependencies are installed + echo 3. Port 8000 is not already in use + echo. + pause +) + diff --git a/packaging/windows/templates/start_web.bat b/packaging/windows/templates/start_web.bat new file mode 100644 index 0000000..aa052b9 --- /dev/null +++ b/packaging/windows/templates/start_web.bat @@ -0,0 +1,44 @@ +@echo off +chcp 65001 >nul +setlocal enabledelayedexpansion + +echo ======================================== +echo Pixelle-Video - Web UI Launcher +echo ======================================== +echo. + +:: Set environment variables +set "PYTHON_HOME=%~dp0python\python311" +set "PATH=%PYTHON_HOME%;%PYTHON_HOME%\Scripts;%~dp0tools\ffmpeg\bin;%PATH%" +set "PROJECT_ROOT=%~dp0Pixelle-Video" + +:: Change to project directory +cd /d "%PROJECT_ROOT%" + +:: Set PYTHONPATH to project root for module imports +set "PYTHONPATH=%PROJECT_ROOT%" + +:: Start Web UI (Standalone mode) +echo [Starting] Launching Pixelle-Video Web UI (Standalone)... +echo Browser will open at: http://localhost:8501 +echo. +echo NOTE: This runs Web UI only. For full features, use start.bat +echo or run start_api.bat in another window. +echo Note: Configure API keys and settings in the Web UI. +echo. +echo Press Ctrl+C to stop the server +echo ======================================== +echo. + +"%PYTHON_HOME%\python.exe" -m streamlit run web\app.py + +if errorlevel 1 ( + echo. + echo [ERROR] Failed to start. Please check: + echo 1. Python is properly installed + echo 2. Dependencies are installed + echo 3. Port 8501 is not already in use + echo. + pause +) +