linoyts HF Staff commited on
Commit
0be9309
·
verified ·
1 Parent(s): c474be0

fixes: natural-speed/aspect, audio, progress, prompts, examples

Browse files
app.py CHANGED
@@ -8,10 +8,11 @@ import random
8
  import tempfile
9
 
10
  import numpy as np
 
11
  import spaces
12
  import torch
13
  import gradio as gr
14
- from PIL import Image
15
  from huggingface_hub import hf_hub_download
16
  from safetensors.torch import load_file
17
 
@@ -30,10 +31,7 @@ NUM_STEPS = len(DISTILLED_SIGMA_VALUES) # 8-step distilled schedule
30
  MAX_SEED = np.iinfo(np.int32).max
31
  HF_TOKEN = os.environ.get("HF_TOKEN")
32
 
33
- RES_PRESETS = {
34
- "Fast (768×448)": (768, 448),
35
- "Quality (960×544)": (960, 544),
36
- }
37
  FRAME_CHOICES = [49, 73, 97, 121]
38
 
39
  # --- Load pipeline once at module scope (ZeroGPU registers it) ---------------
@@ -47,48 +45,70 @@ pipe.set_adapters("colorize", LORA_SCALE)
47
 
48
 
49
  # --- Helpers ----------------------------------------------------------------
50
- def _resample(frames, n):
51
- """Pick n evenly spaced frames (handles clips shorter or longer than n)."""
52
- idx = np.linspace(0, len(frames) - 1, n).round().astype(int)
53
- return [frames[i] for i in idx]
 
54
 
55
 
56
- def _to_grayscale(img: Image.Image) -> Image.Image:
57
- """Desaturate to a monochrome reference, kept as 3-channel RGB."""
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  return img.convert("L").convert("RGB")
59
 
60
 
61
- def _pick_resolution(first_frame: Image.Image, preset: str):
62
  w, h = RES_PRESETS[preset]
63
- # Match the input's orientation (the model saw both landscape and portrait).
64
  if first_frame.height > first_frame.width:
65
  w, h = h, w
66
  return w, h
67
 
68
 
69
- def _build_prompt(colors: str, scene: str) -> str:
70
  colors = colors.strip()
71
  scene = scene.strip() or "the same scene"
72
- return (
73
  f"Reference shows {scene}, rendered in high-contrast monochrome with soft natural daylight. "
74
  f"Edited shows the same scene with natural colors restored. "
75
  f"COLORIZE {colors}. "
76
  f"Subject identity, framing, and background geometry are identical to the reference; "
77
  f"only color information differs between reference and edited."
78
  )
 
 
 
 
 
 
 
 
 
 
79
 
80
 
81
  def _duration(*args, **kwargs):
82
- # args mirror the GPU fn: (video, colors, scene, preset, num_frames, seed, randomize, [progress])
83
- preset = args[3] if len(args) > 3 else "Fast"
84
- num_frames = args[4] if len(args) > 4 else 73
85
  per_frame = 1.6 if "Quality" in str(preset) else 1.0
86
- return int(50 + int(num_frames) * per_frame)
87
 
88
 
89
  # --- Inference --------------------------------------------------------------
90
  @spaces.GPU(duration=_duration)
91
- def colorize(video, colors, scene, preset, num_frames, seed, randomize,
92
  progress=gr.Progress(track_tqdm=True)):
93
  if video is None:
94
  raise gr.Error("Please upload a video to colorize.")
