Release v.0.9.4 (#237)

This commit is contained in:
Filip Strand 2025-07-17 11:12:53 +02:00 committed by GitHub
parent aceeafca8e
commit 3c5df450cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 2662 additions and 650 deletions

View File

@ -46,7 +46,7 @@ jobs:
- name: Run release script
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }}
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }}
run: |
python scripts/release.py
python -m mflux.release.release_script

View File

@ -5,15 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.9.4] - 2025-07-17
# MFLUX v.0.9.4 Release Notes
### 🛠️ Dependency Updates
- Expanded MLX dependency range from `mlx>=0.22.0,<=0.26.1` to `mlx>=0.22.0,<0.27.0` to support newer MLX versions
### 🔧 Developer Experience
- Refactor the release script into a reusable Python module for better maintainability
## [0.9.3] - 2025-07-08
# MFLUX v.0.9.3 Release Notes
### 😖 Revert "Offline Resilience" change
On a "cold start" where user has not previously downloaded the requested model, the workflow does not successfully request the download of all the expected files, blocking the image generation workflow for first time users. The feature will be re-evaluated carefully after this hot fix.
## [0.9.2] - 2025-07-08
# MFLUX v0.9.2 Release Notes
# MFLUX v.0.9.2 Release Notes
### 🏗️ Build System Improvements
@ -41,7 +55,7 @@ On a "cold start" where user has not previously downloaded the requested model,
## [0.9.1] - 2025-07-04
# MFLUX v0.9.1 Release Notes
# MFLUX v.0.9.1 Release Notes
### 🛠️ Dependency Fixes

View File

