File size: 7,238 Bytes
d61da82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
"""LTX-2.3 Gemma text-encoder backend (API Space for the Pre-Production Playground).

Runs Gemma-3-12B + the LTX-2.3 embeddings connectors and returns final
EmbeddingsProcessorOutput tensors (video/audio encodings + attention mask) as a .pt file,
for positive and (optionally) negative prompts — so the generation backends don't need
to hold Gemma in VRAM. Mirrors helpers.encode_prompts at the pinned LTX-2 commit.
Adapted from linoyts/gemma-text-encoder (LTX-2) to LTX-2.3.
"""

import os
import subprocess
import sys

os.environ["TORCH_COMPILE_DISABLE"] = "1"
os.environ["TORCHDYNAMO_DISABLE"] = "1"

LTX_REPO_URL = "https://github.com/Lightricks/LTX-2.git"
LTX_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LTX-2")
LTX_COMMIT_SHA = os.environ.get("LTX_COMMIT_SHA", "ae855f8538843825f9015a419cf4ba5edaf5eec2")

if not os.path.exists(LTX_REPO_DIR):
    os.makedirs(LTX_REPO_DIR)
    subprocess.run(["git", "init", LTX_REPO_DIR], check=True)
    subprocess.run(["git", "remote", "add", "origin", LTX_REPO_URL], cwd=LTX_REPO_DIR, check=True)
    subprocess.run(["git", "fetch", "--depth", "1", "origin", LTX_COMMIT_SHA], cwd=LTX_REPO_DIR, check=True)
    subprocess.run(["git", "checkout", LTX_COMMIT_SHA], cwd=LTX_REPO_DIR, check=True)

subprocess.run(
    [sys.executable, "-m", "pip", "install", "--force-reinstall", "--no-deps", "-e",
     os.path.join(LTX_REPO_DIR, "packages", "ltx-core"),
     "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines")],
    check=True,
)
sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines", "src"))
sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-core", "src"))

import json
import struct
import tempfile
import time

import torch
torch._dynamo.config.suppress_errors = True
torch._dynamo.config.disable = True

import gradio as gr
import spaces
from huggingface_hub import hf_hub_download, snapshot_download

from ltx_pipelines.utils.helpers import generate_enhanced_prompt
from ltx_pipelines.utils.model_ledger import ModelLedger

# Chunked-read safetensors loader: safe_open mmap deadlocks on FUSE-backed storage.
from ltx_core.loader.primitives import StateDict
from ltx_core.loader.sft_loader import SafetensorsStateDictLoader

_SAFETENSORS_DTYPE_MAP = {
    "F64": torch.float64, "F32": torch.float32, "F16": torch.float16,
    "BF16": torch.bfloat16, "F8_E5M2": torch.float8_e5m2, "F8_E4M3": torch.float8_e4m3fn,
    "I64": torch.int64, "I32": torch.int32, "I16": torch.int16, "I8": torch.int8,
    "U8": torch.uint8, "BOOL": torch.bool,
}


def _patched_load(self, path, sd_ops, device=None):
    sd, size, dtype = {}, 0, set()
    device = device or torch.device("cpu")
    for shard_path in (path if isinstance(path, list) else [path]):
        with open(shard_path, "rb") as f:
            header_len = struct.unpack("<Q", f.read(8))[0]
            header = json.loads(f.read(header_len).decode("utf-8"))
            data_base = 8 + header_len
            for name, meta in header.items():
                if name == "__metadata__":
                    continue
                expected_name = name if sd_ops is None else sd_ops.apply_to_key(name)
                if expected_name is None:
                    continue
                start, end = meta["data_offsets"]
                f.seek(data_base + start)
                buf = f.read(end - start)
                t = torch.frombuffer(bytearray(buf), dtype=_SAFETENSORS_DTYPE_MAP[meta["dtype"]]
                                     ).reshape(meta["shape"])
                t = t.to(device=device, non_blocking=True, copy=False)
                kvs = (((expected_name, t),) if sd_ops is None
                       else sd_ops.apply_to_key_value(expected_name, t))
                for key, v in kvs:
                    size += v.nbytes
                    dtype.add(v.dtype)
                    sd[key] = v
    return StateDict(sd=sd, device=device, size=size, dtype=dtype)