@@ -98,43 +118,34 @@ def colorize(video, colors, scene, preset, num_frames, seed, randomize,
98
  if randomize:
99
  seed = random.randint(0, MAX_SEED)
100
  seed = int(seed)
 
101
 
102
- frames = load_video(video)
103
- if not frames:
104
  raise gr.Error("Could not read any frames from that video.")
 
105
 
106
- width, height = _pick_resolution(frames[0], preset)
107
- num_frames = int(num_frames)
108
 
109
- # Desaturate + resample + resize -> the grayscale reference the LoRA expects.
110
- ref = [_to_grayscale(f).resize((width, height), Image.LANCZOS)
111
- for f in _resample(frames, num_frames)]
112
 
113
- prompt = _build_prompt(colors, scene)
114
- ref_cond = LTX2ReferenceCondition(frames=ref, strength=1.0)
115
-
116
- video_out, _audio = pipe(
117
  prompt=prompt,
118
  negative_prompt="",
119
- reference_conditions=[ref_cond],
120
  reference_downscale_factor=1,
121
- width=width,
122
- height=height,
123
- num_frames=num_frames,
124
- frame_rate=FPS,
125
- num_inference_steps=NUM_STEPS,
126
- sigmas=DISTILLED_SIGMA_VALUES,
127
- guidance_scale=1.0,
128
- stg_scale=0.0,
129
- audio_guidance_scale=1.0,
130
- audio_stg_scale=0.0,
131
  generator=torch.Generator(device="cuda").manual_seed(seed),
132
- output_type="np",
133
- return_dict=False,
134
  )
135
 
136
  out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
137
- encode_video(video_out[0], fps=FPS, output_path=out_path)
138
  return out_path, seed
139
 
140
 
@@ -142,25 +153,20 @@ def colorize(video, colors, scene, preset, num_frames, seed, randomize,
142
  with gr.Blocks(title="LTX-2.3 Colorize") as demo:
143
  gr.Markdown(
144
  "# 🎨 LTX-2.3 Video Colorization\n"
145
- "Restore natural color to black-and-white or desaturated footage. "
146
- "Upload a clip it's converted to grayscale and recolored while keeping "
147
- "subject, framing, and motion untouched. "
148
- "IC-LoRA: [`linoyts/LTX-2.3-loras`](https://huggingface.co/linoyts/LTX-2.3-loras) · "
149
- "base: distilled LTX-2.3."
150
  )
151
  with gr.Row():
152
  with gr.Column():
153
  video_in = gr.Video(label="Input video (any clip — recolored as B&W)")
154
- colors = gr.Textbox(
155
- label="Colors to restore",
156
- placeholder="a young brown rabbit with cream underside on grey granite rocks, warm afternoon light, green vegetation",
157
- lines=2,
158
- )
159
- scene = gr.Textbox(
160
- label="Scene description (optional)",
161
- placeholder="a small rabbit sitting among boulders with dry grass behind it",
162
- lines=2,
163
- )
164
  with gr.Accordion("Settings", open=False):
165
  preset = gr.Dropdown(list(RES_PRESETS), value="Fast (768×448)", label="Resolution")
166
  num_frames = gr.Dropdown(FRAME_CHOICES, value=73, label="Frames (24fps)")
@@ -171,38 +177,29 @@ with gr.Blocks(title="LTX-2.3 Colorize") as demo:
171
  video_out = gr.Video(label="Colorized result")
172
  used_seed = gr.Number(label="Seed used", interactive=False)
173
 
174
- run.click(
175
- colorize,
176
- inputs=[video_in, colors, scene, preset, num_frames, seed, randomize],
177
- outputs=[video_out, used_seed],
178
- )
179
 
180
  gr.Examples(
181
  examples=[
182
- [
183
- "examples/cat_on_a_tree_gray.mp4",
184
- "a tabby cat with brown and grey fur and green eyes, clinging to rough brown tree bark, surrounded by lush green leaves",
185
- "a cat climbing a tree among leaves",
186
- "Fast (768×448)", 49, 42, False,
187
- ],
188
- [
189
- "examples/surfing_gray.mp4",
190
- "a surfer in a black wetsuit on a turquoise ocean wave, white foam, bright blue sky",
191
- "a person surfing on an ocean wave",
192
- "Fast (768×448)", 49, 42, False,
193
- ],
194
- [
195
- "examples/slicing_veggie_gray.mp4",
196
- "hands slicing a bright red tomato on a wooden cutting board, fresh green herbs and orange carrots nearby, warm kitchen light",
197
- "hands slicing vegetables on a cutting board",
198
- "Fast (768×448)", 49, 42, False,
199
- ],
200
  ],
201
- inputs=[video_in, colors, scene, preset, num_frames, seed, randomize],
202
- outputs=[video_out, used_seed],
203
- fn=colorize,
204
- cache_examples=True,
205
- cache_mode="lazy",
206
  )
207
 
208
  if __name__ == "__main__":
 
8
  import tempfile
9
 
10
  import numpy as np
11
+ import imageio.v3 as iio
12
  import spaces
13
  import torch
14
  import gradio as gr
15
+ from PIL import Image, ImageOps
16
  from huggingface_hub import hf_hub_download
17
  from safetensors.torch import load_file
18
 
 
31
  MAX_SEED = np.iinfo(np.int32).max
32
  HF_TOKEN = os.environ.get("HF_TOKEN")
33
 
34
+ RES_PRESETS = {"Fast (768×448)": (768, 448), "Quality (960×544)": (960, 544)}
 
 
 
35
  FRAME_CHOICES = [49, 73, 97, 121]
36
 
37
  # --- Load pipeline once at module scope (ZeroGPU registers it) ---------------
 
45
 
46
 
47
  # --- Helpers ----------------------------------------------------------------
48
+ def _src_fps(path, default=FPS):
49
+ try:
50
+ return float(iio.immeta(path, plugin="pyav").get("fps", default)) or default
51
+ except Exception:
52
+ return default
53
 
54
 
55
+ def _load_frames(path, num_frames, width, height):
56
+ """Natural-speed (real-time at 24fps), aspect-preserving (center-crop) frames."""
57
+ frames = load_video(path)
58
+ if not frames:
59
+ return []
60
+ fps = _src_fps(path)
61
+ out = []
62
+ for i in range(num_frames):
63
+ idx = int(round(i / FPS * fps))
64
+ idx = min(idx, len(frames) - 1)
65
+ out.append(ImageOps.fit(frames[idx].convert("RGB"), (width, height), Image.LANCZOS))
66
+ return out
67
+
68
+
69
+ def _to_grayscale(img):
70
  return img.convert("L").convert("RGB")
71
 
72
 
73
+ def _pick_resolution(first_frame, preset):
74
  w, h = RES_PRESETS[preset]
 
75
  if first_frame.height > first_frame.width:
76
  w, h = h, w
77
  return w, h
78
 
79
 
80
+ def _build_prompt(colors, scene, audio):
81
  colors = colors.strip()
82
  scene = scene.strip() or "the same scene"
83
+ p = (
84
  f"Reference shows {scene}, rendered in high-contrast monochrome with soft natural daylight. "
85
  f"Edited shows the same scene with natural colors restored. "
86
  f"COLORIZE {colors}. "
87
  f"Subject identity, framing, and background geometry are identical to the reference; "
88
  f"only color information differs between reference and edited."
89
  )
90
+ if audio.strip():
91
+ p += f" Audio: {audio.strip()}."
92
+ return p
93
+
94
+
95
+ def _export(video_np, audio, path):
96
+ kw = {}
97
+ if audio is not None:
98
+ kw = dict(audio=audio[0].float().cpu(), audio_sample_rate=pipe.vocoder.config.output_sampling_rate)
99
+ encode_video(video_np, fps=FPS, output_path=path, **kw)
100
 
101
 
102
  def _duration(*args, **kwargs):
103
+ preset = next((a for a in args if a in RES_PRESETS), "Fast")
104
+ num_frames = next((a for a in args if a in FRAME_CHOICES), 73)
 
105
  per_frame = 1.6 if "Quality" in str(preset) else 1.0
106
+ return int(60 + int(num_frames) * per_frame)
107
 
108
 
109
  # --- Inference --------------------------------------------------------------
110
  @spaces.GPU(duration=_duration)
111
+ def colorize(video, colors, scene, audio, preset, num_frames, seed, randomize,
112
  progress=gr.Progress(track_tqdm=True)):
113
  if video is None:
114
  raise gr.Error("Please upload a video to colorize.")
 
118
  if randomize:
119
  seed = random.randint(0, MAX_SEED)
120
  seed = int(seed)
121
+ num_frames = int(num_frames)
122
 
123
+ probe = load_video(video)
124
+ if not probe:
125
  raise gr.Error("Could not read any frames from that video.")
126
+ width, height = _pick_resolution(probe[0], preset)
127
 
128
+ ref = [_to_grayscale(f) for f in _load_frames(video, num_frames, width, height)]
129
+ prompt = _build_prompt(colors, scene, audio)
130
 
131
+ def _cb(p, i, t, kw):
132
+ progress((i + 1) / NUM_STEPS, desc=f"Colorizing — step {i + 1}/{NUM_STEPS}")
133
+ return {}
134
 
135
+ video_out, audio_out = pipe(
 
 
 
136
  prompt=prompt,
137
  negative_prompt="",
138
+ reference_conditions=[LTX2ReferenceCondition(frames=ref, strength=1.0)],
139
  reference_downscale_factor=1,
140
+ width=width, height=height, num_frames=num_frames, frame_rate=FPS,
141
+ num_inference_steps=NUM_STEPS, sigmas=DISTILLED_SIGMA_VALUES,
142
+ guidance_scale=1.0, stg_scale=0.0, audio_guidance_scale=1.0, audio_stg_scale=0.0,
 
 
 
 
 
 
 
143
  generator=torch.Generator(device="cuda").manual_seed(seed),
144
+ output_type="np", return_dict=False, callback_on_step_end=_cb,
 
145
  )
146
 
147
  out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
148
+ _export(video_out[0], audio_out, out_path)
149
  return out_path, seed
150
 
151
 
 
153
  with gr.Blocks(title="LTX-2.3 Colorize") as demo:
154
  gr.Markdown(
155
  "# 🎨 LTX-2.3 Video Colorization\n"
156
+ "Restore natural color to black-and-white or desaturated footage. Upload a clip — it's converted "
157
+ "to grayscale and recolored while keeping subject, framing, and motion untouched. Optionally describe "
158
+ "the soundscape and the model generates matching audio. "
159
+ "IC-LoRA: [`linoyts/LTX-2.3-loras`](https://huggingface.co/linoyts/LTX-2.3-loras) · base: distilled LTX-2.3."
 
160
  )
161
  with gr.Row():
162
  with gr.Column():
163
  video_in = gr.Video(label="Input video (any clip — recolored as B&W)")
164
+ colors = gr.Textbox(label="Colors to restore", lines=2,
165
+ placeholder="a young brown rabbit with cream underside on grey granite rocks, warm afternoon light, green vegetation")
166
+ scene = gr.Textbox(label="Scene description (optional)", lines=2,
167
+ placeholder="a small rabbit sitting among boulders with dry grass behind it")
168
+ audio = gr.Textbox(label="Sound / audio (optional)", lines=1,
169
+ placeholder="gentle wind, distant birdsong, soft rustling of dry grass")
 
 
 
 
170
  with gr.Accordion("Settings", open=False):
171
  preset = gr.Dropdown(list(RES_PRESETS), value="Fast (768×448)", label="Resolution")
172
  num_frames = gr.Dropdown(FRAME_CHOICES, value=73, label="Frames (24fps)")
 
177
  video_out = gr.Video(label="Colorized result")
178
  used_seed = gr.Number(label="Seed used", interactive=False)
179
 
180
+ run.click(colorize, inputs=[video_in, colors, scene, audio, preset, num_frames, seed, randomize],
181
+ outputs=[video_out, used_seed])
 
 
 
182
 
183
  gr.Examples(
184
  examples=[
185
+ ["examples/slicing_veggie_gray.mp4",
186
+ "hands slicing a fresh green zucchini into thin rounds on a light wooden cutting board, the bright green skin and pale interior of the courgette, a stainless-steel knife catching warm kitchen light",
187
+ "close-up of hands slicing a vegetable on a cutting board",
188
+ "crisp rhythmic chopping on a wooden board, a faint kitchen ambience",
189
+ "Fast (768×448)", 73, 42, False],
190
+ ["examples/surfing_gray.mp4",
191
+ "a surfer in a black wetsuit riding a curling turquoise ocean wave, bright white foam spraying off the crest, deep blue sky and sunlit teal water",
192
+ "a person surfing along the face of an ocean wave",
193
+ "powerful ocean waves crashing and rushing water, wind, distant seagulls",
194
+ "Fast (768×448)", 73, 42, False],
195
+ ["examples/cat_on_a_tree_gray.mp4",
196
+ "a tabby cat with warm brown and grey striped fur and bright green eyes, clinging to rough red-brown tree bark surrounded by lush sunlit green leaves",
197
+ "a cat climbing a tree among leaves",
198
+ "soft leaves rustling in the breeze, faint birdsong, a quiet meow",
199
+ "Fast (768×448)", 73, 42, False],
 
 
 
200
  ],
201
+ inputs=[video_in, colors, scene, audio, preset, num_frames, seed, randomize],
202
+ outputs=[video_out, used_seed], fn=colorize, cache_examples=True, cache_mode="lazy",
 
 
 
203
  )
204
 
205
  if __name__ == "__main__":
examples/cat_on_a_tree_gray.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:1cb0dda8f536f7e5e0beade829bed92cceebb8e605b0aaaff82a1f28a78a9f70
3
- size 214173
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:15ca25a92785b9f942718e1f9411f668c605808b5c41dd933b5ae0be4e238224
3
+ size 325396
examples/slicing_veggie_gray.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:20690e8a850cf08b8fa1e6e3038182421d72fed0a93d1900a372ba1a95bffa2a
3
- size 232034
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7707149cecf2ed4839904c8a5bf41105a4514dee88ac55f6f9761c537b822e9f
3
+ size 351573
examples/surfing_gray.mp4 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:ce4bf73761f71f382892d9c2ab058f716fd9b7b7e4a0c1107ed096242f52b551
3
- size 267897
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b9b5b4b97d274d2dbfa5e829c2f7acc6510db4945b02f7bf7bb0e54200e0f39
3
+ size 409218