@ -15,7 +15,7 @@ source-exclude = [
[project]
name = "mflux"
version = "0.9.3"
version = "0.9.4"
description = "A MLX port of FLUX based on the Huggingface Diffusers implementation."
readme = "README.md"
keywords = ["diffusers", "flux", "mlx"]
@ -26,7 +26,7 @@ requires-python = ">=3.10"
dependencies = [
"huggingface-hub>=0.24.5,<1.0",
"matplotlib>=3.9.2,<4.0",
"mlx>=0.22.0,<=0.26.1",
"mlx>=0.22.0,<0.27.0",
"numpy>=2.0.1,<3.0",
"opencv-python>=4.10.0,<5.0",
"piexif>=1.1.3,<2.0",
@ -38,13 +38,14 @@ dependencies = [
# use temporary community build of py13 wheel, use until official project build
# uv pip install https://github.com/anthonywu/sentencepiece/releases/download/0.2.1-py13dev/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl
"sentencepiece>=0.2.0,<1.0; python_version<'3.13'",
"tokenizers>=0.20.3; python_version>='3.13'", # transformers -> tokenizers
"tokenizers>=0.20.3; python_version>='3.13'", # transformers -> tokenizers
"toml>=0.10.2,<1.0",
"torch>=2.3.1,<3.0; python_version<'3.13'",
# torch dev builds: pip install --pre --index-url https://download.pytorch.org/whl/nightly
"torch>=2.6.0.dev20241106; python_version>='3.13'",
"tqdm>=4.66.5,<5.0",
"transformers>=4.44.0,<5.0",
"twine>=6.1.0,<7.0",
]
classifiers = [
"Intended Audience :: Developers",

View File

@ -1,192 +0,0 @@
#!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path
from typing import List, Tuple
class ChangelogParser:
def __init__(self, changelog_path: Path = Path("CHANGELOG.md")):
self.changelog_path = changelog_path
if not self.changelog_path.exists():
raise FileNotFoundError(f"Changelog file not found: {self.changelog_path}")
def extract_changelog_entry(self, version: str) -> str:
"""Extract changelog entry for a specific version"""
content = self.changelog_path.read_text(encoding="utf-8")
# Pattern to match version headers: ## [VERSION]
version_pattern = rf"^## \[{re.escape(version)}\].*$"
next_version_pattern = r"^## \[.*\].*$"
lines = content.splitlines()
# Find the start of the target version section
start_idx = None
for i, line in enumerate(lines):
if re.match(version_pattern, line):
start_idx = i + 1 # Start after the version header
break
if start_idx is None:
raise ValueError(f"Version {version} not found in changelog")
# Find the end of the section (next version header)
end_idx = len(lines)
for i in range(start_idx, len(lines)):
if re.match(next_version_pattern, lines[i]):
end_idx = i
break
# Extract the content between version headers
entry_lines = lines[start_idx:end_idx]
# Clean up: remove empty lines at start/end and strip trailing whitespace
while entry_lines and not entry_lines[0].strip():
entry_lines.pop(0)
while entry_lines and not entry_lines[-1].strip():
entry_lines.pop()
entry_content = "\n".join(line.rstrip() for line in entry_lines)
if not entry_content.strip():
raise ValueError(f"No content found for version {version}")
return entry_content
def list_all_versions(self) -> List[Tuple[str, str]]:
"""List all versions in the changelog with their dates"""
content = self.changelog_path.read_text(encoding="utf-8")
lines = content.splitlines()
versions = []
version_pattern = r"^## \[([^\]]+)\](?:\s*-\s*(.+))?.*$"
for line in lines:
match = re.match(version_pattern, line)
if match:
version = match.group(1)
date = match.group(2) if match.group(2) else "No date"
if version.lower() != "unreleased": # Skip "Unreleased" section
versions.append((version, date))
return versions
def validate_changelog_format(self) -> List[str]:
"""Validate changelog format and return list of issues"""
content = self.changelog_path.read_text(encoding="utf-8")
lines = content.splitlines()
issues = []
# Check if file starts with proper header
if not lines or not lines[0].strip().startswith("# Changelog"):
issues.append("Changelog should start with '# Changelog' header")
# Check for proper version format
version_pattern = r"^## \[([^\]]+)\](?:\s*-\s*(.+))?.*$"
found_versions = []
for i, line in enumerate(lines):
if re.match(r"^## \[", line):
match = re.match(version_pattern, line)
if not match:
issues.append(f"Line {i + 1}: Invalid version header format: {line}")
else:
version = match.group(1)
found_versions.append(version)
# Check version format (except for "Unreleased")
if version.lower() != "unreleased":
if not re.match(r"^\d+\.\d+\.\d+", version):
issues.append(f"Line {i + 1}: Version should follow semantic versioning: {version}")
if not found_versions:
issues.append("No version sections found in changelog")
return issues
def get_latest_version(self) -> str:
"""Get the latest version from changelog (excluding Unreleased)"""
versions = self.list_all_versions()
if not versions:
raise ValueError("No versions found in changelog")
return versions[0][0] # First version in list (should be latest)
def main():
parser = argparse.ArgumentParser(description="MFLUX Changelog Parser and Release Notes Generator")
parser.add_argument("--changelog", default="CHANGELOG.md", help="Path to changelog file")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Show changelog entry command
show_parser = subparsers.add_parser("show", help="Show changelog entry for a version")
show_parser.add_argument("version", help="Version to show (e.g., 0.8.0)")
show_parser.add_argument("--markdown", action="store_true", help="Output in markdown format")
# List versions command
subparsers.add_parser("list", help="List all versions in changelog")
# Validate command
subparsers.add_parser("validate", help="Validate changelog format")
# Latest version command
subparsers.add_parser("latest", help="Show latest version")
args = parser.parse_args()
if not args.command:
parser.print_help()
return
try:
changelog_parser = ChangelogParser(Path(args.changelog))
if args.command == "show":
entry = changelog_parser.extract_changelog_entry(args.version)
if args.markdown:
print(f"# Release {args.version}\n")
print(entry)
print(f"\n---\n*Changelog entry: {len(entry)} characters*")
else:
print(f"📝 Changelog entry for version {args.version}:")
print("=" * 60)
print(entry)
print("=" * 60)
print(f"✅ Found changelog entry ({len(entry)} characters)")
elif args.command == "list":
versions = changelog_parser.list_all_versions()
print("📋 Available versions in changelog:")
print("=" * 50)
for version, date in versions:
print(f" {version:<15} - {date}")
print(f"\n✅ Found {len(versions)} versions")
elif args.command == "validate":
issues = changelog_parser.validate_changelog_format()
if issues:
print("❌ Changelog validation failed:")
for issue in issues:
print(f"{issue}")
sys.exit(1)
else:
print("✅ Changelog format is valid")
elif args.command == "latest":
latest = changelog_parser.get_latest_version()
print(f"📦 Latest version: {latest}")
except (ValueError, FileNotFoundError) as e:
print(f"❌ Error: {e}")
sys.exit(1)
except (OSError, ImportError, KeyboardInterrupt) as e:
print(f"❌ Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,414 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import subprocess
import sys
from pathlib import Path
from typing import Optional
import requests
from generate_release_notes import ChangelogParser
class ReleaseManager:
def __init__(self, github_token: str):
self.github_token = github_token
self.github_repo = "filipstrand/mflux"
self.base_url = "https://api.github.com"
self.headers = {
"Authorization": f"token {github_token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "MFLUX-Release-Script",
}
# Package name on (Test)PyPI derive from repo slug for now
self.package_name = self.github_repo.split("/")[1]
@staticmethod
def get_version_from_pyproject(pyproject_path: Path = Path("pyproject.toml")) -> str:
"""Read version from pyproject.toml"""
if not pyproject_path.exists():
raise FileNotFoundError(f"pyproject.toml not found at {pyproject_path}")
import toml
try:
pyproject_data = toml.load(pyproject_path)
version = pyproject_data.get("project", {}).get("version")
if not version:
raise ValueError("Version not found in pyproject.toml under [project] section")
return version
except (toml.TomlDecodeError, KeyError, TypeError) as e:
raise ValueError(f"Failed to read version from pyproject.toml: {e}")
@staticmethod
def validate_release_ready(version: str) -> None:
"""Validate that everything is ready for release"""
# Check if version looks valid
import re
if not re.match(r"^\d+\.\d+\.\d+", version):
raise ValueError(f"Version format appears invalid: {version}")
# Check if changelog has entry for this version and validate version consistency
try:
changelog_parser = ChangelogParser()
changelog_parser.extract_changelog_entry(version) # Just validate entry exists
print(f"✅ Changelog entry found for version {version}")
# Validate that pyproject.toml version matches the latest version in changelog
latest_changelog_version = changelog_parser.get_latest_version()
if version != latest_changelog_version:
raise ValueError(
f"Version mismatch: pyproject.toml has version '{version}' but latest changelog version is '{latest_changelog_version}'. "
f"Please ensure pyproject.toml version matches the latest changelog entry."
)
print(f"✅ Version consistency validated: pyproject.toml ({version}) matches latest changelog entry")
except ValueError as e:
raise ValueError(f"Changelog validation failed: {e}")
# Check that we are releasing from the main branch.
# In local runs we can use `git branch --show-current`, but inside GitHub Actions we are in a
# detached-HEAD state. In that case `GITHUB_REF_NAME` (or `GITHUB_HEAD_REF` for PRs) exposes
# the branch name.
current_branch = os.getenv("GITHUB_REF_NAME") or os.getenv("GITHUB_HEAD_REF")
if not current_branch:
# Fallback to local git query
try:
result = subprocess.run(["git", "branch", "--show-current"], capture_output=True, text=True, check=True)
current_branch = result.stdout.strip()
except subprocess.CalledProcessError:
current_branch = ""
if current_branch != "main":
raise ValueError(
f"Release must be from 'main' branch, currently on '{current_branch or 'UNKNOWN'}'. "
"Please switch to main branch first or ensure the workflow checks out 'main'."
)
print(f"✅ On main branch ({current_branch})")
print(f"✅ Release validation passed for version {version}")
@staticmethod
def extract_version_from_ref(github_ref: str) -> str:
if not github_ref.startswith("refs/tags/v."):
raise ValueError(f"Invalid GitHub ref format: {github_ref}")
return github_ref.replace("refs/tags/v.", "")
def check_release_exists(self, tag_name: str) -> bool:
url = f"{self.base_url}/repos/{self.github_repo}/releases/tags/{tag_name}"
response = requests.get(url, headers=self.headers)
return response.status_code == 200
def check_git_tag_exists(self, tag_name: str) -> bool:
"""Check if a git tag already exists locally or remotely"""
try:
# Check if tag exists locally
result = subprocess.run(["git", "tag", "-l", tag_name], capture_output=True, text=True, check=True)
if result.stdout.strip() == tag_name:
print(f"✅ Git tag {tag_name} exists locally")
return True
# Check if tag exists on remote
result = subprocess.run(
["git", "ls-remote", "--tags", "origin", f"refs/tags/{tag_name}"],
capture_output=True,
text=True,
check=True,
)
if result.stdout.strip():
print(f"✅ Git tag {tag_name} exists on remote")
return True
return False
except subprocess.CalledProcessError as e:
print(f"⚠️ Warning: Could not check git tag existence: {e}")
return False
def create_git_tag(self, tag_name: str, version: str) -> None:
"""Create and push git tag for the release"""
# Create annotated tag
self.run_command(["git", "tag", "-a", tag_name, "-m", f"Release {version}"], f"Creating git tag {tag_name}")
# Push tag to remote
self.run_command(["git", "push", "origin", tag_name], f"Pushing git tag {tag_name} to remote")
def create_github_release(self, tag_name: str, version: str, changelog_entry: str) -> dict:
url = f"{self.base_url}/repos/{self.github_repo}/releases"
data = {
"tag_name": tag_name,
"name": f"Release {version}",
"body": changelog_entry,
"draft": False,
"prerelease": False,
}
response = requests.post(url, json=data, headers=self.headers)
if response.status_code == 201:
print(f"✅ Successfully created GitHub release for {tag_name}")
return response.json()
else:
raise Exception(f"Failed to create GitHub release: {response.status_code} - {response.text}")
@staticmethod
def run_command(cmd: list, description: str, check: bool = True, env: dict = None) -> subprocess.CompletedProcess:
print(f"🔄 {description}...")
print(f" Running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, check=False, env=env)
if result.returncode != 0 and check:
print(f"{description} failed!")
print(f" stdout: {result.stdout}")
print(f" stderr: {result.stderr}")
raise Exception(f"Command failed: {' '.join(cmd)}")
if result.stdout:
print(f" stdout: {result.stdout.strip()}")
print(f"{description} completed successfully")
return result
def build_package(self):
# Clean dist directory to ensure fresh build
import shutil
dist_dir = Path("dist")
if dist_dir.exists():
shutil.rmtree(dist_dir)
print("🧹 Cleaned dist/ directory")
# Build the package
self.run_command([sys.executable, "-m", "uv", "--version"], "Verify 'uv build' version")
self.run_command([sys.executable, "-m", "uv", "build"], "Building package with 'uv build'")
def verify_package(self):
"""Run *twine check* to verify that README / long_description renders on PyPI."""
self.run_command([sys.executable, "-m", "twine", "check", "dist/*"], "Verifying distribution with Twine")
def publish_to_test_pypi(self, test_pypi_token: Optional[str], version: str):
if not test_pypi_token:
print("⚠️ Test PyPI token not provided, skipping Test PyPI upload")
return
if self.version_exists_on_pypi(version, test_pypi=True):
print(f"⚠️ Version {version} already exists on Test PyPI, skipping upload")
return
env = os.environ.copy()
env["TWINE_USERNAME"] = "__token__"
env["TWINE_PASSWORD"] = test_pypi_token
self.run_command(
[sys.executable, "-m", "twine", "upload", "--repository", "testpypi", "dist/*", "--verbose"],
"Publishing to Test PyPI",
env=env,
)
def publish_to_pypi(self, pypi_token: str, version: str):
if self.version_exists_on_pypi(version, test_pypi=False):
print(f"⚠️ Version {version} already exists on PyPI, skipping upload")
return
env = os.environ.copy()
env["TWINE_USERNAME"] = "__token__"
env["TWINE_PASSWORD"] = pypi_token
try:
self.run_command(
[sys.executable, "-m", "twine", "upload", "dist/*", "--verbose"],
"Publishing to PyPI",
env=env,
)
except Exception as e:
if "already been used" in str(e) or "filename has already been used" in str(e):
print(f"⚠️ Version {version} already exists on PyPI (detected during upload)")
print(" This can happen due to race conditions or previous partial uploads")
print(" The release process will continue, but PyPI publishing was skipped")
return
else:
raise
def version_exists_on_pypi(self, version: str, test_pypi: bool = False) -> bool:
"""Return True if *version* of this package is already published on (Test)PyPI."""
repo_url = "https://test.pypi.org/pypi" if test_pypi else "https://pypi.org/pypi"
url = f"{repo_url}/{self.package_name}/json"
try:
response = requests.get(url, timeout=10)
if response.status_code != 200:
return False
data = response.json()
return version in data.get("releases", {})
except requests.RequestException:
return False
def release(self, test_pypi_token: Optional[str], pypi_token: str):
print("🚀 Starting MFLUX release process...")
# Read version from pyproject.toml
version = self.get_version_from_pyproject()
tag_name = f"v.{version}"
print(f"📦 Releasing version: {version} (tag: {tag_name}) [from pyproject.toml]")
# Validate everything is ready for release
print("🔍 Validating release readiness...")
self.validate_release_ready(version)
# Check for uncommitted changes
print("🔍 Checking for uncommitted changes...")
try:
result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, check=True)
if result.stdout.strip():
print("⚠️ Uncommitted changes detected:")
print(result.stdout)
raise ValueError("Cannot release with uncommitted changes. Please commit or stash your changes first.")
print("✅ No uncommitted changes found")
except subprocess.CalledProcessError:
print("⚠️ Warning: Could not check git status")
# Check if release exists
print("🔍 Checking if release already exists...")
if self.check_release_exists(tag_name):
print(f"⚠️ Release {tag_name} already exists, skipping GitHub release creation")
skip_github_release = True
else:
skip_github_release = False
# Check if git tag exists
print("🔍 Checking if git tag already exists...")
git_tag_exists = self.check_git_tag_exists(tag_name)
# Determine if this is a completely new release or if we're re-running
if skip_github_release and git_tag_exists:
print(f"✅ Release {tag_name} already exists completely (both git tag and GitHub release)")
print("🔄 This appears to be a re-run of an existing release - nothing to do!")
print(" If you want to republish to PyPI, delete the GitHub release first.")
return
elif skip_github_release or git_tag_exists:
print("⚠️ Partial release state detected:")
print(f" Git tag exists: {git_tag_exists}")
print(f" GitHub release exists: {skip_github_release}")
print(" Will complete the missing parts...")
# Extract changelog
print("📝 Extracting changelog entry...")
changelog_parser = ChangelogParser()
changelog_entry = changelog_parser.extract_changelog_entry(version)
print(f" Found changelog entry ({len(changelog_entry)} characters)")
# Create git tag if it doesn't exist
if not git_tag_exists:
print("🏷️ Creating git tag...")
self.create_git_tag(tag_name, version)
else:
print(f"✅ Git tag {tag_name} already exists, skipping creation")
# Create GitHub release if it doesn't exist
if not skip_github_release:
print("🐙 Creating GitHub release...")
self.create_github_release(tag_name, version, changelog_entry)
else:
print(f"✅ GitHub release {tag_name} already exists, skipping creation")
# Build & verify package
self.build_package()
self.verify_package()
# Check if version already exists on PyPI before attempting to publish
if self.version_exists_on_pypi(version, test_pypi=False):
print(f"⚠️ Version {version} already exists on PyPI, skipping all PyPI publishing")
return
# Only publish to PyPI if this is a new release (both git tag and GitHub release were created)
if not skip_github_release and not git_tag_exists:
print("📦 Publishing to PyPI (new release)...")
# Publish to Test PyPI
self.publish_to_test_pypi(test_pypi_token, version)
# Publish to PyPI
self.publish_to_pypi(pypi_token, version)
else:
print("⚠️ Skipping PyPI publishing since this appears to be a re-run")
print(" (Either git tag or GitHub release already existed)")
print(" If you need to republish to PyPI, delete both the git tag and GitHub release first.")
print(f"🎉 Release process completed successfully for version {version}!")
def main():
parser = argparse.ArgumentParser(description="MFLUX Release Management")
parser.add_argument("--github-token", help="GitHub token")
parser.add_argument("--test-pypi-token", help="Test PyPI API token (optional)")
parser.add_argument("--pypi-token", help="PyPI API token")
parser.add_argument("--dry-run", action="store_true", help="Print what would be done without executing")
parser.add_argument("--show-changelog", help="Show changelog entry for a specific version (e.g., 0.8.0) and exit")
parser.add_argument("--markdown", action="store_true", help="Output changelog in markdown format (use with --show-changelog)") # fmt: off
args = parser.parse_args()
# Handle --show-changelog option
if args.show_changelog:
try:
changelog_parser = ChangelogParser()
changelog_entry = changelog_parser.extract_changelog_entry(args.show_changelog)
if args.markdown:
# Output as Markdown
print(f"# Release {args.show_changelog}\n")
print(changelog_entry)
print(f"\n---\n*Changelog entry: {len(changelog_entry)} characters*")
else:
# Output with separators (original format)
print(f"📝 Changelog entry for version {args.show_changelog}:")
print("=" * 60)
print(changelog_entry)
print("=" * 60)
print(f"✅ Found changelog entry ({len(changelog_entry)} characters)")
except (ValueError, FileNotFoundError) as e:
print(f"❌ Failed to extract changelog: {e}")
sys.exit(1)
return
# Get values from args or environment variables
github_token = args.github_token or os.getenv("GITHUB_TOKEN")
test_pypi_token = args.test_pypi_token or os.getenv("TEST_PYPI_API_TOKEN")
pypi_token = args.pypi_token or os.getenv("PYPI_API_TOKEN")
# Validate required parameters
if not github_token:
print("❌ GitHub token is required (--github-token or GITHUB_TOKEN env var)")
sys.exit(1)
if not pypi_token:
print("❌ PyPI token is required (--pypi-token or PYPI_API_TOKEN env var)")
sys.exit(1)
if args.dry_run:
try:
version = ReleaseManager.get_version_from_pyproject()
print("🔍 DRY RUN MODE - would execute release process with:")
print(f" Version: {version} [from pyproject.toml]")
print(f" Has Test PyPI token: {bool(test_pypi_token)}")
print(f" Has PyPI token: {bool(pypi_token)}")
return
except (ValueError, FileNotFoundError) as e:
print(f"❌ Could not read version for dry run: {e}")
sys.exit(1)
try:
release_manager = ReleaseManager(github_token)
release_manager.release(test_pypi_token, pypi_token)
except (ValueError, FileNotFoundError, requests.RequestException) as e:
print(f"❌ Release failed: {e}")
sys.exit(1)
except Exception as e: # noqa: BLE001
print(f"❌ Release failed with unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -11,7 +11,7 @@ from mflux.dreambooth.state.training_spec import SingleTransformerBlocks, Traini
from mflux.dreambooth.state.zip_util import ZipUtil
from mflux.models.transformer.joint_transformer_block import JointTransformerBlock
from mflux.models.transformer.single_transformer_block import SingleTransformerBlock
from mflux.post_processing.generated_image import GeneratedImage
from mflux.utils.version_util import VersionUtil
from mflux.weights.weight_handler import MetaData, WeightHandler
if TYPE_CHECKING:
@ -58,7 +58,7 @@ class LoRALayers:
lora_layers = {**transformer_lora_layers, **single_transformer_lora_layers}
weights = WeightHandler(
meta_data=MetaData(mflux_version=GeneratedImage.get_version()),
meta_data=MetaData(mflux_version=VersionUtil.get_mflux_version()),
transformer=mlx.utils.tree_unflatten(list(lora_layers.items()))["transformer"],
)
@ -272,7 +272,7 @@ class LoRALayers:
str(path),
dict(tree_flatten(weights)),
metadata={
"mflux_version": GeneratedImage.get_version(),
"mflux_version": VersionUtil.get_mflux_version(),
"transformer_blocks": str(training_spec.lora_layers.transformer_blocks),
"single_transformer_blocks": str(training_spec.lora_layers.single_transformer_blocks),
},

View File

@ -24,3 +24,16 @@ class StopImageGenerationException(MFluxUserException):
class StopTrainingException(MFluxUserException):
"""user has requested to stop the training process in progress."""
class CommandExecutionError(MFluxException):
"""Raised when a subprocess command invoked by mflux tooling fails."""
def __init__(self, cmd: list[str], return_code: int, stdout: str | None, stderr: str | None):
self.cmd = cmd
self.return_code = return_code
self.stdout = stdout or ""
self.stderr = stderr or ""
super().__init__(
f"Command '{' '.join(cmd)}' exited with code {return_code}. See .stdout / .stderr for details."
)

View File

@ -1,12 +1,11 @@
import importlib
from pathlib import Path
import mlx.core as mx
import PIL.Image
import toml
from mflux.community.concept_attention.attention_data import ConceptHeatmap
from mflux.config.model_config import ModelConfig
from mflux.utils.version_util import VersionUtil
class GeneratedImage:
@ -107,7 +106,10 @@ class GeneratedImage:
self.save_concept_heatmap(path=heatmap_path, export_json_metadata=export_json_metadata, overwrite=overwrite)
def save_concept_heatmap(
self, path: str | Path, export_json_metadata: bool = False, overwrite: bool = False
self,
path: str | Path,
export_json_metadata: bool = False,
overwrite: bool = False,
) -> None:
if self.concept_heatmap:
from mflux import ImageUtil
@ -129,7 +131,7 @@ class GeneratedImage:
def _get_metadata(self) -> dict:
return {
"mflux_version": GeneratedImage.get_version(),
"mflux_version": VersionUtil.get_mflux_version(),
"model": self.model_config.model_name,
"base_model": str(self.model_config.base_model),
"seed": self.seed,
@ -150,29 +152,3 @@ class GeneratedImage:
"redux_image_strengths": self._format_redux_strengths(),
"prompt": self.prompt,
}
@staticmethod
def get_version() -> str:
version = GeneratedImage._get_version_from_toml()
if version:
return version
# Fallback to an installed package version
try:
return str(importlib.metadata.version("mflux"))
except importlib.metadata.PackageNotFoundError:
return "unknown"
@staticmethod
def _get_version_from_toml() -> str | None:
# Search for pyproject.toml by traversing up from the current working directory
current_dir = Path(__file__).resolve().parent
for parent in current_dir.parents:
pyproject_path = parent / "pyproject.toml"
if pyproject_path.exists():
try:
pyproject_data = toml.load(pyproject_path)
return pyproject_data.get("project", {}).get("version")
except (toml.TomlDecodeError, KeyError, TypeError):
return None
return None

View File

View File

@ -0,0 +1,116 @@
import re
from pathlib import Path
from typing import List, Tuple
class ChangelogParser:
@staticmethod
def extract_release_notes_from_changelog(version: str, changelog_path: Path = Path("CHANGELOG.md")) -> str:
print("📝 Extracting changelog entry...")
if not changelog_path.exists():
raise FileNotFoundError(f"Changelog file not found: {changelog_path}")
content = changelog_path.read_text(encoding="utf-8")
# Pattern to match version headers: ## [VERSION]
version_pattern = rf"^## \[{re.escape(version)}\].*$"
next_version_pattern = r"^## \[.*\].*$"
lines = content.splitlines()
# Find the start of the target version section
start_idx = None
for i, line in enumerate(lines):
if re.match(version_pattern, line):
start_idx = i + 1 # Start after the version header
break
if start_idx is None:
raise ValueError(f"Version {version} not found in changelog")
# Find the end of the section (next version header)
end_idx = len(lines)
for i in range(start_idx, len(lines)):
if re.match(next_version_pattern, lines[i]):
end_idx = i
break
# Extract the content between version headers
entry_lines = lines[start_idx:end_idx]
# Clean up: remove empty lines at start/end and strip trailing whitespace
while entry_lines and not entry_lines[0].strip():
entry_lines.pop(0)
while entry_lines and not entry_lines[-1].strip():
entry_lines.pop()
entry_content = "\n".join(line.rstrip() for line in entry_lines)
if not entry_content.strip():
raise ValueError(f"No content found for version {version}")
print(f" Found changelog entry ({len(entry_content)} characters)")
return entry_content
@staticmethod
def validate_changelog_format(changelog_path: Path = Path("CHANGELOG.md")) -> List[str]:
if not changelog_path.exists():
raise FileNotFoundError(f"Changelog file not found: {changelog_path}")
content = changelog_path.read_text(encoding="utf-8")
lines = content.splitlines()
issues = []
# Check if file starts with proper header
if not lines or not lines[0].strip().startswith("# Changelog"):
issues.append("Changelog should start with '# Changelog' header")
# Check for proper version format
version_pattern = r"^## \[([^\]]+)\](?:\s*-\s*(.+))?.*$"
found_versions = []
for i, line in enumerate(lines):
if re.match(r"^## \[", line):
match = re.match(version_pattern, line)
if not match:
issues.append(f"Line {i + 1}: Invalid version header format: {line}")
else:
version = match.group(1)
found_versions.append(version)
# Check version format (except for "Unreleased")
if version.lower() != "unreleased":
if not re.match(r"^\d+\.\d+\.\d+", version):
issues.append(f"Line {i + 1}: Version should follow semantic versioning: {version}")
if not found_versions:
issues.append("No version sections found in changelog")
return issues
@staticmethod
def get_latest_version(changelog_path: Path = Path("CHANGELOG.md")) -> str:
versions = ChangelogParser._list_all_versions(changelog_path)
if not versions:
raise ValueError("No versions found in changelog")
return versions[0][0] # First version in list (should be latest)
@staticmethod
def _list_all_versions(changelog_path: Path = Path("CHANGELOG.md")) -> List[Tuple[str, str]]:
if not changelog_path.exists():
raise FileNotFoundError(f"Changelog file not found: {changelog_path}")
content = changelog_path.read_text(encoding="utf-8")
lines = content.splitlines()
versions = []
version_pattern = r"^## \[([^\]]+)\](?:\s*-\s*(.+))?.*$"
for line in lines:
match = re.match(version_pattern, line)
if match:
version = match.group(1)
date = match.group(2) if match.group(2) else "No date"
if version.lower() != "unreleased": # Skip "Unreleased" section
versions.append((version, date))
return versions

View File

@ -0,0 +1,59 @@
import subprocess
from mflux.error.exceptions import CommandExecutionError
class GitOperations:
@staticmethod
def run_command(cmd: list, description: str, check: bool = True) -> subprocess.CompletedProcess:
print(f"🔄 {description}...")
print(f" Running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
if result.returncode != 0 and check:
print(f"{description} failed!")
print(f" stdout: {result.stdout}")
print(f" stderr: {result.stderr}")
raise CommandExecutionError(cmd, result.returncode, result.stdout, result.stderr)
if result.stdout:
print(f" stdout: {result.stdout.strip()}")
print(f"{description} completed successfully")
return result
@staticmethod
def check_tag_exists(tag_name: str) -> bool:
print("🔍 Checking git tag existence...")
try:
# Check if tag exists locally
result = subprocess.run(["git", "tag", "-l", tag_name], capture_output=True, text=True, check=True)
if result.stdout.strip() == tag_name:
print(f"✅ Git tag {tag_name} exists locally")
return True
# Check if tag exists on remote
result = subprocess.run(
["git", "ls-remote", "--tags", "origin", f"refs/tags/{tag_name}"],
capture_output=True,
text=True,
check=True,
)
if result.stdout.strip():
print(f"✅ Git tag {tag_name} exists on remote")
return True
print(f" Git tag {tag_name} does not exist")
return False
except subprocess.CalledProcessError as e:
print(f"⚠️ Warning: Could not check git tag existence: {e}")
return False
@staticmethod
def create_and_push_tag(tag_name: str, version: str) -> None:
print("🏷️ Creating git tag...")
GitOperations.run_command(
["git", "tag", "-a", tag_name, "-m", f"Release {version}"], f"Creating git tag {tag_name}"
)
GitOperations.run_command(["git", "push", "origin", tag_name], f"Pushing git tag {tag_name} to remote")

View File

@ -0,0 +1,66 @@
import requests
class GitHubAPI:
@staticmethod
def check_github_release_exists(
github_token: str,
github_repo: str,
tag_name: str,
) -> bool:
print("🔍 Checking GitHub release existence...")
headers = {
"Authorization": f"token {github_token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "MFLUX-Release-Script",
}
url = f"https://api.github.com/repos/{github_repo}/releases/tags/{tag_name}"
response = requests.get(url, headers=headers)
if response.status_code == 200:
print(f"✅ GitHub release {tag_name} exists")
return True
if response.status_code == 404:
print(f" GitHub release {tag_name} does not exist")
return False
# Any other status code is unexpected and likely indicates auth/rate-limit issues.
error_msg = f"GitHub API returned {response.status_code} while checking for release {tag_name}: {response.text}"
raise requests.HTTPError(error_msg, response=response)
@staticmethod
def create_github_release(
github_token: str,
github_repo: str,
tag_name: str,
version: str,
release_notes: str,
) -> dict:
print("🐙 Creating GitHub release...")
headers = {
"Authorization": f"token {github_token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "MFLUX-Release-Script",
}
url = f"https://api.github.com/repos/{github_repo}/releases"
data = {
"tag_name": tag_name,
"name": f"Release {version}",
"body": release_notes,
"draft": False,
"prerelease": False,
}
response = requests.post(url, json=data, headers=headers)
if response.status_code == 201:
print(f"✅ Successfully created GitHub release for {tag_name}")
return response.json()
else:
raise Exception(f"Failed to create GitHub release: {response.status_code} - {response.text}")

View File

@ -0,0 +1,176 @@
import shutil
import sys
import time
from pathlib import Path
from typing import Optional
import requests
from twine.commands import upload
from twine.exceptions import TwineException
from twine.settings import Settings
from .git_operations import GitOperations
class PyPIPublisher:
@staticmethod
def build_and_verify_package() -> None:
print("📦 Building and verifying package...")
# Clean dist directory
dist_dir = Path("dist")
if dist_dir.exists():
shutil.rmtree(dist_dir)
print("🧹 Cleaned dist/ directory")
# Build the package
GitOperations.run_command([sys.executable, "-m", "uv", "--version"], "Verify 'uv build' version")
GitOperations.run_command([sys.executable, "-m", "uv", "build"], "Building package with 'uv build'")
# Verify the package
print("🔍 Verifying package...")
GitOperations.run_command(
[sys.executable, "-m", "twine", "check", "dist/*"], "Verifying distribution with Twine"
)
@staticmethod
def version_exists_on_pypi(package_name: str, version: str, test_pypi: bool = False) -> bool:
print("🔍 Checking if version already exists on PyPI...")
repo_url = "https://test.pypi.org/pypi" if test_pypi else "https://pypi.org/pypi"
url = f"{repo_url}/{package_name}/json"
for attempt in range(3):
try:
print(f" Checking PyPI API (attempt {attempt + 1}/3): {url}")
response = requests.get(url, timeout=30)
if response.status_code == 200:
data = response.json()
releases = data.get("releases", {})
version_exists = version in releases
print(
f" PyPI API response: {len(releases)} total versions, version {version} exists: {version_exists}"
)
if version_exists:
print(f"⚠️ Version {version} already exists on PyPI")
return version_exists
elif response.status_code == 404:
print(" Package not found on PyPI (404) - this is normal for new packages")
return False
else:
print(f" PyPI API returned status {response.status_code}: {response.text[:200]}")
if attempt < 2:
time.sleep(2**attempt)
continue
else:
raise ValueError(f"PyPI API returned unexpected status {response.status_code}")
except requests.RequestException as e:
print(f" Network error checking PyPI (attempt {attempt + 1}/3): {e}")
if attempt < 2:
time.sleep(2**attempt)
continue
else:
raise requests.RequestException(f"Failed to check PyPI after 3 attempts: {e}") from e
raise RuntimeError("Unexpected error in version_exists_on_pypi")
@staticmethod
def publish_to_test_pypi(test_pypi_token: Optional[str], package_name: str, version: str) -> None:
if not test_pypi_token:
print("⚠️ Test PyPI token not provided, skipping Test PyPI upload")
return
PyPIPublisher._upload_to_pypi(
token=test_pypi_token,
repository="testpypi",
display_name="Test PyPI",
package_name=package_name,
version=version,
optional=True,
)
@staticmethod
def publish_to_pypi(pypi_token: str, package_name: str, version: str) -> None:
PyPIPublisher._upload_to_pypi(
token=pypi_token,
repository="pypi",
display_name="PyPI",
package_name=package_name,
version=version,
optional=False,
)
@staticmethod
def _upload_to_pypi(
token: str,
repository: str,
display_name: str,
package_name: str,
version: str,
optional: bool = False,
) -> None:
print(f"📦 Publishing to {display_name}...")
try:
print(f"🔄 Using programmatic twine upload for {display_name}...")
settings = Settings(
username="__token__",
password=token,
repository=repository,
verbose=True,
skip_existing=True,
disable_progress_bar=False,
comment="Automated release via mflux-release-script",
)
dist_files = list(Path("dist").glob("*"))
if not dist_files:
raise ValueError("No distribution files found in dist/")
print(f"📦 Uploading {len(dist_files)} files to {display_name}...")
for file_path in dist_files:
print(f"{file_path.name}")
# Retry transient failures (e.g. 5xx replies / connection resets)
for attempt in range(3):
try:
upload.upload(settings, [str(f) for f in dist_files])
print(f"✅ Programmatic {display_name} upload completed successfully (attempt {attempt + 1})")
break # success → leave retry loop
except TwineException as te:
transient = any(x in str(te).lower() for x in ["500", "502", "503", "504", "timeout"])
if transient and attempt < 2:
wait = 2**attempt
print(
f"⚠️ Transient {display_name} upload error (attempt {attempt + 1}/3): {te}. Retrying in {wait}s"
)
time.sleep(wait)
continue
raise # re-raise to outer handler for consistent processing
except TwineException as e:
error_msg = str(e).lower()
if "already exists" in error_msg:
print(f"⚠️ Version {version} already exists on {display_name}")
return
else:
# Handle all other TwineException errors consistently
print(f"⚠️ {display_name} upload failed: {e}")
if "authentication" in error_msg or "403" in error_msg:
print(f" This appears to be an authentication issue - check your {display_name} token.")
elif "400" in error_msg or "bad request" in error_msg:
print(" This might be a partial upload where some files (like wheels) succeeded.")
print(" If only wheel uploaded, this is a known issue with source distribution validation.")
else:
print(" This might be a partial upload where some files succeeded.")
print(f" Check {display_name} manually to verify which files were uploaded.")
print(f" {display_name} failures are non-critical, continuing with release process")
return
except (OSError, ValueError, RuntimeError) as e:
print(f"⚠️ Unexpected error during {display_name} upload: {e}")
print(" This might be a partial upload where some files succeeded.")
print(f" Check {display_name} manually to verify which files were uploaded.")
print(f" {display_name} failures are non-critical, continuing with release process")
return

View File

@ -0,0 +1,96 @@
from typing import Optional
import requests
from mflux.release.changelog_parser import ChangelogParser
from mflux.release.git_operations import GitOperations
from mflux.release.github_api import GitHubAPI
from mflux.release.pypi_publisher import PyPIPublisher
from mflux.release.release_validator import ReleaseValidator
from mflux.utils.version_util import VersionUtil
class ReleaseManager:
@staticmethod
def create_release(
github_token: str,
pypi_token: str,
test_pypi_token: Optional[str] = None,
github_repo: str = "filipstrand/mflux",
package_name: str = "mflux",
) -> None:
# 0. Load version from pyproject.toml
version = VersionUtil.get_mflux_version()
tag_name = f"v.{version}"
print("🚀 Starting MFLUX release process...")
print(f"📦 Releasing version: {version} (tag: {tag_name}) [from pyproject.toml]")
# 1. Validate everything is ready for release
ReleaseValidator.validate_release_ready(version)
# 2. Check current release state
git_tag_exists = GitOperations.check_tag_exists(tag_name)
github_release_exists = GitHubAPI.check_github_release_exists(github_token, github_repo, tag_name)
# 3. Print release status and exit early if already complete
ReleaseManager._print_release_status(tag_name, git_tag_exists, github_release_exists)
if ReleaseManager._is_release_complete(git_tag_exists, github_release_exists):
return
# 4. Create git tag if needed
if not git_tag_exists:
GitOperations.create_and_push_tag(tag_name, version)
# 5. Create GitHub release if needed
if not github_release_exists:
release_notes = ChangelogParser.extract_release_notes_from_changelog(version)
GitHubAPI.create_github_release(github_token, github_repo, tag_name, version, release_notes)
# 6. Handle PyPI publishing
if ReleaseManager._should_publish_to_pypi(git_tag_exists, github_release_exists, package_name, version):
PyPIPublisher.build_and_verify_package()
PyPIPublisher.publish_to_test_pypi(test_pypi_token, package_name, version)
PyPIPublisher.publish_to_pypi(pypi_token, package_name, version)
print(f"🎉 Release process completed successfully for version {version}!")
@staticmethod
def _is_release_complete(git_tag_exists: bool, github_release_exists: bool) -> bool:
return git_tag_exists and github_release_exists
@staticmethod
def _is_release_partial(git_tag_exists: bool, github_release_exists: bool) -> bool:
return git_tag_exists or github_release_exists
@staticmethod
def _should_publish_to_pypi(
git_tag_exists: bool,
github_release_exists: bool,
package_name: str,
version: str,
) -> bool:
# Only publish if this is a completely new release
if ReleaseManager._is_release_partial(git_tag_exists, github_release_exists):
print("⚠️ Skipping PyPI publishing since this appears to be a re-run")
return False
# Check if version already exists on PyPI
try:
if PyPIPublisher.version_exists_on_pypi(package_name, version, test_pypi=False):
return False
except (requests.RequestException, ValueError, OSError) as e:
print(f"❌ Failed to check PyPI version: {e}")
raise ValueError(f"PyPI version check failed: {e}") from e
return True
@staticmethod
def _print_release_status(tag_name: str, git_tag_exists: bool, github_release_exists: bool) -> None:
if ReleaseManager._is_release_complete(git_tag_exists, github_release_exists):
print(f"✅ Release {tag_name} already exists completely")
print("🔄 This appears to be a re-run of an existing release - nothing to do!")
elif ReleaseManager._is_release_partial(git_tag_exists, github_release_exists):
print("⚠️ Partial release state detected:")
print(f" Git tag exists: {git_tag_exists}")
print(f" GitHub release exists: {github_release_exists}")
print(" Will complete the missing parts...")

View File

@ -0,0 +1,32 @@
import argparse
import os
import sys
import requests
from mflux.release.release_manager import ReleaseManager
def main():
parser = argparse.ArgumentParser(description="MFLUX Release Management")
parser.add_argument("--github-token", help="GitHub token")
parser.add_argument("--pypi-token", help="PyPI API token")
parser.add_argument("--test-pypi-token", help="Test PyPI API token (optional)")
args = parser.parse_args()
try:
ReleaseManager.create_release(
github_token=args.github_token or os.getenv("GITHUB_TOKEN"),
pypi_token=args.pypi_token or os.getenv("PYPI_API_TOKEN"),
test_pypi_token=args.test_pypi_token or os.getenv("TEST_PYPI_API_TOKEN"),
)
except (ValueError, FileNotFoundError, requests.RequestException) as e:
print(f"❌ Release failed: {e}")
sys.exit(1)
except Exception as e: # noqa: BLE001
print(f"❌ Release failed with unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,87 @@
import os
import re
import subprocess
from .changelog_parser import ChangelogParser
class ReleaseValidator:
@staticmethod
def validate_release_ready(version: str) -> None:
print("🔍 Validating release readiness...")
ReleaseValidator._validate_version_format(version)
ReleaseValidator._validate_changelog_format()
ReleaseValidator._validate_changelog_entry(version)
ReleaseValidator._validate_branch()
ReleaseValidator._validate_uncommitted_changes()
print(f"✅ Release validation passed for version {version}")
@staticmethod
def _validate_version_format(version: str) -> None:
if not re.match(r"^\d+\.\d+\.\d+", version):
raise ValueError(f"Version format appears invalid: {version}")
@staticmethod
def _validate_changelog_entry(version: str) -> None:
try:
ChangelogParser.extract_release_notes_from_changelog(version)
print(f"✅ Changelog entry found for version {version}")
# Validate version consistency
latest_changelog_version = ChangelogParser.get_latest_version()
if version != latest_changelog_version:
raise ValueError(
f"Version mismatch: pyproject.toml has version '{version}' but "
f"latest changelog version is '{latest_changelog_version}'. "
f"Please ensure pyproject.toml version matches the latest changelog entry."
)
print(f"✅ Version consistency validated: pyproject.toml ({version}) matches latest changelog entry")
except ValueError as e:
raise ValueError(f"Changelog validation failed: {e}")
@staticmethod
def _validate_branch() -> None:
current_branch = os.getenv("GITHUB_REF_NAME") or os.getenv("GITHUB_HEAD_REF")
if not current_branch:
try:
result = subprocess.run(["git", "branch", "--show-current"], capture_output=True, text=True, check=True)
current_branch = result.stdout.strip()
except subprocess.CalledProcessError:
current_branch = ""
if current_branch != "main":
raise ValueError(
f"Release must be from 'main' branch, currently on '{current_branch or 'UNKNOWN'}'. "
"Please switch to main branch first or ensure the workflow checks out 'main'."
)
print(f"✅ On main branch ({current_branch})")
@staticmethod
def _validate_changelog_format() -> None:
try:
issues = ChangelogParser.validate_changelog_format()
if issues:
print("❌ Changelog format validation failed:")
for issue in issues:
print(f"{issue}")
raise ValueError(f"Changelog format validation failed with {len(issues)} issues")
print("✅ Changelog format validation passed")
except ValueError as e:
if "Changelog format validation failed" in str(e):
raise # Re-raise our own validation error
else:
raise ValueError(f"Changelog format validation failed: {e}")
@staticmethod
def _validate_uncommitted_changes() -> None:
try:
result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, check=True)
if result.stdout.strip():
print("⚠️ Uncommitted changes detected:")
print(result.stdout)
raise ValueError("Cannot release with uncommitted changes. Please commit or stash your changes first.")
print("✅ No uncommitted changes found")
except subprocess.CalledProcessError:
print("⚠️ Warning: Could not check git status")

View File

View File

@ -0,0 +1,32 @@
from __future__ import annotations
import importlib.metadata
from pathlib import Path
import toml
class VersionUtil:
@staticmethod
def get_mflux_version() -> str:
return VersionUtil._scan_pyproject() or VersionUtil._get_installed_version() or "unknown"
@staticmethod
def _scan_pyproject() -> str | None:
current_dir = Path(__file__).resolve().parent
for parent in current_dir.parents:
pyproject_path = parent / "pyproject.toml"
if pyproject_path.exists():
try:
data = toml.load(pyproject_path)
return data.get("project", {}).get("version")
except Exception: # noqa: BLE001
return None
return None
@staticmethod
def _get_installed_version() -> str | None:
try:
return importlib.metadata.version("mflux")
except importlib.metadata.PackageNotFoundError:
return None

View File

@ -5,7 +5,7 @@ from mlx import nn
from mlx.utils import tree_flatten
from transformers import CLIPTokenizer, T5Tokenizer
from mflux.post_processing.generated_image import GeneratedImage
from mflux.utils.version_util import VersionUtil
class ModelSaver:
@ -38,7 +38,7 @@ class ModelSaver:
weight,
{
"quantization_level": str(bits),
"mflux_version": GeneratedImage.get_version(),
"mflux_version": VersionUtil.get_mflux_version(),
},
)

View File

@ -5,7 +5,7 @@ from pathlib import Path
import numpy as np
from mflux import Config, Flux1, ModelConfig
from mflux.post_processing.generated_image import GeneratedImage
from mflux.utils.version_util import VersionUtil
from mflux.weights.weight_handler import WeightHandler
PATH = "tests/4bit/"
@ -36,7 +36,7 @@ class TestModelSaving:
# Verify that the mflux version is correctly saved in the model's metadata
_, quantization_level, mflux_version = WeightHandler._load_vae(root_path=Path(PATH))
assert mflux_version == GeneratedImage.get_version(), "mflux version not correctly saved in metadata" # fmt: off
assert mflux_version == VersionUtil.get_mflux_version(), "mflux version not correctly saved in metadata" # fmt: off
assert quantization_level == "4", "quantization level not correctly saved in metadata" # fmt: off
# when loading the quantized model (also without specifying bits)

1950
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff