File size: 8,809 Bytes
acf4896
 
 
 
 
e451b04
acf4896
 
 
 
 
 
 
e451b04
acf4896
 
 
 
 
 
 
 
 
e451b04
acf4896
 
ddb0915
acf4896
 
 
 
 
 
 
 
 
 
 
 
e451b04
 
 
 
f46f8d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e451b04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a418dac
 
ddb0915
 
 
 
 
 
 
acf4896
 
4bdc016
acf4896
 
 
 
f46f8d8
 
 
acf4896
e451b04
acf4896
 
 
 
 
 
 
f46f8d8
acf4896
e451b04
a418dac
acf4896
ddb0915
 
 
 
 
e451b04
ddb0915
acf4896
ddb0915
 
 
acf4896
ddb0915
acf4896
 
ddb0915
acf4896
 
 
e451b04
acf4896
e451b04
f46f8d8
0cb6be2
 
 
acf4896
 
 
f46f8d8
 
 
 
 
 
 
 
ddb0915
 
f46f8d8
 
acf4896
 
 
 
 
 
 
 
 
f46f8d8
 
acf4896
9047bce
 
f46f8d8
e451b04
f46f8d8
ddb0915
f46f8d8
e451b04
f46f8d8
ddb0915
f46f8d8
e451b04
f46f8d8
ddb0915
9047bce
f46f8d8
ddb0915
9047bce
 
acf4896
062bd15
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
179
180
181
182
183
184
import os

os.environ.setdefault("TORCH_COMPILE_DISABLE", "1")
os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")

import math
import random
import tempfile

import numpy as np
import spaces
import torch
import gradio as gr
from PIL import Image, ImageOps
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file

from diffusers import LTX2InContextPipeline
from diffusers.pipelines.ltx2.pipeline_ltx2_ic_lora import LTX2ReferenceCondition
from diffusers.pipelines.ltx2.utils import DISTILLED_SIGMA_VALUES
from diffusers.utils import encode_video

# --- Config -----------------------------------------------------------------
# FAST distilled variant of the ingredients (reference-sheet) IC-LoRA: 8-step schedule, CFG off.
BASE_MODEL = "diffusers/LTX-2.3-Distilled-Diffusers"
LORA_REPO = "linoyts/LTX-2.3-loras"
LORA_FILE = "ltx-2.3-22b-ic-lora-ingredients-0.9"  # no .safetensors extension in the repo
LORA_SCALE = 1.4
FPS = 24
WIDTH, HEIGHT = 768, 448
NUM_FRAMES = 121
NUM_STEPS = len(DISTILLED_SIGMA_VALUES)  # 8
MAX_SEED = np.iinfo(np.int32).max
HF_TOKEN = os.environ.get("HF_TOKEN")

pipe = LTX2InContextPipeline.from_pretrained(BASE_MODEL, torch_dtype=torch.bfloat16)
pipe.to("cuda")
pipe.vae.enable_tiling()
_lora_path = hf_hub_download(LORA_REPO, LORA_FILE, token=HF_TOKEN)
pipe.load_lora_weights(load_file(_lora_path), adapter_name="ingredients")
pipe.set_adapters("ingredients", LORA_SCALE)


def _gallery_paths(gallery):
    paths = []
    for item in gallery or []:
        if isinstance(item, (list, tuple)):
            item = item[0]
        if isinstance(item, dict):
            item = item.get("path") or item.get("name") or item.get("image")
        if isinstance(item, str):
            paths.append(item)
    return paths


def compose_sheet(gallery):
    paths = _gallery_paths(gallery)
    if not paths:
        raise gr.Error("Upload at least one subject image to build a sheet.")
    imgs = [Image.open(p).convert("RGB") for p in paths]
    if len(imgs) == 1:
        return imgs[0]
    CW, CH = 1536, 896
    canvas = Image.new("RGB", (CW, CH), (0, 0, 0))
    n = len(imgs)
    cols = math.ceil(math.sqrt(n))
    rows = math.ceil(n / cols)
    g = 16
    cw = (CW - g * (cols + 1)) // cols
    ch = (CH - g * (rows + 1)) // rows
    for i, im in enumerate(imgs):
        r, c = divmod(i, cols)
        canvas.paste(ImageOps.fit(im, (cw, ch), Image.LANCZOS), (g + c * (cw + g), g + r * (ch + g)))
    return canvas


def _build_prompt(sheet, action):
    return f"Reference sheet: {sheet.strip()}\n\nGenerated video: {action.strip()}"


def _export(video_np, audio, path):
    kw = {}
    if audio is not None:
        kw = dict(audio=audio[0].float().cpu(), audio_sample_rate=pipe.vocoder.config.output_sampling_rate)
    encode_video(video_np, fps=FPS, output_path=path, **kw)


def _duration(*args, **kwargs):
    return 200


