Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sd_installer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
from .installer import Installer
from .verifier import Verifier


__all__ = ["Installer", "Verifier", "__version__"]
1 change: 1 addition & 0 deletions sd_installer/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

from .cli import main


if __name__ == "__main__":
exit(main())
39 changes: 23 additions & 16 deletions sd_installer/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@
"""

import argparse
import os
import sys
from pathlib import Path


def find_base_folder() -> Path:
"""
r"""
Find the StreamDiffusion base folder (where setup.py lives).

Runtime structure:
Expand All @@ -47,9 +46,9 @@ def find_base_folder() -> Path:
# __file__ = .../StreamDiffusion-installer/sd_installer/cli.py
# We want: .../StreamDiffusion/
this_file = Path(__file__).resolve()
sd_installer_pkg = this_file.parent # sd_installer/
installer_repo = sd_installer_pkg.parent # StreamDiffusion-installer/
base = installer_repo.parent # StreamDiffusion/
sd_installer_pkg = this_file.parent # sd_installer/
installer_repo = sd_installer_pkg.parent # StreamDiffusion-installer/
base = installer_repo.parent # StreamDiffusion/
if (base / "setup.py").exists():
return base

Expand Down Expand Up @@ -85,7 +84,7 @@ def cmd_check(args):
if venv_path.exists():
print(f"Venv: Found at {venv_path}")
else:
print(f"Venv: Not found (will be created during install)")
print("Venv: Not found (will be created during install)")

# Check StreamDiffusion setup.py (base folder IS StreamDiffusion)
setup_py = base / "setup.py"
Expand Down Expand Up @@ -197,15 +196,15 @@ def cmd_diagnose(args):
print(f" [{status}] {check['name']}")
if check["error"]:
# Print just the last line of the error
error_line = check["error"].split('\n')[-1][:60]
error_line = check["error"].split("\n")[-1][:60]
print(f" {error_line}")

return 0


def cmd_repair(args):
"""Auto-fix known issues."""
from .verifier import Verifier, KNOWN_ERRORS
from .verifier import Verifier

try:
base = Path(args.base_folder) if args.base_folder else find_base_folder()
Expand Down Expand Up @@ -236,6 +235,7 @@ def cmd_repair(args):
for check in info["checks"]:
if not check["passed"] and check["error"]:
from .verifier import match_known_error

fix = match_known_error(check["error"])
if fix:
fixes_needed.append((check["name"], fix))
Expand All @@ -245,10 +245,15 @@ def cmd_repair(args):
# numpy 2.x
numpy_ver = info["versions"].get("numpy", "")
if numpy_ver.startswith("2."):
fixes_needed.append(("numpy version", {
"cause": f"numpy {numpy_ver} detected (2.x breaks things)",
"fix": "pip install numpy==1.26.4 --force-reinstall"
}))
fixes_needed.append(
(
"numpy version",
{
"cause": f"numpy {numpy_ver} detected (2.x breaks things)",
"fix": "pip install numpy==1.26.4 --force-reinstall",
},
)
)

if not fixes_needed:
print("No known issues detected that can be auto-fixed.")
Expand All @@ -264,18 +269,19 @@ def cmd_repair(args):

if not args.yes:
response = input("Apply fixes? [y/N]: ")
if response.lower() != 'y':
if response.lower() != "y":
print("Aborted.")
return 0

# Apply fixes
import subprocess

for name, fix in fixes_needed:
print(f"Applying fix for {name}...")
cmd = [str(python_exe), "-m", "pip"] + fix["fix"].replace("pip ", "").split()
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f" OK")
print(" OK")
else:
print(f" FAILED: {result.stderr}")

Expand Down Expand Up @@ -314,7 +320,7 @@ def cmd_generate_bat(args):

def cmd_install_tensorrt(args):
"""Install TensorRT packages."""
from .tensorrt import install, get_cuda_version_from_torch
from .tensorrt import get_cuda_version_from_torch, install

print("StreamDiffusionTD TensorRT Installation")
print("=" * 40)
Expand Down Expand Up @@ -381,7 +387,8 @@ def main():
# repair command
repair_parser = subparsers.add_parser("repair", help="Auto-fix known issues")
repair_parser.add_argument(
"-y", "--yes",
"-y",
"--yes",
action="store_true",
help="Apply fixes without prompting",
)
Expand Down
90 changes: 68 additions & 22 deletions sd_installer/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
5. Verify imports - Catch failures immediately
"""

