comfy-diffusion 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ """Public package entrypoint for comfy_diffusion."""
2
+
3
+ from ._runtime import ensure_comfyui_on_path
4
+ from .lora import apply_lora
5
+ from .runtime import check_runtime
6
+ from .vae import (
7
+ vae_decode,
8
+ vae_decode_batch,
9
+ vae_decode_batch_tiled,
10
+ vae_decode_tiled,
11
+ vae_encode,
12
+ vae_encode_batch,
13
+ vae_encode_batch_tiled,
14
+ vae_encode_tiled,
15
+ )
16
+
17
+ ensure_comfyui_on_path()
18
+
19
+ __all__ = [
20
+ "check_runtime",
21
+ "vae_decode",
22
+ "vae_decode_batch",
23
+ "vae_decode_batch_tiled",
24
+ "vae_decode_tiled",
25
+ "vae_encode",
26
+ "vae_encode_batch",
27
+ "vae_encode_batch_tiled",
28
+ "vae_encode_tiled",
29
+ "apply_lora",
30
+ ]
@@ -0,0 +1,26 @@
1
+ """Internal runtime bootstrap for comfy_diffusion.
2
+
3
+ Path insertion is intentionally lightweight and import-safe: this module must not
4
+ import torch or comfy internals just to make ComfyUI discoverable.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+
13
+ def _comfyui_root() -> Path:
14
+ """Return the absolute path to the vendored ComfyUI directory."""
15
+ return Path(__file__).resolve().parents[1] / "vendor" / "ComfyUI"
16
+
17
+
18
+ def ensure_comfyui_on_path() -> Path:
19
+ """Ensure vendored ComfyUI is importable and return the inserted path."""
20
+ comfyui_root = _comfyui_root()
21
+ comfyui_root_str = str(comfyui_root)
22
+
23
+ if comfyui_root_str not in sys.path:
24
+ sys.path.insert(0, comfyui_root_str)
25
+
26
+ return comfyui_root
@@ -0,0 +1,168 @@
1
+ """Audio helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol, cast
6
+
7
+
8
+ class _LtxvAudioVaeEncoder(Protocol):
9
+ sample_rate: int
10
+
11
+ def encode(self, audio: Any) -> Any: ...
12
+
13
+
14
+ class _LtxvAudioVaeDecoder(Protocol):
15
+ output_sample_rate: int
16
+
17
+ def decode(self, latent: Any) -> Any: ...
18
+
19
+
20
+ class _LtxvAudioVae(Protocol):
21
+ sample_rate: int
22
+ latent_channels: int
23
+ latent_frequency_bins: int
24
+
25
+ def num_of_latents_from_frames(self, frames_number: int, frame_rate: int) -> int: ...
26
+
27
+
28
+ class _AceStep15Clip(Protocol):
29
+ def tokenize(
30
+ self,
31
+ tags: str,
32
+ *,
33
+ lyrics: str,
34
+ bpm: int,
35
+ duration: float,
36
+ timesignature: int,
37
+ language: str,
38
+ keyscale: str,
39
+ seed: int,
40
+ generate_audio_codes: bool,
41
+ cfg_scale: float,
42
+ temperature: float,
43
+ top_p: float,
44
+ top_k: int,
45
+ min_p: float,
46
+ ) -> Any: ...
47
+
48
+ def encode_from_tokens_scheduled(self, tokens: Any) -> Any: ...
49
+
50
+
51
+ def _get_ltxv_empty_latent_audio_type() -> Any:
52
+ """Resolve ComfyUI LTXVEmptyLatentAudio node at call time."""
53
+ from ._runtime import ensure_comfyui_on_path
54
+
55
+ ensure_comfyui_on_path()
56
+ from comfy_extras.nodes_lt_audio import LTXVEmptyLatentAudio
57
+
58
+ return LTXVEmptyLatentAudio
59
+
60
+
61
+ def _get_ace_step_15_latent_audio_dependencies() -> tuple[Any, Any]:
62
+ """Resolve torch and ComfyUI model management at call time."""
63
+ from ._runtime import ensure_comfyui_on_path
64
+
65
+ ensure_comfyui_on_path()
66
+ import comfy.model_management
67
+ import torch
68
+
69
+ return torch, comfy.model_management
70
+
71
+
72
+ def _unwrap_node_output(output: Any) -> Any:
73
+ """Return first output for ComfyUI V3 nodes and tuple-style APIs."""
74
+ if hasattr(output, "result"):
75
+ return output.result[0]
76
+ if isinstance(output, tuple):
77
+ return output[0]
78
+ return output
79
+
80
+
81
+ def ltxv_audio_vae_encode(vae: _LtxvAudioVaeEncoder, audio: Any) -> dict[str, Any]:
82
+ """Encode raw audio with an LTXV audio VAE."""
83
+ audio_latents = vae.encode(audio)
84
+ return {"samples": audio_latents, "sample_rate": int(vae.sample_rate), "type": "audio"}
85
+
86
+
87
+ def ltxv_audio_vae_decode(vae: _LtxvAudioVaeDecoder, latent: Any) -> dict[str, Any]:
88
+ """Decode latent audio with an LTXV audio VAE."""
89
+ latent_tensor = latent["samples"] if isinstance(latent, dict) else latent
90
+ if getattr(latent_tensor, "is_nested", False):
91
+ latent_tensor = latent_tensor.unbind()[-1]
92
+ audio = vae.decode(latent_tensor).to(latent_tensor.device)
93
+ return {"waveform": audio, "sample_rate": int(vae.output_sample_rate)}
94
+
95
+
96
+ def ltxv_empty_latent_audio(
97
+ audio_vae: _LtxvAudioVae,
98
+ frames_number: int,
99
+ frame_rate: int = 25,
100
+ batch_size: int = 1,
101
+ ) -> dict[str, Any]:
102
+ """Create empty LTXV audio latents compatible with ComfyUI's audio pipeline."""
103
+ ltxv_empty_latent_audio_type = _get_ltxv_empty_latent_audio_type()
104
+ return cast(
105
+ dict[str, Any],
106
+ _unwrap_node_output(
107
+ ltxv_empty_latent_audio_type.execute(
108
+ frames_number=frames_number,
109
+ frame_rate=frame_rate,
110
+ batch_size=batch_size,
111
+ audio_vae=audio_vae,
112
+ )
113
+ )
114
+ )
115
+
116
+
117
+ def encode_ace_step_15_audio(
118
+ clip: _AceStep15Clip,
119
+ tags: str,
120
+ lyrics: str = "",
121
+ seed: int = 0,
122
+ bpm: int = 120,
123
+ duration: float = 120.0,
124
+ timesignature: str = "4",
125
+ language: str = "en",
126
+ keyscale: str = "C major",
127
+ generate_audio_codes: bool = True,
128
+ cfg_scale: float = 2.0,
129
+ temperature: float = 0.85,
130
+ top_p: float = 0.9,
131
+ top_k: int = 0,
132
+ min_p: float = 0.0,
133
+ ) -> Any:
134
+ """Encode ACE Step 1.5 text/audio metadata conditioning."""
135
+ tokens = clip.tokenize(
136
+ tags,
137
+ lyrics=lyrics,
138
+ bpm=bpm,
139
+ duration=duration,
140
+ timesignature=int(timesignature),
141
+ language=language,
142
+ keyscale=keyscale,
143
+ seed=seed,
144
+ generate_audio_codes=generate_audio_codes,
145
+ cfg_scale=cfg_scale,
146
+ temperature=temperature,
147
+ top_p=top_p,
148
+ top_k=top_k,
149
+ min_p=min_p,
150
+ )
151
+ return clip.encode_from_tokens_scheduled(tokens)
152
+
153
+
154
+ def empty_ace_step_15_latent_audio(seconds: float, batch_size: int = 1) -> dict[str, Any]:
155
+ """Create empty ACE Step 1.5 latents used as sampler noise input."""
156
+ torch, model_management = _get_ace_step_15_latent_audio_dependencies()
157
+ length = round(seconds * 48000 / 1920)
158
+ latent = torch.zeros([batch_size, 64, length], device=model_management.intermediate_device())
159
+ return {"samples": latent, "type": "audio"}
160
+
161
+
162
+ __all__ = [
163
+ "ltxv_audio_vae_encode",
164
+ "ltxv_audio_vae_decode",
165
+ "ltxv_empty_latent_audio",
166
+ "encode_ace_step_15_audio",
167
+ "empty_ace_step_15_latent_audio",
168
+ ]
@@ -0,0 +1,25 @@
1
+ """Prompt conditioning helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol
6
+
7
+
8
+ class _ClipTextEncoder(Protocol):
9
+ def tokenize(self, text: str) -> Any: ...
10
+
11
+ def encode_from_tokens_scheduled(self, tokens: Any) -> Any: ...
12
+
13
+
14
+ def encode_prompt(clip: _ClipTextEncoder, text: str) -> Any:
15
+ """Encode prompt text with a ComfyUI-compatible CLIP object.
16
+
17
+ Positive and negative prompts use the same encoding path; prompt
18
+ semantics are owned by the caller.
19
+ """
20
+ normalized_text = " " if text == "" else text
21
+ tokens = clip.tokenize(normalized_text)
22
+ return clip.encode_from_tokens_scheduled(tokens)
23
+
24
+
25
+ __all__ = ["encode_prompt"]
@@ -0,0 +1,34 @@
1
+ """LoRA application helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, cast
7
+
8
+
9
+ def apply_lora(
10
+ model: Any,
11
+ clip: Any,
12
+ path: str | Path,
13
+ strength_model: float,
14
+ strength_clip: float,
15
+ ) -> tuple[Any, Any]:
16
+ """Apply a LoRA file to a model/CLIP pair and return patched copies.
17
+
18
+ The returned pair can be passed back into ``apply_lora`` to stack
19
+ multiple LoRAs by chaining calls.
20
+ """
21
+ from ._runtime import ensure_comfyui_on_path
22
+
23
+ ensure_comfyui_on_path()
24
+
25
+ import comfy.sd
26
+ import comfy.utils
27
+
28
+ lora_path = str(Path(path))
29
+ lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
30
+ patched = comfy.sd.load_lora_for_models(model, clip, lora, strength_model, strength_clip)
31
+ return cast(tuple[Any, Any], patched)
32
+
33
+
34
+ __all__ = ["apply_lora"]
@@ -0,0 +1,245 @@
1
+ """Model management public API.
2
+
3
+ This module must stay import-safe in CPU-only environments. It intentionally avoids
4
+ importing ComfyUI loaders at module import time.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from ._runtime import ensure_comfyui_on_path
14
+
15
+
16
+ @dataclass
17
+ class CheckpointResult:
18
+ """Container for objects produced by a ComfyUI checkpoint load."""
19
+
20
+ model: Any
21
+ clip: Any | None
22
+ vae: Any | None
23
+
24
+
25
+ class ModelManager:
26
+ """Entry point for model-loading operations.
27
+
28
+ The implementation is intentionally deferred to later user stories; this class
29
+ exists now to provide a stable, side-effect-free import surface.
30
+ """
31
+
32
+ def __init__(self, models_dir: str | Path) -> None:
33
+ """Store and validate the models directory used by future load operations."""
34
+ path = Path(models_dir)
35
+
36
+ if not path.exists():
37
+ raise ValueError(f"models_dir does not exist: {path}")
38
+ if not path.is_dir():
39
+ raise ValueError(f"models_dir is not a directory: {path}")
40
+
41
+ self.models_dir = path
42
+
43
+ ensure_comfyui_on_path()
44
+ import folder_paths
45
+
46
+ folder_paths.add_model_folder_path(
47
+ "checkpoints", str(self.models_dir / "checkpoints"), is_default=True
48
+ )
49
+ folder_paths.add_model_folder_path(
50
+ "embeddings", str(self.models_dir / "embeddings"), is_default=True
51
+ )
52
+ folder_paths.add_model_folder_path(
53
+ "diffusion_models", str(self.models_dir / "unet"), is_default=True
54
+ )
55
+ folder_paths.add_model_folder_path(
56
+ "diffusion_models", str(self.models_dir / "diffusion_models"), is_default=False
57
+ )
58
+ folder_paths.add_model_folder_path(
59
+ "text_encoders", str(self.models_dir / "text_encoders"), is_default=True
60
+ )
61
+ folder_paths.add_model_folder_path(
62
+ "text_encoders", str(self.models_dir / "clip"), is_default=False
63
+ )
64
+ folder_paths.add_model_folder_path(
65
+ "vae", str(self.models_dir / "vae"), is_default=True
66
+ )
67
+
68
+ def load_checkpoint(self, filename: str) -> CheckpointResult:
69
+ """Load a checkpoint by filename from the configured checkpoints directory."""
70
+ ensure_comfyui_on_path()
71
+
72
+ requested_path = (self.models_dir / "checkpoints" / filename).resolve()
73
+ if not requested_path.is_file():
74
+ raise FileNotFoundError(f"checkpoint file not found: {requested_path}")
75
+
76
+ import folder_paths
77
+ from comfy import sd as comfy_sd
78
+
79
+ checkpoint_path = folder_paths.get_full_path_or_raise("checkpoints", filename)
80
+ loaded = comfy_sd.load_checkpoint_guess_config(
81
+ checkpoint_path,
82
+ output_vae=True,
83
+ output_clip=True,
84
+ embedding_directory=folder_paths.get_folder_paths("embeddings"),
85
+ )
86
+ model, clip, vae = loaded[:3]
87
+ return CheckpointResult(model=model, clip=clip, vae=vae)
88
+
89
+ def load_vae(self, path: str | Path) -> Any:
90
+ """Load a standalone VAE from a path or filename.
91
+
92
+ If ``path`` is an absolute path to an existing file, that file is loaded.
93
+ Otherwise ``path`` is treated as a filename under the ``vae`` folder.
94
+ """
95
+ ensure_comfyui_on_path()
96
+
97
+ import folder_paths
98
+ from comfy import sd as comfy_sd
99
+ from comfy import utils as comfy_utils
100
+
101
+ p = Path(path)
102
+ if p.is_absolute() and p.is_file():
103
+ vae_path = str(p.resolve())
104
+ elif p.is_absolute():
105
+ raise FileNotFoundError(f"vae file not found: {p}")
106
+ else:
107
+ name = path if isinstance(path, str) else p.name
108
+ vae_path = folder_paths.get_full_path_or_raise("vae", name)
109
+
110
+ state_dict, metadata = comfy_utils.load_torch_file(
111
+ vae_path, return_metadata=True
112
+ )
113
+ vae = comfy_sd.VAE(sd=state_dict, metadata=metadata)
114
+ vae.throw_exception_if_invalid()
115
+ return vae
116
+
117
+ def load_clip(
118
+ self,
119
+ path: str | Path,
120
+ *,
121
+ clip_type: str = "stable_diffusion",
122
+ ) -> Any:
123
+ """Load a standalone text encoder (CLIP) from a path or filename.
124
+
125
+ If ``path`` is an absolute path to an existing file, that file is loaded.
126
+ Otherwise ``path`` is treated as a filename under ``text_encoders`` / ``clip``.
127
+
128
+ ``clip_type`` selects the encoder architecture (e.g. ``"wan"`` for Wan / UMT5-XXL,
129
+ ``"stable_diffusion"``, ``"sd3"``, ``"flux"``). Must match the model weights.
130
+ """
131
+ ensure_comfyui_on_path()
132
+
133
+ import folder_paths
134
+ from comfy import sd as comfy_sd
135
+
136
+ p = Path(path)
137
+ if p.is_absolute() and p.is_file():
138
+ full_path = str(p.resolve())
139
+ elif p.is_absolute():
140
+ raise FileNotFoundError(f"clip file not found: {p}")
141
+ else:
142
+ name = path if isinstance(path, str) else p.name
143
+ full_path = folder_paths.get_full_path_or_raise("text_encoders", name)
144
+
145
+ clip_type_enum = getattr(
146
+ comfy_sd.CLIPType,
147
+ clip_type.upper(),
148
+ comfy_sd.CLIPType.STABLE_DIFFUSION,
149
+ )
150
+ return comfy_sd.load_clip(
151
+ ckpt_paths=[full_path],
152
+ embedding_directory=folder_paths.get_folder_paths("embeddings"),
153
+ clip_type=clip_type_enum,
154
+ )
155
+
156
+ def load_unet(self, path: str | Path) -> Any:
157
+ """Load a standalone diffusion model (UNet) from a path or filename.
158
+
159
+ If ``path`` is an absolute path to an existing file, that file is loaded.
160
+ Otherwise ``path`` is treated as a filename and resolved under the
161
+ ``diffusion_models`` / ``unet`` folders (see ComfyUI folder layout).
162
+ """
163
+ ensure_comfyui_on_path()
164
+
165
+ import folder_paths
166
+ from comfy import sd as comfy_sd
167
+
168
+ p = Path(path)
169
+ if p.is_absolute() and p.is_file():
170
+ full_path = str(p.resolve())
171
+ elif p.is_absolute():
172
+ raise FileNotFoundError(f"unet file not found: {p}")
173
+ else:
174
+ name = path if isinstance(path, str) else p.name
175
+ full_path = folder_paths.get_full_path_or_raise("diffusion_models", name)
176
+
177
+ return comfy_sd.load_diffusion_model(full_path)
178
+
179
+ def load_ltxv_audio_vae(self, path: str | Path) -> object:
180
+ """Load an LTXV audio VAE checkpoint from a path or filename.
181
+
182
+ If ``path`` is an absolute path to an existing file, that file is loaded.
183
+ Otherwise ``path`` is treated as a filename under the ``checkpoints`` folder.
184
+ """
185
+ ensure_comfyui_on_path()
186
+
187
+ import folder_paths
188
+ from comfy import utils as comfy_utils
189
+ from comfy.ldm.lightricks.vae.audio_vae import AudioVAE
190
+
191
+ p = Path(path)
192
+ if p.is_absolute() and p.is_file():
193
+ checkpoint_path = str(p.resolve())
194
+ elif p.is_absolute():
195
+ raise FileNotFoundError(f"ltxv audio vae file not found: {p}")
196
+ else:
197
+ name = path if isinstance(path, str) else p.name
198
+ checkpoint_path = folder_paths.get_full_path_or_raise("checkpoints", name)
199
+
200
+ state_dict, metadata = comfy_utils.load_torch_file(
201
+ checkpoint_path, return_metadata=True
202
+ )
203
+ return AudioVAE(state_dict, metadata)
204
+
205
+ def load_ltxav_text_encoder(
206
+ self, text_encoder_path: str | Path, checkpoint_path: str | Path
207
+ ) -> object:
208
+ """Load an LTXAV text encoder from two separate files.
209
+
210
+ ``text_encoder_path`` is the text encoder file (from ``text_encoders/``).
211
+ ``checkpoint_path`` is the companion checkpoint file (from ``checkpoints/``).
212
+ Both can be absolute paths to existing files or relative filenames resolved
213
+ via folder_paths.
214
+ """
215
+ ensure_comfyui_on_path()
216
+
217
+ import folder_paths
218
+ from comfy import sd as comfy_sd
219
+
220
+ te_p = Path(text_encoder_path)
221
+ if te_p.is_absolute() and te_p.is_file():
222
+ resolved_te = str(te_p.resolve())
223
+ elif te_p.is_absolute():
224
+ raise FileNotFoundError(f"ltxav text encoder file not found: {te_p}")
225
+ else:
226
+ name = text_encoder_path if isinstance(text_encoder_path, str) else te_p.name
227
+ resolved_te = folder_paths.get_full_path_or_raise("text_encoders", name)
228
+
229
+ ckpt_p = Path(checkpoint_path)
230
+ if ckpt_p.is_absolute() and ckpt_p.is_file():
231
+ resolved_ckpt = str(ckpt_p.resolve())
232
+ elif ckpt_p.is_absolute():
233
+ raise FileNotFoundError(f"ltxav checkpoint file not found: {ckpt_p}")
234
+ else:
235
+ name = checkpoint_path if isinstance(checkpoint_path, str) else ckpt_p.name
236
+ resolved_ckpt = folder_paths.get_full_path_or_raise("checkpoints", name)
237
+
238
+ return comfy_sd.load_clip(
239
+ ckpt_paths=[resolved_te, resolved_ckpt],
240
+ embedding_directory=folder_paths.get_folder_paths("embeddings"),
241
+ clip_type=comfy_sd.CLIPType.LTXV,
242
+ )
243
+
244
+
245
+ __all__ = ["CheckpointResult", "ModelManager"]
@@ -0,0 +1,86 @@
1
+ """Runtime diagnostics for comfy_diffusion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+ from typing import Any
8
+
9
+
10
+ def _python_version() -> str:
11
+ return ".".join(str(part) for part in sys.version_info[:3])
12
+
13
+
14
+ def _runtime_not_found(python_version: str, detail: str = "") -> dict[str, Any]:
15
+ msg = "ComfyUI runtime not found. Run: git submodule update --init"
16
+ if detail:
17
+ msg += f" (or install missing deps). Cause: {detail}"
18
+ return {
19
+ "error": msg,
20
+ "comfyui_version": None,
21
+ "device": None,
22
+ "vram_total_mb": None,
23
+ "vram_free_mb": None,
24
+ "python_version": python_version,
25
+ }
26
+
27
+
28
+ def _runtime_not_responsive(python_version: str, message: str) -> dict[str, Any]:
29
+ return {
30
+ "error": f"ComfyUI runtime is not responsive: {message}",
31
+ "comfyui_version": None,
32
+ "device": None,
33
+ "vram_total_mb": None,
34
+ "vram_free_mb": None,
35
+ "python_version": python_version,
36
+ }
37
+
38
+
39
+ def _bytes_to_mb(value: int) -> int:
40
+ return value // (1024 * 1024)
41
+
42
+
43
+ def check_runtime() -> dict[str, Any]:
44
+ """Return structured runtime diagnostics for the current Python process."""
45
+ python_version = _python_version()
46
+
47
+ from ._runtime import ensure_comfyui_on_path
48
+ ensure_comfyui_on_path()
49
+
50
+ try:
51
+ comfyui_version_module = importlib.import_module("comfyui_version")
52
+ model_management = importlib.import_module("comfy.model_management")
53
+ except Exception as exc:
54
+ return _runtime_not_found(python_version, str(exc))
55
+
56
+ try:
57
+ device = model_management.get_torch_device()
58
+ except Exception as exc:
59
+ return _runtime_not_responsive(python_version, str(exc))
60
+
61
+ comfyui_version = str(getattr(comfyui_version_module, "__version__", "unknown"))
62
+ device_str = str(device)
63
+ device_type = getattr(device, "type", "")
64
+
65
+ if device_type == "cpu" or device_str == "cpu":
66
+ return {
67
+ "comfyui_version": comfyui_version,
68
+ "device": "cpu",
69
+ "vram_total_mb": 0,
70
+ "vram_free_mb": 0,
71
+ "python_version": python_version,
72
+ }
73
+
74
+ try:
75
+ total_memory_bytes = model_management.get_total_memory(device)
76
+ free_memory_bytes = model_management.get_free_memory(device)
77
+ except Exception as exc:
78
+ return _runtime_not_responsive(python_version, str(exc))
79
+
80
+ return {
81
+ "comfyui_version": comfyui_version,
82
+ "device": device_str,
83
+ "vram_total_mb": _bytes_to_mb(int(total_memory_bytes)),
84
+ "vram_free_mb": _bytes_to_mb(int(free_memory_bytes)),
85
+ "python_version": python_version,
86
+ }