@spaces.GPU(duration=_duration)
def generate(sheet_image, sheet, action, lora_scale, seed, randomize, progress=gr.Progress()):
    if sheet_image is None:
        raise gr.Error("Add a reference sheet (upload one, or build one from subject images in the other tab).")
    if not sheet.strip():
        raise gr.Error("Describe the elements in the reference sheet (characters, props, location).")
    if not action.strip():
        raise gr.Error("Describe the action / shot you want generated.")

    if randomize:
        seed = random.randint(0, MAX_SEED)
    seed = int(seed)

    sheet_img = sheet_image.convert("RGB").resize((WIDTH, HEIGHT), Image.LANCZOS)
    ref = [sheet_img] * NUM_FRAMES
    pipe.set_adapters("ingredients", float(lora_scale))
    prompt = _build_prompt(sheet, action)

    def _cb(p, i, t, kw):
        progress((i + 1) / NUM_STEPS, desc=f"Generating — step {i + 1}/{NUM_STEPS}")
        return {}

    video_out, audio_out = pipe(
        prompt=prompt, negative_prompt="",
        reference_conditions=[LTX2ReferenceCondition(frames=ref, strength=1.0)],
        reference_downscale_factor=1,
        width=WIDTH, height=HEIGHT, num_frames=NUM_FRAMES, frame_rate=FPS,
        num_inference_steps=NUM_STEPS, sigmas=DISTILLED_SIGMA_VALUES,
        guidance_scale=1.0, stg_scale=0.0, audio_guidance_scale=1.0, audio_stg_scale=0.0,
        generator=torch.Generator(device="cuda").manual_seed(seed),
        output_type="np", return_dict=False, callback_on_step_end=_cb,
    )
    out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
    _export(video_out[0], audio_out, out_path)
    return out_path, seed


with gr.Blocks(title="LTX-2.3 Ingredients (Fast)") as demo:
    gr.Markdown(
        "# ⚡ LTX-2.3 Ingredients — Fast (Distilled)\n"
        "Reference-sheet control, fast. Upload a ready sheet, or build one from individual subject images. Using "
        "[LTX 2.3 Distilled](https://huggingface.co/diffusers/LTX-2.3-Distilled-Diffusers) with the "
        "[Ingredients IC-LoRA](https://huggingface.co/ltx-community/LTX-2.3-loras), via diffusers 🧨. "
        "(For maximum fidelity, see the Dev demo.)"
    )
    with gr.Row():
        with gr.Column():
            with gr.Tabs():
                with gr.Tab("Reference sheet"):
                    sheet_image = gr.Image(type="pil", label="Reference sheet (composite of characters / props / location)")
                with gr.Tab("Build from subject images"):
                    gallery = gr.Gallery(label="Upload subject images (characters, props, location)",
                                         type="filepath", interactive=True, columns=4, height=240)
                    build_btn = gr.Button("Build reference sheet ➜")
                    gr.Markdown("*Tiles your images into one sheet and loads it into the **Reference sheet** tab.*")
            sheet = gr.Textbox(label="Reference sheet description", lines=3,
                               placeholder="a young woman with red hair in a green jacket (face close-up + turnaround); a brass pocket watch; a cobblestone alley at night")
            action = gr.Textbox(label="Generated video — action / shot, plus any speech & sounds", lines=3,
                                placeholder="the woman walks down the alley, checks the pocket watch and whispers 'almost time'; footsteps on cobblestone, distant city hum")
            with gr.Accordion("Settings", open=False):
                lora_scale = gr.Slider(0.8, 1.8, value=1.4, step=0.05, label="LoRA strength")
                randomize = gr.Checkbox(True, label="Randomize seed")
                seed = gr.Slider(0, MAX_SEED, value=42, step=1, label="Seed")
            run = gr.Button("Generate (fast)", variant="primary")
        with gr.Column():
            video_out = gr.Video(label="Generated video")
            used_seed = gr.Number(label="Seed used", interactive=False)

    build_btn.click(compose_sheet, inputs=gallery, outputs=sheet_image)
    run.click(generate, inputs=[sheet_image, sheet, action, lora_scale, seed, randomize], outputs=[video_out, used_seed])

    gr.Examples(
        examples=[
            ["examples/sheet_garden.png",
             "a cartoon hedgehog (face close-up and body turnaround) and a cartoon rabbit (turnaround); a green coiled garden hose reel and green spray bottles; the interior of a 'Greenfield Home & Garden' store with shelves of plants",
             "the hedgehog waddles up to the camera in the Greenfield Home & Garden store and says cheerfully 'Welcome to Greenfield!', while the rabbit hops past holding a green spray bottle; upbeat store music, the hedgehog's friendly voice and soft footsteps",
             1.4, 42, False],
            ["examples/sheet_hiker.png",
             "a young woman hiker in a green shirt and khaki shorts (face close-up and body turnaround); a large blue hiking backpack; a wooden walking stick; a shaggy yak with a colorful woven saddle blanket; a Himalayan stone village with prayer flags and snowy mountains",
             "the woman loads the blue backpack onto the yak, pats its neck and says warmly 'almost at the summit, buddy', snowy peaks and a monastery behind her; wind, distant prayer bells, her voice and the yak's low grunt",
             1.4, 42, False],
            ["examples/subj_composite.png",
             "a smiling young woman with curly dark hair; a dappled grey horse; a green misty mountain meadow",
             "the woman walks up to the grey horse in the misty meadow, gently strokes its neck and says softly 'good boy, easy now'; a soft horse nicker, her gentle voice, light wind and birdsong",
             1.4, 42, False],
        ],
        inputs=[sheet_image, sheet, action, lora_scale, seed, randomize],
        outputs=[video_out, used_seed], fn=generate, cache_examples=True, cache_mode="lazy",
    )

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