import os
import sys
import subprocess
import shutil
import sys
from pathlib import Path
from typing import Optional, Callable
from typing import Callable, Optional


# Version pins - packages NOT in setup.py that must be manually pinned
MANUAL_PINS = {
Expand All @@ -26,6 +25,10 @@
"python-osc": "", # Required for TouchDesigner OSC communication
"peft": "0.17.1", # Required for Cached Attention (StreamV2V) - enables USE_PEFT_BACKEND
"protobuf": "4.25.8", # Required by mediapipe, onnx/TensorRT - protobuf 6.x breaks serialization, setup.py requires >=4.25.8
# Security floor pins (transitive deps — pip resolves these on fresh install, but floor ensures upgrade on update)
"idna": ">=3.16", # CVE-2026-45409: punycode resource exhaustion
"Mako": ">=1.3.12", # CVE-2026-44307: Windows backslash path traversal
"urllib3": ">=2.7.0", # CVE-2026-44432/44431: response over-decompression; cross-origin redirect
}

# Pre-built insightface wheels for Windows (PyPI has no Windows wheels, requires C++ build tools)
Expand All @@ -36,6 +39,14 @@
(3, 12): "https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl",
}

# Pre-built cuda-link wheel (CUDA-IPC zero-copy transport). setup.py's cuda-link pin lives only in
# the optional cuda_ipc extra as a git ref (compiled cp311 extension) — installing that extra would
# force an MSVC/nvcc source build. Install the prebuilt wheel directly instead, --no-deps, so this
# extra is never triggered. Only a cp311 wheel is published.
CUDA_LINK_WHEELS = {
(3, 11): "https://github.com/forkni/cuda-link/releases/download/v1.12.0/cuda_link-1.12.0-cp311-cp311-win_amd64.whl",
}

