Quantumbraid commited on
Commit
dc2004d
·
verified ·
1 Parent(s): 5b9aca8

Restore original app; fix bucket path to /data/v1/model/...

Browse files
Files changed (1) hide show
  1. app.py +73 -480
app.py CHANGED
@@ -1,495 +1,88 @@
1
- """PinkCherry LTX 2.3 — Gradio Space for Quantumbraid/grok."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import logging
7
- import os
8
- import random
9
- import struct
10
- import subprocess
11
- import sys
12
- import tempfile
13
- from pathlib import Path
14
-
15
- # Disable torch.compile before any torch import.
16
- os.environ.setdefault("TORCH_COMPILE_DISABLE", "1")
17
- os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")
18
- os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
19
-
20
- # ---------------------------------------------------------------------------
21
- # Install LTX-2 packages (ltx-core + ltx-pipelines)
22
- # ---------------------------------------------------------------------------
23
-
24
- LTX_REPO_URL = "https://github.com/Lightricks/LTX-2.git"
25
- LTX_COMMIT_SHA = "780984275fd47128b02bef9b5c085404276866ee" # main — has OffloadMode + blocks API
26
- LTX_REPO_DIR = Path(__file__).resolve().parent / "LTX-2"
27
-
28
-
29
- def _ensure_ltx_repo() -> None:
30
- import shutil
31
-
32
- if LTX_REPO_DIR.exists():
33
- head = subprocess.run(
34
- ["git", "-C", str(LTX_REPO_DIR), "rev-parse", "HEAD"],
35
- capture_output=True,
36
- text=True,
37
- check=False,
38
- )
39
- if head.returncode == 0 and head.stdout.strip() == LTX_COMMIT_SHA:
40
- return
41
- shutil.rmtree(LTX_REPO_DIR, ignore_errors=True)
42
-
43
- print(f"Cloning {LTX_REPO_URL} @ {LTX_COMMIT_SHA[:8]}...")
44
- LTX_REPO_DIR.mkdir(parents=True, exist_ok=True)
45
- subprocess.run(["git", "init", str(LTX_REPO_DIR)], check=True)
46
- subprocess.run(["git", "-C", str(LTX_REPO_DIR), "remote", "add", "origin", LTX_REPO_URL], check=True)
47
- subprocess.run(
48
- ["git", "-C", str(LTX_REPO_DIR), "fetch", "--depth", "1", "origin", LTX_COMMIT_SHA],
49
- check=True,
50
- )
51
- subprocess.run(["git", "-C", str(LTX_REPO_DIR), "checkout", LTX_COMMIT_SHA], check=True)
52
-
53
-
54
- _ensure_ltx_repo()
55
-
56
- print("Installing ltx-core and ltx-pipelines...")
57
- subprocess.run(
58
- [
59
- sys.executable,
60
- "-m",
61
- "pip",
62
- "install",
63
- "--quiet",
64
- "--no-deps",
65
- "-e",
66
- str(LTX_REPO_DIR / "packages" / "ltx-core"),
67
- "-e",
68
- str(LTX_REPO_DIR / "packages" / "ltx-pipelines"),
69
- ],
70
- check=True,
71
- )
72
-
73
- sys.path.insert(0, str(LTX_REPO_DIR / "packages" / "ltx-pipelines" / "src"))
74
- sys.path.insert(0, str(LTX_REPO_DIR / "packages" / "ltx-core" / "src"))
75
-
76
  import gradio as gr
77
- import numpy as np
78
- import spaces
79
  import torch
80
- from huggingface_hub import hf_hub_download, snapshot_download
81
-
82
- from ltx_core.loader import LTXV_LORA_COMFY_RENAMING_MAP, LoraPathStrengthAndSDOps
83
- from ltx_core.loader.primitives import StateDict
84
- from ltx_core.loader.sft_loader import SafetensorsStateDictLoader
85
- from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number
86
- from ltx_core.quantization.fp8_cast import build_policy as build_fp8_cast_policy
87
- from ltx_pipelines.ti2vid_two_stages import TI2VidTwoStagesPipeline
88
- from ltx_pipelines.utils.args import ImageConditioningInput
89
- from ltx_pipelines.utils.constants import (
90
- DEFAULT_NEGATIVE_PROMPT,
91
- LTX_2_3_PARAMS,
92
- detect_params,
93
- )
94
- from ltx_pipelines.utils.media_io import encode_video
95
- from ltx_pipelines.utils.types import OffloadMode
96
-
97
- logging.basicConfig(level=logging.INFO)
98
- logger = logging.getLogger(__name__)
99
-
100
- # ---------------------------------------------------------------------------
101
- # Bucket paths (mounted at /data)
102
- # ---------------------------------------------------------------------------
103
-
104
- DATA_ROOT = Path(os.environ.get("LTX_DATA_ROOT", "/data"))
105
- CHECKPOINT_PATH = DATA_ROOT / "v1/model/SexGod_PinkCherry_dev_bf16_LTX23_v1.safetensors"
106
- DISTILLED_LORA_PATH = (
107
- DATA_ROOT / "v1/distil_lora/ltx-2.3-22b-distilled-lora-1.1_fro90_ceil72_condsafe.safetensors"
108
  )
109
- WORKFLOW_PATH = DATA_ROOT / "v1/PInkCherry_LTX23_NSFW_Workflow.json"
110
- DISTILLED_LORA_STRENGTH = 0.6 # from bundled ComfyUI workflow
111
-
112
- CACHE_DIR = Path(os.environ.get("LTX_CACHE_DIR", str(Path.home() / ".cache" / "grok-ltx")))
113
- CACHE_DIR.mkdir(parents=True, exist_ok=True)
114
-
115
- SPATIAL_UPSAMPLER_PATH = CACHE_DIR / "ltx-2.3-spatial-upscaler-x2-1.1.safetensors"
116
- GEMMA_ROOT = CACHE_DIR / "gemma-3-12b-it"
117
-
118
- FRAME_RATE = 24.0
119
- MAX_SEED = np.iinfo(np.int32).max
120
- TOKEN_ENV_NAMES = ("HF_TOKEN", "HUGGINGFACE_HUB_TOKEN", "HUGGING_FACE_HUB_TOKEN", "HUGGINGFACE_TOKEN")
121
- BUCKET_ID = "Quantumbraid/PinkCherry_NSFW_LTX23-bucket"
122
-
123
- RESOLUTION_PRESETS = {
124
- "16:9 low (768×512)": (768, 512),
125
- "16:9 medium (1024×576)": (1024, 576),
126
- "16:9 high (1536×864)": (1536, 864),
127
- "9:16 low (512×768)": (512, 768),
128
- "9:16 medium (576×1024)": (576, 1024),
129
- "9:16 high (864×1536)": (864, 1536),
130
- "1:1 low (768×768)": (768, 768),
131
- }
132
-
133
- # ---------------------------------------------------------------------------
134
- # FUSE-safe safetensors loader (bucket mounts can deadlock on mmap)
135
- # ---------------------------------------------------------------------------
136
-
137
- _SAFETENSORS_DTYPE_MAP = {
138
- "F64": torch.float64,
139
- "F32": torch.float32,
140
- "F16": torch.float16,
141
- "BF16": torch.bfloat16,
142
- "F8_E5M2": torch.float8_e5m2,
143
- "F8_E4M3": torch.float8_e4m3fn,
144
- "I64": torch.int64,
145
- "I32": torch.int32,
146
- "I16": torch.int16,
147
- "I8": torch.int8,
148
- "U8": torch.uint8,
149
- "BOOL": torch.bool,
150
- }
151
-
152
-
153
- def _patched_safetensors_load(self, path, sd_ops, device=None):
154
- sd = {}
155
- size = 0
156
- dtype = set()
157
- device = device or torch.device("cpu")
158
- model_paths = path if isinstance(path, list) else [path]
159
- for shard_path in model_paths:
160
- with open(shard_path, "rb") as f:
161
- header_len = struct.unpack("<Q", f.read(8))[0]
162
- header = json.loads(f.read(header_len).decode("utf-8"))
163
- data_base = 8 + header_len
164
- for name, meta in header.items():
165
- if name == "__metadata__":
166
- continue
167
- expected_name = name if sd_ops is None else sd_ops.apply_to_key(name)
168
- if expected_name is None:
169
- continue
170
- start, end = meta["data_offsets"]
171
- f.seek(data_base + start)
172
- buf = f.read(end - start)
173
- t = torch.frombuffer(
174
- bytearray(buf), dtype=_SAFETENSORS_DTYPE_MAP[meta["dtype"]]
175
- ).reshape(meta["shape"])
176
- t = t.to(device=device, non_blocking=True, copy=False)
177
- kvs = (
178
- ((expected_name, t),)
179
- if sd_ops is None
180
- else sd_ops.apply_to_key_value(expected_name, t)
181
- )
182
- for key, v in kvs:
183
- size += v.nbytes
184
- dtype.add(v.dtype)
185
- sd[key] = v
186
- return StateDict(sd=sd, device=device, size=size, dtype=dtype)
187
-
188
-
189
- SafetensorsStateDictLoader.load = _patched_safetensors_load
190
-
191
-
192
- # ---------------------------------------------------------------------------
193
- # xformers attention patch
194
- # ---------------------------------------------------------------------------
195
-
196
- from ltx_core.model.transformer import attention as _attn_mod
197
-
198
- try:
199
- from xformers.ops import memory_efficient_attention as _mea
200
-
201
- _attn_mod.memory_efficient_attention = _mea
202
- try:
203
- from xformers.ops.fmha import _set_use_fa3
204
-
205
- _set_use_fa3(False)
206
- except Exception:
207
- pass
208
- except Exception as exc:
209
- logger.warning("xformers patch skipped: %s", exc)
210
-
211
- torch._dynamo.config.suppress_errors = True
212
- torch._dynamo.config.disable = True
213
-
214
-
215
- # ---------------------------------------------------------------------------
216
- # Asset verification + hub downloads
217
- # ---------------------------------------------------------------------------
218
-
219
-
220
- def _get_hf_token() -> str | None:
221
- for name in TOKEN_ENV_NAMES:
222
- token = os.environ.get(name)
223
- if token and token.strip():
224
- return token.strip()
225
- return None
226
-
227
 
228
- def _token_status() -> str:
229
- present = [name for name in TOKEN_ENV_NAMES if os.environ.get(name)]
230
- return present[0] if present else "none"
231
-
232
-
233
- def _describe_data_root() -> str:
234
- if not DATA_ROOT.exists():
235
- return f"{DATA_ROOT} does not exist (bucket volume not mounted?)"
236
- try:
237
- entries = sorted(p.name for p in DATA_ROOT.iterdir())
238
- except OSError as exc:
239
- return f"{DATA_ROOT} exists but is not readable: {exc}"
240
- if not entries:
241
- return f"{DATA_ROOT} is empty"
242
- preview = ", ".join(entries[:12])
243
- suffix = " ..." if len(entries) > 12 else ""
244
- return f"{DATA_ROOT} contains: {preview}{suffix}"
245
-
246
-
247
- def _require_bucket_files() -> None:
248
- missing = [p for p in (CHECKPOINT_PATH, DISTILLED_LORA_PATH) if not p.exists()]
249
- if missing:
250
- token_hint = (
251
- f"A Space secret named HF_TOKEN is set ({_token_status()})."
252
- if _get_hf_token()
253
- else (
254
- "No HF token secret found. Add a Space secret named HF_TOKEN with a read token "
255
- f"from the account that owns {BUCKET_ID}, then restart the Space."
256
- )
257
- )
258
- raise FileNotFoundError(
259
- "Missing bucket files. Mount the bucket at /data and verify the v1/ layout:\n"
260
- + "\n".join(f" - {p}" for p in missing)
261
- + f"\n\nBucket: {BUCKET_ID}\n"
262
- + f"Mount: hf spaces volumes set Quantumbraid/tst "
263
- + f"-v hf://buckets/{BUCKET_ID}:/data\n"
264
- + f"Diagnostics: {_describe_data_root()}\n"
265
- + token_hint
266
- )
267
-
268
-
269
- def _ensure_hub_assets() -> None:
270
- token = _get_hf_token()
271
- if token is None:
272
- logger.warning(
273
- "No HF token secret detected (%s). Gated Hub downloads may fail.",
274
- _token_status(),
275
- )
276
-
277
- if not SPATIAL_UPSAMPLER_PATH.exists():
278
- logger.info("Downloading spatial upsampler...")
279
- hf_hub_download(
280
- repo_id="Lightricks/LTX-2.3",
281
- filename="ltx-2.3-spatial-upscaler-x2-1.1.safetensors",
282
- token=token,
283
- local_dir=str(CACHE_DIR),
284
- )
285
-
286
- if not GEMMA_ROOT.exists() or not any(GEMMA_ROOT.rglob("model*.safetensors")):
287
- logger.info("Downloading Gemma 3 text encoder...")
288
- snapshot_download(
289
- "google/gemma-3-12b-it-qat-q4_0-unquantized",
290
- token=token,
291
- local_dir=str(GEMMA_ROOT),
292
- )
293
-
294
-
295
- _require_bucket_files()
296
- _ensure_hub_assets()
297
-
298
- PIPELINE_PARAMS = detect_params(str(CHECKPOINT_PATH))
299
-
300
- distilled_lora = [
301
- LoraPathStrengthAndSDOps(
302
- str(DISTILLED_LORA_PATH),
303
- DISTILLED_LORA_STRENGTH,
304
- LTXV_LORA_COMFY_RENAMING_MAP,
305
  )
306
- ]
307
 
308
- logger.info("Loading TI2VidTwoStagesPipeline (FP8 + CPU offload)...")
309
- pipeline = TI2VidTwoStagesPipeline(
310
- checkpoint_path=str(CHECKPOINT_PATH),
311
- distilled_lora=distilled_lora,
312
- spatial_upsampler_path=str(SPATIAL_UPSAMPLER_PATH),
313
- gemma_root=str(GEMMA_ROOT),
314
- loras=[],
315
- quantization=build_fp8_cast_policy(str(CHECKPOINT_PATH)),
316
- offload_mode=OffloadMode.CPU,
317
  )
318
- logger.info("Pipeline ready.")
319
-
320
-
321
- # ---------------------------------------------------------------------------
322
- # Helpers
323
- # ---------------------------------------------------------------------------
324
-
325
-
326
- def _snap_frames(duration: float, fps: float = FRAME_RATE) -> int:
327
- raw = int(duration * fps) + 1
328
- k = max(0, (raw - 1 + 7) // 8)
329
- return 8 * k + 1
330
-
331
-
332
- def _resolution_from_preset(preset: str) -> tuple[int, int]:
333
- return RESOLUTION_PRESETS.get(preset, (768, 512))
334
-
335
-
336
- @spaces.GPU(duration=120)
337
- @torch.inference_mode()
338
- def generate(
339
- prompt: str,
340
- negative_prompt: str,
341
- input_image,
342
- duration: float,
343
- resolution_preset: str,
344
- num_inference_steps: int,
345
- enhance_prompt: bool,
346
- seed: int,
347
- randomize_seed: bool,
348
- progress=gr.Progress(track_tqdm=True),
349
- ):
350
- if not prompt.strip():
351
- raise gr.Error("Please enter a prompt.")
352
-
353
- current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
354
- width, height = _resolution_from_preset(resolution_preset)
355
- num_frames = _snap_frames(duration)
356
-
357
- images: list[ImageConditioningInput] = []
358
- if input_image is not None:
359
- temp_dir = Path("inputs")
360
- temp_dir.mkdir(exist_ok=True)
361
- image_path = temp_dir / f"frame_{current_seed}.jpg"
362
- if hasattr(input_image, "save"):
363
- input_image.save(image_path)
364
- else:
365
- image_path = Path(input_image)
366
- images = [ImageConditioningInput(path=str(image_path), frame_idx=0, strength=1.0)]
367
-
368
- logger.info(
369
- "Generating %dx%d, %d frames (%.1fs), seed=%d, steps=%d",
370
- width,
371
- height,
372
- num_frames,
373
- duration,
374
- current_seed,
375
- num_inference_steps,
376
- )
377
-
378
- tiling_config = TilingConfig.default()
379
- video_chunks_number = get_video_chunks_number(num_frames, tiling_config)
380
-
381
- video, audio = pipeline(
382
  prompt=prompt,
383
- negative_prompt=negative_prompt or DEFAULT_NEGATIVE_PROMPT,
384
- seed=current_seed,
385
- height=height,
386
- width=width,
387
  num_frames=num_frames,
388
- frame_rate=FRAME_RATE,
389
- num_inference_steps=num_inference_steps,
390
- video_guider_params=PIPELINE_PARAMS.video_guider_params,
391
- audio_guider_params=PIPELINE_PARAMS.audio_guider_params,
392
- images=images,
393
- tiling_config=tiling_config,
394
- enhance_prompt=enhance_prompt,
395
  )
396
-
397
- output_path = tempfile.mktemp(suffix=".mp4")
398
- encode_video(
399
- video=video,
400
- fps=FRAME_RATE,
401
- audio=audio,
402
- output_path=output_path,
403
- video_chunks_number=video_chunks_number,
404
- )
405
- return output_path, current_seed
406
-
407
-
408
- # ---------------------------------------------------------------------------
409
- # Gradio UI
410
- # ---------------------------------------------------------------------------
411
-
412
- DEFAULT_PROMPT = (
413
- "Cinematic close-up, natural warm lighting, smooth camera motion, "
414
- "detailed textures, synchronized ambient audio."
 
 
 
415
  )
416
 
417
- with gr.Blocks(title="Grok LTX 2.3") as demo:
418
- gr.Markdown(
419
- "# Grok — PinkCherry LTX 2.3\n"
420
- "Text-to-video and image-to-video with synchronized audio. "
421
- "Checkpoint and distilled LoRA load from the mounted `/data` bucket."
422
- )
423
-
424
- with gr.Row():
425
- with gr.Column():
426
- input_image = gr.Image(label="First frame (optional, image-to-video)", type="pil")
427
- prompt = gr.Textbox(
428
- label="Prompt",
429
- lines=4,
430
- value=DEFAULT_PROMPT,
431
- placeholder="Describe scene, motion, camera, lighting, and audio...",
432
- )
433
- negative_prompt = gr.Textbox(
434
- label="Negative prompt",
435
- lines=2,
436
- value=DEFAULT_NEGATIVE_PROMPT,
437
- )
438
-
439
- with gr.Row():
440
- duration = gr.Slider(
441
- label="Duration (seconds)",
442
- minimum=2.0,
443
- maximum=10.0,
444
- value=5.0,
445
- step=0.5,
446
- )
447
- resolution_preset = gr.Dropdown(
448
- label="Resolution",
449
- choices=list(RESOLUTION_PRESETS.keys()),
450
- value="16:9 low (768×512)",
451
- )
452
-
453
- with gr.Row():
454
- num_inference_steps = gr.Slider(
455
- label="Inference steps (stage 1)",
456
- minimum=8,
457
- maximum=40,
458
- value=PIPELINE_PARAMS.num_inference_steps,
459
- step=1,
460
- )
461
- enhance_prompt = gr.Checkbox(label="Enhance prompt", value=False)
462
-
463
- with gr.Accordion("Advanced", open=False):
464
- seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, value=42, step=1)
465
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
466
-
467
- generate_btn = gr.Button("Generate video", variant="primary")
468
-
469
- with gr.Column():
470
- output_video = gr.Video(label="Output", autoplay=True)
471
-
472
- gr.Markdown(
473
- f"**Bucket checkpoint:** `{CHECKPOINT_PATH.name}` \n"
474
- f"**Distilled LoRA:** `{DISTILLED_LORA_PATH.name}` @ strength {DISTILLED_LORA_STRENGTH} \n"
475
- f"**Workflow reference:** `{WORKFLOW_PATH.name}`"
476
- )
477
-
478
- generate_btn.click(
479
- fn=generate,
480
- inputs=[
481
- prompt,
482
- negative_prompt,
483
- input_image,
484
- duration,
485
- resolution_preset,
486
- num_inference_steps,
487
- enhance_prompt,
488
- seed,
489
- randomize_seed,
490
- ],
491
- outputs=[output_video, seed],
492
- )
493
 
494
  if __name__ == "__main__":
495
  demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ import os
 
3
  import torch
4
+ from diffusers import LTXPipeline # LTX video generation pipeline (correct class)
5
+
6
+ # -------------------------------------------------
7
+ # Load the model (from the attached bucket)
8
+ # -------------------------------------------------
9
+
10
+ # Mounted bucket path inside the Space container
11
+ DATA_ROOT = os.environ.get("LTX_DATA_ROOT", "/data")
12
+ model_path = os.path.join(
13
+ DATA_ROOT,
14
+ "v1",
15
+ "model",
16
+ "SexGod_PinkCherry_dev_bf16_LTX23_v1.safetensors",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ if not os.path.isfile(model_path):
20
+ raise FileNotFoundError(
21
+ f"Checkpoint not found at {model_path}. "
22
+ f"Ensure bucket Quantumbraid/PinkCherry_NSFW_LTX23-bucket is mounted at {DATA_ROOT}."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  )
 
24
 
25
+ # Initialise the LTX pipeline from the local checkpoint
26
+ pipe = LTXPipeline.from_pretrained(
27
+ model_path,
28
+ torch_dtype=torch.float16,
 
 
 
 
 
29
  )
30
+ pipe = pipe.to("cuda")
31
+
32
+ # -------------------------------------------------
33
+ # Gradio UI helper
34
+ # -------------------------------------------------
35
+ def generate_video(prompt: str, num_frames: int = 16, seed: int = 42):
36
+ """Generate a short video (as a GIF) from a text prompt."""
37
+ generator = torch.Generator("cuda").manual_seed(seed)
38
+
39
+ # Run the pipeline the return type can differ across versions,
40
+ # so we handle both the new attribute style and the older tuple style.
41
+ output = pipe(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  prompt=prompt,
 
 
 
 
43
  num_frames=num_frames,
44
+ generator=generator,
 
 
 
 
 
 
45
  )
46
+ # Newer versions return an object with a .videos attribute.
47
+ if hasattr(output, "videos"):
48
+ video = output.videos
49
+ else:
50
+ # Older versions return a tuple where the first element is the video tensor.
51
+ video = output[0]
52
+
53
+ # video shape: (B, T, C, H, W). We take the first batch element.
54
+ frames_tensor = video[0] # (T, C, H, W)
55
+ frames = [
56
+ frame.cpu().numpy().transpose(1, 2, 0) # convert to H×W×C for Gradio/GIF
57
+ for frame in frames_tensor
58
+ ]
59
+ return frames
60
+
61
+ # -------------------------------------------------
62
+ # Build the Gradio interface
63
+ # -------------------------------------------------
64
+ title = "💥 PinkCherry NSFW LTX 2.3 – Video Generation Demo"
65
+ description = (
66
+ "Enter a prompt (NSFW/uncensored) and generate a short video. "
67
+ "⚠️ **Not safe for work – use responsibly**."
68
  )
69
 
70
+ demo = gr.Interface(
71
+ fn=generate_video,
72
+ inputs=[
73
+ gr.Textbox(label="Prompt", placeholder="e.g. a futuristic city at night"),
74
+ gr.Slider(8, 32, step=4, label="Number of frames", value=16),
75
+ gr.Number(label="Seed (optional)", value=42),
76
+ ],
77
+ outputs=gr.Gallery(label="Generated video (GIF)"),
78
+ title=title,
79
+ description=description,
80
+ allow_flagging="never",
81
+ examples=[
82
+ ["a seductive dancer in a neon club", 16, 123],
83
+ ["a cyber-punk couple on a rooftop", 24, 456],
84
+ ],
85
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  if __name__ == "__main__":
88
  demo.launch()