108 lines
3.9 KiB
Python
108 lines
3.9 KiB
Python
# SimpleSyrup - workflow-focused ComfyUI extensions for image generation
|
|
# Copyright (C) 2026 Artificial Sweetener and contributors
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
"""Pin and validate the external P9.4 Anima text-encoder LoRA fixture."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from tools.comfy_api import JsonObject
|
|
|
|
_DIGEST_CHUNK_SIZE = 1024 * 1024
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class TextEncoderLoraFixtureIdentity:
|
|
"""Describe one immutable external LoRA evidence artifact."""
|
|
|
|
lora_name: str
|
|
path: Path
|
|
sha256: str
|
|
size_bytes: int
|
|
tensor_count: int
|
|
source_sha256: str
|
|
transformation: str
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Reject incomplete or ambiguous fixture identities."""
|
|
|
|
if not self.lora_name or Path(self.lora_name).is_absolute():
|
|
raise ValueError("P9.4 fixture LoRA name must be non-empty and relative.")
|
|
if not self.path.is_absolute():
|
|
raise ValueError("P9.4 fixture path must be absolute.")
|
|
for label, digest in (
|
|
("fixture", self.sha256),
|
|
("source", self.source_sha256),
|
|
):
|
|
if len(digest) != 64 or any(
|
|
character not in "0123456789abcdef" for character in digest
|
|
):
|
|
raise ValueError(f"P9.4 {label} SHA-256 must be lowercase hex.")
|
|
if self.size_bytes <= 0 or self.tensor_count <= 0:
|
|
raise ValueError("P9.4 fixture size and tensor count must be positive.")
|
|
if not self.transformation:
|
|
raise ValueError("P9.4 fixture transformation must be recorded.")
|
|
|
|
def as_record(self) -> JsonObject:
|
|
"""Return deterministic fixture provenance for terminal evidence."""
|
|
|
|
return {
|
|
"lora_name": self.lora_name,
|
|
"path": str(self.path),
|
|
"sha256": self.sha256,
|
|
"size_bytes": self.size_bytes,
|
|
"tensor_count": self.tensor_count,
|
|
"source_sha256": self.source_sha256,
|
|
"transformation": self.transformation,
|
|
}
|
|
|
|
|
|
PINNED_TEXT_ENCODER_LORA_FIXTURE = TextEncoderLoraFixtureIdentity(
|
|
lora_name=(
|
|
"SimpleSyrup Evidence\\P9.4\\text_adapter_yoshiyuki_anima_qwen3_06b_te.safetensors"
|
|
),
|
|
path=(
|
|
Path(r"<MODEL_ROOT>\Loras\SimpleSyrup Evidence\P9.4")
|
|
/ "text_adapter_yoshiyuki_anima_qwen3_06b_te.safetensors"
|
|
),
|
|
sha256="a1132f426fd7d29dfa70348ea68d10a1d3e882d859b76c951d664749ed212a79",
|
|
size_bytes=10_202_136,
|
|
tensor_count=588,
|
|
source_sha256=("57040de66329a481a37d64fc23e63b304e1939c48863ab1fe60db14e8e78661d"),
|
|
transformation="retain lora_te1 tensors and rename prefix to lora_te",
|
|
)
|
|
|
|
|
|
def validate_text_encoder_lora_fixture(
|
|
identity: TextEncoderLoraFixtureIdentity,
|
|
) -> TextEncoderLoraFixtureIdentity:
|
|
"""Verify the exact pinned bytes without deserializing model tensors."""
|
|
|
|
if not isinstance(identity, TextEncoderLoraFixtureIdentity):
|
|
raise TypeError("P9.4 fixture preflight requires a fixture identity.")
|
|
if not identity.path.is_file():
|
|
raise FileNotFoundError(
|
|
f"P9.4 text-encoder LoRA fixture is missing: {identity.path}"
|
|
)
|
|
actual_size = identity.path.stat().st_size
|
|
if actual_size != identity.size_bytes:
|
|
raise ValueError(
|
|
"P9.4 text-encoder LoRA fixture size mismatch: "
|
|
f"expected {identity.size_bytes}, got {actual_size}."
|
|
)
|
|
digest = hashlib.sha256()
|
|
with identity.path.open("rb") as stream:
|
|
for chunk in iter(lambda: stream.read(_DIGEST_CHUNK_SIZE), b""):
|
|
digest.update(chunk)
|
|
actual_digest = digest.hexdigest()
|
|
if actual_digest != identity.sha256:
|
|
raise ValueError(
|
|
"P9.4 text-encoder LoRA fixture SHA-256 mismatch: "
|
|
f"expected {identity.sha256}, got {actual_digest}."
|
|
)
|
|
return identity
|