# PyTorch configurations by CUDA version
PYTORCH_CONFIGS = {
"cu118": {
Expand Down Expand Up @@ -63,9 +74,9 @@
"xformers": None, # Skip - causes conflicts
},
"cu128": {
"torch": "2.7.0",
"torchvision": "0.22.0",
"torchaudio": "2.7.0",
"torch": "2.8.0",
"torchvision": "0.23.0",
"torchaudio": None,
"index_url": "https://download.pytorch.org/whl/cu128",
"cuda_python": "12.9.0",
"xformers": None, # Not needed - PyTorch 2.7+ has native SDPA
Expand Down Expand Up @@ -103,10 +114,7 @@ def __init__(

# Validate CUDA version
if cuda_version not in PYTORCH_CONFIGS:
raise ValueError(
f"Unsupported CUDA version: {cuda_version}. "
f"Supported: {list(PYTORCH_CONFIGS.keys())}"
)
raise ValueError(f"Unsupported CUDA version: {cuda_version}. Supported: {list(PYTORCH_CONFIGS.keys())}")

self.pytorch_config = PYTORCH_CONFIGS[cuda_version]

Expand Down Expand Up @@ -238,7 +246,7 @@ def phase3b_insightface(self):

version_str = result.stdout.strip()
try:
major, minor = map(int, version_str.split('.'))
major, minor = map(int, version_str.split("."))
py_version = (major, minor)
except ValueError:
print(f" WARNING: Could not parse Python version '{version_str}', skipping insightface pre-install")
Expand All @@ -260,6 +268,34 @@ def phase4_streamdiffusion(self):
# The -e flag makes it editable, setup.py handles all pinned versions
self._run_pip(["-e", ".[tensorrt,controlnet,ipadapter]"], check=True, cwd=self.streamdiffusion_path)

def phase4b_cuda_link(self):
"""Phase 4b: Install cuda-link from pre-built wheel (CUDA-IPC zero-copy transport).

Not covered by any setup.py extra actually installed above (cuda_ipc is intentionally
skipped to avoid a source build) — install the compiled wheel directly. Non-fatal: if no
wheel exists for this venv's Python, CUDA-IPC falls back to the mirror-DAT transport.
"""
result = self._run_python("import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
if result.returncode != 0:
print(" WARNING: Could not detect venv Python version, skipping cuda-link pre-install")
return

version_str = result.stdout.strip()
try:
major, minor = map(int, version_str.split("."))
py_version = (major, minor)
except ValueError:
print(f" WARNING: Could not parse Python version '{version_str}', skipping cuda-link pre-install")
return

wheel_url = CUDA_LINK_WHEELS.get(py_version)
if wheel_url:
self._report_progress(f"Installing cuda-link 1.12.0 from pre-built wheel (Python {version_str})...", 4, 8)
self._run_pip(["--no-deps", wheel_url], check=False)
else:
print(f" WARNING: No pre-built cuda-link wheel for Python {version_str}")
print(" CUDA-IPC zero-copy export will fall back to the mirror-DAT transport")

def phase5_missing_pins(self):
"""Phase 5: Install packages not pinned in setup.py and fix diffusers."""
self._report_progress("Installing packages not in setup.py (timm, python-osc, peft)...", 5, 8)
Expand All @@ -269,33 +305,42 @@ def phase5_missing_pins(self):

# Force reinstall varshith15 diffusers (other deps may have overwritten it)
self._report_progress("Ensuring varshith15 diffusers fork with kvo_cache support...", 5, 8)
self._run_pip([
"--force-reinstall", "--no-deps",
"diffusers @ git+https://github.com/varshith15/diffusers.git@3e3b72f557e91546894340edabc845e894f00922"
])
self._run_pip(
[
"--force-reinstall",
"--no-deps",
"diffusers @ git+https://github.com/varshith15/diffusers.git@3e3b72f557e91546894340edabc845e894f00922",
]
)

def phase6_conflict_prone(self):
"""Phase 6: Fix conflict-prone packages with --no-deps."""
self._report_progress("Fixing conflict-prone packages...", 6, 8)

# Remove conflicting opencv variants
subprocess.run(
[str(self.python_exe), "-m", "pip", "uninstall", "-y",
"opencv-python-headless", "opencv-contrib-python"],
[str(self.python_exe), "-m", "pip", "uninstall", "-y", "opencv-python-headless", "opencv-contrib-python"],
capture_output=True,
)

# Install correct opencv
self._run_pip(["--no-deps", f"opencv-python=={MANUAL_PINS['opencv-python']}"])

def phase7_numpy_lock(self):
"""Phase 7: Final numpy and protobuf lock (other packages may have upgraded them)."""
"""Phase 7: Final numpy/protobuf lock + security floor pins."""
self._report_progress(f"Final numpy lock (numpy=={MANUAL_PINS['numpy']})...", 7, 8)
self._run_pip([f"numpy=={MANUAL_PINS['numpy']}", "--force-reinstall"])

self._report_progress(f"Final protobuf lock (protobuf=={MANUAL_PINS['protobuf']})...", 7, 8)
self._run_pip([f"protobuf=={MANUAL_PINS['protobuf']}", "--force-reinstall"])

self._report_progress("Applying security floor pins (idna, Mako, urllib3)...", 7, 8)
self._run_pip([
f"idna{MANUAL_PINS['idna']}",
f"Mako{MANUAL_PINS['Mako']}",
f"urllib3{MANUAL_PINS['urllib3']}",
])

def phase8_verify(self) -> bool:
"""Phase 8: Verify installation with import tests."""
from .verifier import Verifier
Expand All @@ -315,7 +360,7 @@ def install(self, python_exe: Optional[str] = None) -> bool:
True if installation and verification succeeded.
"""
print("=" * 50)
print(" StreamDiffusionTD v0.3.1 Installation")
print(" StreamDiffusionTD v0.3.2 Installation")
print(" Daydream Fork with StreamV2V")
print("=" * 50)
print()
Expand All @@ -332,6 +377,7 @@ def install(self, python_exe: Optional[str] = None) -> bool:
self.phase3_xformers()
self.phase3b_insightface() # Pre-install insightface from wheel (Windows)
self.phase4_streamdiffusion()
self.phase4b_cuda_link() # Pre-install cuda-link from wheel (CUDA-IPC transport)
self.phase5_missing_pins()
self.phase6_conflict_prone()
self.phase7_numpy_lock()
Expand Down Expand Up @@ -374,7 +420,7 @@ def generate_batch_file(self, output_path: Optional[str] = None, python_exe: Opt

content = f'''@echo off
echo ========================================
echo StreamDiffusionTD v0.3.1 Installation
echo StreamDiffusionTD v0.3.2 Installation
echo Daydream Fork with StreamV2V
echo ========================================

Expand All @@ -386,7 +432,7 @@ def generate_batch_file(self, output_path: Optional[str] = None, python_exe: Opt
pause
'''

with open(output_path, 'w', encoding='utf-8') as f:
with open(output_path, "w", encoding="utf-8") as f:
f.write(content)

print(f"Generated batch file: {output_path}")
Expand Down
Loading