SafetensorsStateDictLoader.load = _patched_load

LTX_REPO = os.environ.get("LTX_REPO", "Lightricks/LTX-2.3")
CHECKPOINT_FILE = "ltx-2.3-22b-distilled-1.1.safetensors"  # connector weights only
GEMMA_REPO = "google/gemma-3-12b-it-qat-q4_0-unquantized"
TOKEN = os.environ.get("HF_TOKEN")

print("Downloading checkpoint (connectors) + Gemma…")
checkpoint_path = hf_hub_download(LTX_REPO, CHECKPOINT_FILE, token=TOKEN)
gemma_root = snapshot_download(GEMMA_REPO, token=TOKEN)

ledger = ModelLedger(
    dtype=torch.bfloat16,
    device="cuda",
    checkpoint_path=checkpoint_path,
    gemma_root_path=gemma_root,
)

print("Preloading text encoder + embeddings processor…")
_text_encoder = ledger.text_encoder()
_embeddings_processor = ledger.gemma_embeddings_processor()
print("Encoder ready.")


def _pack(output):
    return {"video": output.video_encoding.cpu(),
            "audio": output.audio_encoding.cpu() if output.audio_encoding is not None else None,
            "mask": output.attention_mask.cpu()}


@spaces.GPU(duration=60)
@torch.inference_mode()
def encode(
    prompt: str,
    negative_prompt: str = "",
    encode_negative: bool = False,
    enhance_prompt: bool = True,
    seed: int = 42,
):
    """Encode prompt (and optionally negative prompt) → .pt with final LTX-2.3 embeddings.

    Returns (embeddings_file, final_prompt, status).
    """
    start = time.time()
    final_prompt = prompt
    if enhance_prompt:
        final_prompt = generate_enhanced_prompt(_text_encoder, prompt, None, seed=int(seed))

    prompts = [final_prompt] + ([negative_prompt or ""] if encode_negative else [])
    raw = [_text_encoder.encode(p) for p in prompts]
    outputs = [_embeddings_processor.process_hidden_states(hs, mask) for hs, mask in raw]

    data = {"positive": _pack(outputs[0]),
            "negative": _pack(outputs[1]) if encode_negative else None,
            "final_prompt": final_prompt}
    out_path = tempfile.mktemp(suffix=".pt")
    torch.save(data, out_path)

    status = f"encoded in {time.time() - start:.1f}s"
    return out_path, final_prompt, status


with gr.Blocks(title="LTX-2.3 Gemma encoder API") as demo:
    gr.Markdown("# 🧠 LTX-2.3 Gemma text encoder\n"
                "API backend for the LTX-2.3 Pre-Production Playground — returns final "
                "embeddings (video/audio encodings + mask) for positive and negative prompts.")
    with gr.Row():
        with gr.Column():
            prompt = gr.Textbox(label="Prompt", lines=4)
            negative_prompt = gr.Textbox(label="Negative prompt", lines=2)
            encode_negative = gr.Checkbox(label="Encode negative (CFG / two-stage)", value=False)
            enhance_prompt = gr.Checkbox(label="Enhance prompt", value=True)
            seed = gr.Number(label="Enhancement seed", value=42, precision=0)
            btn = gr.Button("Encode", variant="primary")
        with gr.Column():
            emb_file = gr.File(label="Embeddings (.pt)")
            final_prompt = gr.Textbox(label="Final prompt used", lines=4)
            status = gr.Textbox(label="Status")

    btn.click(encode, [prompt, negative_prompt, encode_negative, enhance_prompt, seed],
              [emb_file, final_prompt, status], api_name="encode")

if __name__ == "__main__":
    demo.launch()