inferencekit-cli 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.
- inferencekit/__init__.py +9 -0
- inferencekit/__main__.py +5 -0
- inferencekit/analysis/__init__.py +1 -0
- inferencekit/analysis/ncu_csv.py +284 -0
- inferencekit/analysis/nsys_sqlite.py +518 -0
- inferencekit/canonical.py +104 -0
- inferencekit/checkpoint.py +396 -0
- inferencekit/cli.py +274 -0
- inferencekit/cuda/bw_peak.cu +252 -0
- inferencekit/cuda/torch_bw.py +60 -0
- inferencekit/engines/__init__.py +7 -0
- inferencekit/engines/adapters.py +190 -0
- inferencekit/engines/clip/__init__.py +13 -0
- inferencekit/engines/clip/comfyui.py +749 -0
- inferencekit/engines/clip/diffusers_ltx.py +601 -0
- inferencekit/engines/clip/noise.py +46 -0
- inferencekit/engines/clip/official_ltx.py +316 -0
- inferencekit/engines/clip/server.py +542 -0
- inferencekit/engines/clip/stepdump.py +182 -0
- inferencekit/engines/gates.py +94 -0
- inferencekit/engines/sglang_worker.py +86 -0
- inferencekit/engines/vllm_worker.py +129 -0
- inferencekit/evidence.py +427 -0
- inferencekit/gpuinfo.py +93 -0
- inferencekit/hardware.py +135 -0
- inferencekit/mcp_server.py +178 -0
- inferencekit/ops/__init__.py +30 -0
- inferencekit/ops/bench.py +184 -0
- inferencekit/ops/bench_audio.py +384 -0
- inferencekit/ops/bench_clip.py +819 -0
- inferencekit/ops/bench_image.py +393 -0
- inferencekit/ops/bench_rounds.py +297 -0
- inferencekit/ops/bench_serve.py +866 -0
- inferencekit/ops/ceiling.py +135 -0
- inferencekit/ops/compare.py +378 -0
- inferencekit/ops/correctness.py +153 -0
- inferencekit/ops/doctor.py +486 -0
- inferencekit/ops/gates.py +1772 -0
- inferencekit/ops/gates_clip.py +930 -0
- inferencekit/ops/intake.py +1321 -0
- inferencekit/ops/ledger.py +749 -0
- inferencekit/ops/manifest.py +309 -0
- inferencekit/ops/ncu.py +183 -0
- inferencekit/ops/nsys.py +224 -0
- inferencekit/ops/prompts.py +316 -0
- inferencekit/ops/quality.py +2161 -0
- inferencekit/ops/quality_clip.py +690 -0
- inferencekit/ops/quality_clip_steps.py +341 -0
- inferencekit/ops/report.py +317 -0
- inferencekit/ops/review.py +119 -0
- inferencekit/ops/rocprof.py +89 -0
- inferencekit/ops/roofline.py +392 -0
- inferencekit/ops/store.py +61 -0
- inferencekit/proc.py +172 -0
- inferencekit/prompts/ATTRIBUTION.md +56 -0
- inferencekit/prompts/__init__.py +167 -0
- inferencekit/prompts/build.py +518 -0
- inferencekit/prompts/dev-load.jsonl.gz +0 -0
- inferencekit/prompts/dev-load.manifest.json +212 -0
- inferencekit/prompts/dev.jsonl.gz +0 -0
- inferencekit/prompts/dev.manifest.json +214 -0
- inferencekit/prompts/exclusions.json +54 -0
- inferencekit/quality/__init__.py +23 -0
- inferencekit/quality/client.py +66 -0
- inferencekit/quality/data/gsm8k_LICENSE.txt +21 -0
- inferencekit/quality/fake.py +329 -0
- inferencekit/quality/gsm8k.py +224 -0
- inferencekit/quality/logprobs.py +106 -0
- inferencekit/quality/metrics.py +235 -0
- inferencekit/quality/niah.py +63 -0
- inferencekit/quality/parquet.py +296 -0
- inferencekit/quality/sampling.py +262 -0
- inferencekit/quality/step_replay.py +217 -0
- inferencekit/quality/video_metrics.py +304 -0
- inferencekit/registry.py +206 -0
- inferencekit/review/__init__.py +6 -0
- inferencekit/review/diff.py +163 -0
- inferencekit/review/media.py +181 -0
- inferencekit/review/page.py +920 -0
- inferencekit/review/rundir.py +844 -0
- inferencekit/roofline_headers.py +504 -0
- inferencekit/schemas/evidence.v1.json +130 -0
- inferencekit/schemas/evidence.v2.json +499 -0
- inferencekit/schemas/intake.v1.json +276 -0
- inferencekit/schemas/run-manifest.v1.json +118 -0
- inferencekit/serve/__init__.py +8 -0
- inferencekit/serve/audio_client.py +330 -0
- inferencekit/serve/client.py +446 -0
- inferencekit/serve/clip_client.py +351 -0
- inferencekit/serve/clip_rounds.py +429 -0
- inferencekit/serve/image_client.py +373 -0
- inferencekit/serve/loadgen.py +136 -0
- inferencekit/serve/metrics.py +203 -0
- inferencekit/serve/profiles.py +105 -0
- inferencekit/serve/prompts_io.py +125 -0
- inferencekit/serve/rounds.py +1111 -0
- inferencekit/serve/servers.py +897 -0
- inferencekit/stats.py +249 -0
- inferencekit_cli-0.1.0.dist-info/METADATA +233 -0
- inferencekit_cli-0.1.0.dist-info/RECORD +104 -0
- inferencekit_cli-0.1.0.dist-info/WHEEL +4 -0
- inferencekit_cli-0.1.0.dist-info/entry_points.txt +2 -0
- inferencekit_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- inferencekit_cli-0.1.0.dist-info/licenses/NOTICE +9 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""The official LTX-2 inference pipelines (the ``ltx_pipelines`` package) behind the clip server.
|
|
2
|
+
|
|
3
|
+
python -m inferencekit.engines.clip.server --pipeline official_ltx --model DIR --task t2v \\
|
|
4
|
+
--pipeline-arg builder=MODULE:CALLABLE [--pipeline-arg builder.NAME=VALUE ...] \\
|
|
5
|
+
[--pipeline-arg step_module=ATTR.PATH --pipeline-arg forwards_per_step=N]
|
|
6
|
+
|
|
7
|
+
Run on a GPU host with ltx-pipelines v1.4.2 (15-16 Oct 2026: the LTX-2 monolith checkpoint, and LTX-2.5's split
|
|
8
|
+
files through ``builder.transformer/text_encoder/video_vae/audio_vae``, with the one_stage builder below; on
|
|
9
|
+
LTX-2.5 bench rounds and gates clip PASS for t2v and i2v). Written before the package was installed: Diffusers'
|
|
10
|
+
encode_video cites
|
|
11
|
+
``packages/ltx-pipelines/src/ltx_pipelines/utils/media_io.py`` in the LTX-2 repository, so the import name
|
|
12
|
+
is ``ltx_pipelines``. Everything else is looked up when the server starts or a clip runs, and refused when
|
|
13
|
+
it is not there, rather than guessed:
|
|
14
|
+
|
|
15
|
+
* The pipeline: ``builder=MODULE:CALLABLE`` (required; e.g. a pipeline class or a small function of
|
|
16
|
+
your own in that venv) is called as ``builder(model=..., device=..., dtype=..., **builder.* args)``;
|
|
17
|
+
the object it returns makes one clip per call (``generate`` if it has one, else ``__call__``).
|
|
18
|
+
* Call arguments are matched by name (ALIASES, e.g. ``num_inference_steps`` or ``steps``). A request
|
|
19
|
+
field with no matching argument is refused (400), never dropped.
|
|
20
|
+
* Noise: if the call takes ``generator``, it gets ``torch.Generator("cpu").manual_seed(noise_seed)``;
|
|
21
|
+
else, if it takes ``seed``, it gets ``noise_seed``. Whether the package draws video then audio noise
|
|
22
|
+
from it the way noise.py does is unchecked, so ``effective.noise_check`` is false: do not use this
|
|
23
|
+
engine as one side of a shared-noise quality comparison until a check shows it.
|
|
24
|
+
* Steps: ``step_module=ATTR.PATH`` names the denoiser module on the pipeline object; its forward
|
|
25
|
+
calls are counted and divided by ``forwards_per_step`` (default 1; 2 when guidance runs the
|
|
26
|
+
conditional and unconditional passes as separate calls). Without it steps_run is null (not counted)
|
|
27
|
+
and bench clip says the steps were not checked.
|
|
28
|
+
* Output: if the call takes ``output_path`` the package writes the mp4 itself; otherwise it must return
|
|
29
|
+
(video, audio) or an object with ``video``/``frames`` and ``audio``, frames shaped [F, H, W, 3]
|
|
30
|
+
(uint8, or float in [0, 1]), which are muxed with ``diffusers.utils.encode_video`` (needs Diffusers
|
|
31
|
+
and PyAV in that venv). Audio needs ``--pipeline-arg audio_sample_rate=N``.
|
|
32
|
+
|
|
33
|
+
Stages: only ``generate`` (the whole call) and ``mux``; the package's inner stages are not hooked.
|
|
34
|
+
|
|
35
|
+
Per-step dumps (stepdump.py, for quality clip-steps): NOT written. The package's denoising loop is not hooked (it
|
|
36
|
+
was not read when this wrapper was written), so an engine served through this wrapper has no dumps and cannot pass
|
|
37
|
+
quality clip-steps; a hook belongs where the package calls its sampler step, recording x, the guided velocity and
|
|
38
|
+
the next latent with stepdump.StepRecorder and returning it as Generated.step_dump.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import hashlib
|
|
44
|
+
import importlib
|
|
45
|
+
import inspect
|
|
46
|
+
import io
|
|
47
|
+
|
|
48
|
+
from inferencekit.engines.clip.server import BadRequest, ClipJob, Generated, PipelineOptions, StageClock
|
|
49
|
+
|
|
50
|
+
# request field -> call argument names to try, in order
|
|
51
|
+
ALIASES = {
|
|
52
|
+
"prompt": ("prompt",),
|
|
53
|
+
"width": ("width",),
|
|
54
|
+
"height": ("height",),
|
|
55
|
+
"num_frames": ("num_frames", "frames", "length"),
|
|
56
|
+
"fps": ("frame_rate", "fps"),
|
|
57
|
+
"steps": ("num_inference_steps", "steps", "num_steps"),
|
|
58
|
+
"guidance_scale": ("guidance_scale", "cfg_guidance_scale", "cfg_scale", "cfg"),
|
|
59
|
+
"image": ("image", "images", "conditioning_image"),
|
|
60
|
+
}
|
|
61
|
+
OPTIONS = {"builder", "step_module", "forwards_per_step", "audio_sample_rate", "negative_prompt"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _resolve(spec: str):
|
|
65
|
+
mod, _, attr = str(spec).partition(":")
|
|
66
|
+
if not attr:
|
|
67
|
+
raise ValueError(f"builder must be MODULE:CALLABLE, got {spec!r}")
|
|
68
|
+
obj = importlib.import_module(mod)
|
|
69
|
+
for part in attr.split("."):
|
|
70
|
+
obj = getattr(obj, part)
|
|
71
|
+
return obj
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_pipeline(opts: PipelineOptions) -> OfficialLTX:
|
|
75
|
+
try:
|
|
76
|
+
import ltx_pipelines # noqa: F401 - the guard: refuse early when the package is missing
|
|
77
|
+
except ImportError as e:
|
|
78
|
+
raise ImportError("official_ltx needs the ltx_pipelines package (LTX-2 repository) in this "
|
|
79
|
+
f"interpreter: {e}") from None
|
|
80
|
+
import torch
|
|
81
|
+
|
|
82
|
+
unknown = sorted(k for k in opts.extra if k not in OPTIONS and not k.startswith("builder."))
|
|
83
|
+
if unknown:
|
|
84
|
+
raise ValueError(f"unknown --pipeline-arg {unknown}; known: {sorted(OPTIONS)} and builder.NAME")
|
|
85
|
+
if not opts.extra.get("builder"):
|
|
86
|
+
raise ValueError("official_ltx needs --pipeline-arg builder=MODULE:CALLABLE (see the module docstring)")
|
|
87
|
+
dtypes = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}
|
|
88
|
+
if opts.dtype not in dtypes:
|
|
89
|
+
raise ValueError(f"--dtype must be one of {sorted(dtypes)}")
|
|
90
|
+
if opts.offload != "none" or opts.attention:
|
|
91
|
+
raise ValueError("official_ltx takes no --offload or --attention (pass the package's own options "
|
|
92
|
+
"as builder.NAME=VALUE)")
|
|
93
|
+
build = _resolve(opts.extra["builder"])
|
|
94
|
+
bkw = {k[8:]: v for k, v in opts.extra.items() if k.startswith("builder.")}
|
|
95
|
+
pipe = build(model=opts.model, device=opts.device, dtype=dtypes[opts.dtype], **bkw)
|
|
96
|
+
version = getattr(importlib.import_module("ltx_pipelines"), "__version__", None)
|
|
97
|
+
return OfficialLTX(pipe, opts, torch, {"pipeline": f"ltx_pipelines:{opts.extra['builder']}",
|
|
98
|
+
"ltx_pipelines_version": version, "torch_version": torch.__version__})
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class OfficialLTX:
|
|
102
|
+
def __init__(self, pipe, opts: PipelineOptions, torch_mod, facts: dict | None = None):
|
|
103
|
+
self.pipe, self.opts, self.torch = pipe, opts, torch_mod
|
|
104
|
+
self.facts = dict(facts or {})
|
|
105
|
+
self.fn = getattr(pipe, "generate", None) or pipe
|
|
106
|
+
self.params = set(inspect.signature(self.fn).parameters)
|
|
107
|
+
self.forwards = 0
|
|
108
|
+
self.per_step = int(opts.extra.get("forwards_per_step") or 1)
|
|
109
|
+
self.counting = False
|
|
110
|
+
self._out_path: str | None = None
|
|
111
|
+
if opts.extra.get("step_module"):
|
|
112
|
+
mod = pipe
|
|
113
|
+
for part in str(opts.extra["step_module"]).split("."):
|
|
114
|
+
mod = getattr(mod, part)
|
|
115
|
+
mod.register_forward_pre_hook(self._count)
|
|
116
|
+
self.counting = True
|
|
117
|
+
|
|
118
|
+
def _count(self, module, args):
|
|
119
|
+
self.forwards += 1
|
|
120
|
+
|
|
121
|
+
def _arg(self, field: str) -> str:
|
|
122
|
+
for name in ALIASES[field]:
|
|
123
|
+
if name in self.params:
|
|
124
|
+
return name
|
|
125
|
+
raise BadRequest(f"the pipeline call takes none of {list(ALIASES[field])} for {field}")
|
|
126
|
+
|
|
127
|
+
def describe(self) -> dict:
|
|
128
|
+
neg = self.opts.extra.get("negative_prompt")
|
|
129
|
+
own = self.pipe.describe() if callable(getattr(self.pipe, "describe", None)) else {}
|
|
130
|
+
return {**self.facts, "model": self.opts.model, "task": self.opts.task, "dtype": self.opts.dtype,
|
|
131
|
+
"attention": "package default", "device": self.opts.device,
|
|
132
|
+
"steps_counted": self.counting or hasattr(self.pipe, "steps_run"),
|
|
133
|
+
"negative_prompt_sha256": hashlib.sha256(str(neg).encode()).hexdigest() if neg is not None else None,
|
|
134
|
+
**own}
|
|
135
|
+
|
|
136
|
+
def noise_shapes(self, job: ClipJob) -> dict:
|
|
137
|
+
if job.task != self.opts.task:
|
|
138
|
+
raise BadRequest(f"this server loaded the {self.opts.task} pipeline; start another for {job.task}")
|
|
139
|
+
if "output_path" not in self.params and float(job.fps) != int(job.fps):
|
|
140
|
+
raise BadRequest(f"fps must be a whole number for this muxer, got {job.fps:g}")
|
|
141
|
+
return {} # the package draws its own noise (see the module docstring)
|
|
142
|
+
|
|
143
|
+
def generate(self, job: ClipJob, noise: dict, clock: StageClock) -> Generated:
|
|
144
|
+
kw = {self._arg(f): getattr(job, f) for f in ("prompt", "width", "height", "num_frames", "fps", "steps",
|
|
145
|
+
"guidance_scale")}
|
|
146
|
+
if job.task == "i2v":
|
|
147
|
+
from PIL import Image
|
|
148
|
+
|
|
149
|
+
kw[self._arg("image")] = Image.open(io.BytesIO(job.image)).convert("RGB")
|
|
150
|
+
if "generator" in self.params:
|
|
151
|
+
kw["generator"], how = self.torch.Generator(device="cpu").manual_seed(job.noise_seed), "cpu generator"
|
|
152
|
+
elif "seed" in self.params:
|
|
153
|
+
kw["seed"], how = job.noise_seed, "package seed"
|
|
154
|
+
else:
|
|
155
|
+
raise BadRequest("the pipeline call takes neither generator nor seed: the noise cannot be fixed")
|
|
156
|
+
if self.opts.extra.get("negative_prompt") is not None:
|
|
157
|
+
if "negative_prompt" not in self.params:
|
|
158
|
+
raise BadRequest("negative_prompt was set but the pipeline call does not take it")
|
|
159
|
+
kw["negative_prompt"] = self.opts.extra["negative_prompt"]
|
|
160
|
+
self._out_path = None
|
|
161
|
+
if "output_path" in self.params:
|
|
162
|
+
import os
|
|
163
|
+
import tempfile
|
|
164
|
+
|
|
165
|
+
fd, self._out_path = tempfile.mkstemp(suffix=".mp4", dir=os.getcwd())
|
|
166
|
+
os.close(fd)
|
|
167
|
+
kw["output_path"] = self._out_path
|
|
168
|
+
self.forwards = 0
|
|
169
|
+
with clock.span("generate"):
|
|
170
|
+
ret = self.fn(**kw)
|
|
171
|
+
if self.counting: # else nothing counted them: the server reports steps_run null, not 0
|
|
172
|
+
clock.set_steps(self.forwards // self.per_step)
|
|
173
|
+
elif isinstance(getattr(self.pipe, "steps_run", None), int): # a builder that counts its own loop
|
|
174
|
+
clock.set_steps(self.pipe.steps_run)
|
|
175
|
+
video, audio = (ret if isinstance(ret, tuple) else (getattr(ret, "video", getattr(ret, "frames", None)),
|
|
176
|
+
getattr(ret, "audio", None)))
|
|
177
|
+
eff = {"noise_check": False, "generator": how}
|
|
178
|
+
if self._out_path: # the package wrote the file: audio is what the builder says it muxed, else unknown
|
|
179
|
+
audio_written = getattr(self.pipe, "audio_written", None)
|
|
180
|
+
return Generated(video=None, width=job.width, height=job.height, frames=job.num_frames, fps=job.fps,
|
|
181
|
+
effective={**eff, "shape_source": "request (the package wrote the file)",
|
|
182
|
+
"audio": audio_written if isinstance(audio_written, bool) else None})
|
|
183
|
+
if video is None:
|
|
184
|
+
raise RuntimeError("the pipeline returned no frames and took no output_path")
|
|
185
|
+
frames = video[0] if getattr(video, "ndim", 4) == 5 else video
|
|
186
|
+
n, h, w = (int(d) for d in frames.shape[:3])
|
|
187
|
+
rate = self.opts.extra.get("audio_sample_rate")
|
|
188
|
+
wave = None
|
|
189
|
+
if audio is not None and job.audio is not False and rate:
|
|
190
|
+
wave = audio[0] if getattr(audio, "ndim", 2) == 3 else audio
|
|
191
|
+
return Generated(video=frames, width=w, height=h, frames=n, fps=job.fps, audio=wave,
|
|
192
|
+
audio_sample_rate=int(rate) if wave is not None else None, effective=eff)
|
|
193
|
+
|
|
194
|
+
def write(self, out: Generated, path, clock: StageClock) -> None:
|
|
195
|
+
if out.video is None and self._out_path:
|
|
196
|
+
import shutil
|
|
197
|
+
|
|
198
|
+
shutil.move(self._out_path, path)
|
|
199
|
+
return
|
|
200
|
+
from diffusers.utils import encode_video
|
|
201
|
+
|
|
202
|
+
encode_video(out.video, fps=int(out.fps), output_path=str(path), audio=out.audio,
|
|
203
|
+
audio_sample_rate=out.audio_sample_rate)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# --------------------------------------------------------------------------- a builder for the one-stage pipeline
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def one_stage(model: str, device: str, dtype, gemma_root: str | None = None, transformer: str | None = None,
|
|
210
|
+
text_encoder: str | None = None, video_vae: str | None = None, audio_vae: str | None = None,
|
|
211
|
+
negative_prompt: str | None = None) -> OneStage:
|
|
212
|
+
"""``builder=inferencekit.engines.clip.official_ltx:one_stage``: the official text/image-to-video one-stage
|
|
213
|
+
pipeline (``ltx_pipelines.ti2vid_one_stage.TI2VidOneStagePipeline``, the full model with guidance), with
|
|
214
|
+
the package's own defaults for everything the request does not set.
|
|
215
|
+
|
|
216
|
+
``--model`` is the monolith checkpoint (e.g. ltx-2-19b-dev.safetensors) with ``builder.gemma_root=DIR``
|
|
217
|
+
(the Gemma text encoder directory), or a split pack: ``builder.transformer=``, ``builder.text_encoder=``,
|
|
218
|
+
``builder.video_vae=``, ``builder.audio_vae=`` (``--model`` is then only recorded). Guidance: the request's
|
|
219
|
+
guidance_scale is the video CFG scale; every other guider setting (STG, rescale, modality, the audio
|
|
220
|
+
guider) is the package's default for the checkpoint (``detect_params``), recorded in describe(). The
|
|
221
|
+
negative prompt is the package's DEFAULT_NEGATIVE_PROMPT unless ``builder.negative_prompt`` is given. The
|
|
222
|
+
package draws its own noise on the GPU from the seed (no shared-noise recipe), writes the mp4 itself and
|
|
223
|
+
builds the transformer on every call, as it ships. Steps are counted by the denoise loop's calls.
|
|
224
|
+
"""
|
|
225
|
+
import torch
|
|
226
|
+
from ltx_pipelines.ti2vid_one_stage import TI2VidOneStagePipeline
|
|
227
|
+
from ltx_pipelines.utils.constants import detect_params
|
|
228
|
+
from ltx_pipelines.utils.model_paths import ModelPaths
|
|
229
|
+
|
|
230
|
+
if dtype != torch.bfloat16:
|
|
231
|
+
raise ValueError("the official one-stage pipeline runs in BF16 only (--dtype bf16)")
|
|
232
|
+
split = {k: v for k, v in (("transformer_path", transformer), ("text_encoder_path", text_encoder),
|
|
233
|
+
("video_vae_path", video_vae), ("audio_vae_path", audio_vae)) if v}
|
|
234
|
+
if split:
|
|
235
|
+
paths = ModelPaths.from_split(**split)
|
|
236
|
+
checkpoint = transformer or model
|
|
237
|
+
else:
|
|
238
|
+
if not gemma_root:
|
|
239
|
+
raise ValueError("a monolith checkpoint needs --pipeline-arg builder.gemma_root=DIR (the text encoder)")
|
|
240
|
+
paths, checkpoint = ModelPaths.from_monolith(model, gemma_root), model
|
|
241
|
+
params = detect_params(checkpoint)
|
|
242
|
+
pipe = TI2VidOneStagePipeline(model_paths=paths, loras=[], device=torch.device(device))
|
|
243
|
+
if negative_prompt is None:
|
|
244
|
+
from ltx_pipelines.utils.args import DEFAULT_NEGATIVE_PROMPT
|
|
245
|
+
negative_prompt = DEFAULT_NEGATIVE_PROMPT
|
|
246
|
+
return OneStage(pipe, params, negative_prompt)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class _CountingStage:
|
|
250
|
+
"""The pipeline's diffusion stage, with its denoiser wrapped so each loop step is counted."""
|
|
251
|
+
|
|
252
|
+
def __init__(self, stage, owner: OneStage):
|
|
253
|
+
self._stage, self._owner = stage, owner
|
|
254
|
+
|
|
255
|
+
def __getattr__(self, name):
|
|
256
|
+
return getattr(self._stage, name)
|
|
257
|
+
|
|
258
|
+
def __call__(self, *args, denoiser, **kwargs):
|
|
259
|
+
owner = self._owner
|
|
260
|
+
|
|
261
|
+
def counted(*a, **k):
|
|
262
|
+
owner.steps_run += 1
|
|
263
|
+
return denoiser(*a, **k)
|
|
264
|
+
|
|
265
|
+
return self._stage(*args, denoiser=counted, **kwargs)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class OneStage:
|
|
269
|
+
"""What ``one_stage`` returns: ``generate(...)`` makes one clip and writes it to ``output_path``."""
|
|
270
|
+
|
|
271
|
+
def __init__(self, pipe, params, negative_prompt: str):
|
|
272
|
+
self.pipe, self.params, self.negative_prompt = pipe, params, negative_prompt
|
|
273
|
+
self.steps_run: int | None = None
|
|
274
|
+
self.audio_written: bool | None = None # whether the last clip's file got an audio track
|
|
275
|
+
pipe.stage = _CountingStage(pipe.stage, self)
|
|
276
|
+
|
|
277
|
+
def describe(self) -> dict:
|
|
278
|
+
vg, ag = self.params.video_guider_params, self.params.audio_guider_params
|
|
279
|
+
return {"video_guider": {k: getattr(vg, k) for k in ("stg_scale", "rescale_scale", "modality_scale",
|
|
280
|
+
"skip_step", "stg_blocks")},
|
|
281
|
+
"audio_guider": {k: getattr(ag, k) for k in ("cfg_scale", "stg_scale", "rescale_scale",
|
|
282
|
+
"modality_scale", "skip_step", "stg_blocks")},
|
|
283
|
+
"negative_prompt_sha256": hashlib.sha256(self.negative_prompt.encode()).hexdigest()}
|
|
284
|
+
|
|
285
|
+
def generate(self, prompt, width, height, num_frames, frame_rate, num_inference_steps, guidance_scale, seed,
|
|
286
|
+
output_path, image=None):
|
|
287
|
+
import dataclasses
|
|
288
|
+
import os
|
|
289
|
+
import tempfile
|
|
290
|
+
|
|
291
|
+
import torch
|
|
292
|
+
from ltx_core.model.video_vae import get_video_chunks_number
|
|
293
|
+
from ltx_pipelines.utils.media_io import encode_video
|
|
294
|
+
from ltx_pipelines.utils.types import ImageConditioningInput
|
|
295
|
+
|
|
296
|
+
images, tmp = [], None
|
|
297
|
+
if image is not None: # the package conditions on an image file at frame 0, strength 1.0
|
|
298
|
+
fd, tmp = tempfile.mkstemp(suffix=".png", dir=os.path.dirname(os.path.abspath(output_path)))
|
|
299
|
+
os.close(fd)
|
|
300
|
+
image.save(tmp)
|
|
301
|
+
images = [ImageConditioningInput(tmp, 0, 1.0)]
|
|
302
|
+
self.steps_run, self.audio_written = 0, None
|
|
303
|
+
try:
|
|
304
|
+
with torch.inference_mode():
|
|
305
|
+
out = self.pipe(prompt=prompt, negative_prompt=self.negative_prompt, seed=int(seed), height=int(height),
|
|
306
|
+
width=int(width), frame_rate=float(frame_rate), num_frames=int(num_frames),
|
|
307
|
+
num_inference_steps=int(num_inference_steps),
|
|
308
|
+
video_guider_params=dataclasses.replace(self.params.video_guider_params,
|
|
309
|
+
cfg_scale=float(guidance_scale)),
|
|
310
|
+
audio_guider_params=self.params.audio_guider_params, images=images)
|
|
311
|
+
encode_video(video=out.video, fps=int(frame_rate), audio=out.audio, output_path=str(output_path),
|
|
312
|
+
video_chunks_number=get_video_chunks_number(out.num_frames, out.tiling_config))
|
|
313
|
+
self.audio_written = out.audio is not None
|
|
314
|
+
finally:
|
|
315
|
+
if tmp:
|
|
316
|
+
os.unlink(tmp)
|