添加windows打包逻辑
This commit is contained in:
216
packaging/windows/README.md
Normal file
216
packaging/windows/README.md
Normal file
@@ -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.
|
||||
|
||||
690
packaging/windows/build.py
Normal file
690
packaging/windows/build.py
Normal file
@@ -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()
|
||||
|
||||
78
packaging/windows/config/build_config.yaml
Normal file
78
packaging/windows/config/build_config.yaml
Normal file
@@ -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"
|
||||
|
||||
5
packaging/windows/requirements.txt
Normal file
5
packaging/windows/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# Requirements for building Windows package
|
||||
# Install with: pip install -r requirements.txt
|
||||
|
||||
pyyaml>=6.0.0
|
||||
|
||||
110
packaging/windows/templates/README.txt
Normal file
110
packaging/windows/templates/README.txt
Normal file
@@ -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
|
||||
|
||||
40
packaging/windows/templates/start.bat
Normal file
40
packaging/windows/templates/start.bat
Normal file
@@ -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
|
||||
)
|
||||
|
||||
42
packaging/windows/templates/start_api.bat
Normal file
42
packaging/windows/templates/start_api.bat
Normal file
@@ -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
|
||||
)
|
||||
|
||||
44
packaging/windows/templates/start_web.bat
Normal file
44
packaging/windows/templates/start_web.bat
Normal file
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user