linoyts HF Staff commited on
Commit
865c308
·
verified ·
1 Parent(s): a637a9c

Switch backend to native LTX-2 (ICLoraPipeline)

Browse files

Swaps the inference backend from diffusers to the native LTX-2 codebase (ICLoraPipeline, distilled, bf16; clones Lightricks/LTX-2 at runtime). UI, inputs/outputs, examples and API endpoint are unchanged; only the description drops the diffusers mention. requirements.txt updated to native deps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Files changed (3) hide show
  1. README.md +2 -2
  2. app.py +205 -83
  3. requirements.txt +9 -7
README.md CHANGED
@@ -11,10 +11,10 @@ pinned: false
11
  hardware: zero-a10g
12
  short_description: Remove beards from video with an LTX-2.3 IC-LoRA
13
  models:
14
- - diffusers/LTX-2.3-Diffusers
15
  - Lightricks/LTX-2.3-22b-IC-LoRA-Instant-Shave
16
  ---
17
 
18
  # 🪒 LTX-2.3 Beard Removal (Instant Shave)
19
  Removes beard, mustache and stubble from a person in a video while preserving identity, expression and motion.
20
- IC-LoRA on LTX-2.3 (`LTX2InContextPipeline`, 30 steps, guidance 4.0, STG, 25fps, `REMOVEBEARD` trigger).
 
11
  hardware: zero-a10g
12
  short_description: Remove beards from video with an LTX-2.3 IC-LoRA
13
  models:
14
+ - Lightricks/LTX-2.3
15
  - Lightricks/LTX-2.3-22b-IC-LoRA-Instant-Shave
16
  ---
17
 
18
  # 🪒 LTX-2.3 Beard Removal (Instant Shave)
19
  Removes beard, mustache and stubble from a person in a video while preserving identity, expression and motion.
20
+ IC-LoRA on LTX-2.3 (the native LTX-2 pipeline, 30 steps, guidance 4.0, STG, 25fps, `REMOVEBEARD` trigger).
app.py CHANGED
@@ -1,50 +1,136 @@
1
  import os
 
 
2
 
3
- os.environ.setdefault("TORCH_COMPILE_DISABLE", "1")
4
- os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")
 
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import random
7
  import tempfile
8
- import threading
9
- import time
10
 
11
  import numpy as np
12
  import imageio.v3 as iio
13
- import spaces
 
14
  import torch
 
 
 
 
15
  import gradio as gr
16
- from PIL import Image, ImageOps
17
- from huggingface_hub import hf_hub_download
18
- from safetensors.torch import load_file
 
 
 
 
 
 
 
 
 
19
 
20
- from diffusers import LTX2InContextPipeline
21
- from diffusers.pipelines.ltx2.pipeline_ltx2_ic_lora import LTX2ReferenceCondition
22
- from diffusers.utils import load_video, encode_video
 
 
 
 
 
 
23
 
24
- # --- Config -----------------------------------------------------------------
25
- # Beard-removal IC-LoRA non-distilled recipe: 30 steps, guidance 4.0, STG, 25 fps.
26
- BASE_MODEL = "diffusers/LTX-2.3-Diffusers"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  LORA_REPO = "Lightricks/LTX-2.3-22b-IC-LoRA-Instant-Shave"
28
  LORA_FILE = "ltx-2.3-22b-ic-lora-instant-shave-0.9.safetensors"
29
  LORA_SCALE = 1.0
30
- FPS = 25 # the card recommends 25 fps
31
- NUM_STEPS = 30
32
- GUIDANCE = 4.0
33
- STG_BLOCKS = [29]
34
- NEGATIVE = ("beard, mustache, facial hair, stubble, worst quality, "
35
- "inconsistent motion, blurry, jittery, distorted")
36
- MAX_SEED = np.iinfo(np.int32).max
37
- HF_TOKEN = os.environ.get("HF_TOKEN")
38
-
39
  RES_PRESETS = {"960×544 (recommended)": (960, 544), "768×448 (fast)": (768, 448)}
 
40
  FRAME_CHOICES = [33, 49, 73, 97, 121]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- pipe = LTX2InContextPipeline.from_pretrained(BASE_MODEL, torch_dtype=torch.bfloat16)
43
- pipe.to("cuda")
44
- pipe.vae.enable_tiling()
45
- _lora_path = hf_hub_download(LORA_REPO, LORA_FILE, token=HF_TOKEN)
46
- pipe.load_lora_weights(load_file(_lora_path), adapter_name="shave")
47
- pipe.set_adapters("shave", LORA_SCALE)
48
 
49
 
50
  def _src_fps(path, default=FPS):
@@ -54,85 +140,121 @@ def _src_fps(path, default=FPS):
54
  return default
55
 
56
 
57
- def _load_frames(path, num_frames, width, height):
58
- frames = load_video(path)
59
- if not frames:
60
- return []
61
- fps = _src_fps(path)
62
  out = []
63
  for i in range(num_frames):
64
- idx = min(int(round(i / FPS * fps)), len(frames) - 1)
65
- out.append(ImageOps.fit(frames[idx].convert("RGB"), (width, height), Image.LANCZOS))
66
- return out
 
 
 
 
 
 
67
 
68
 
69
- def _pick_resolution(first_frame, preset):
70
  w, h = RES_PRESETS[preset]
71
- if first_frame.height > first_frame.width:
72
- w, h = h, w
 
 
 
 
73
  return w, h
74
 
75
 
76
- def _build_prompt(prompt):
77
- desc = prompt.strip() or "the same person, completely clean-shaven"
78
- return (f"REMOVEBEARD {desc}, completely smooth and clean-shaven face, bare skin, "
79
- f"no beard, no stubble, no facial hair; identity, expression, motion, lighting and scene unchanged.")
 
 
80
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- def _export(video_np, audio, path):
83
- kw = {}
84
- if audio is not None:
85
- kw = dict(audio=audio[0].float().cpu(), audio_sample_rate=pipe.vocoder.config.output_sampling_rate)
86
- encode_video(video_np, fps=FPS, output_path=path, **kw)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
 
89
  def _duration(*args, **kwargs):
90
- preset = next((a for a in args if isinstance(a, str) and a in RES_PRESETS), None)
91
- num_frames = next((a for a in args if isinstance(a, int) and a in FRAME_CHOICES), 49)
92
- w, h = RES_PRESETS.get(preset, (960, 544))
93
- per_frame = max(3.0, 3.0 * (w * h) / (768 * 448)) # 30 steps + CFG + STG (dev path)
94
- return int(120 + int(num_frames) * per_frame)
95
 
96
 
97
  @spaces.GPU(duration=_duration)
98
- def shave(video, prompt, preset, num_frames, negative, seed, randomize,
99
- progress=gr.Progress(track_tqdm=True)):
100
  if video is None:
101
- raise gr.Error("Please upload a video of a bearded subject.")
102
- if randomize:
103
- seed = random.randint(0, MAX_SEED)
104
- seed = int(seed)
105
  num_frames = int(num_frames)
106
-
107
- probe = load_video(video)
108
- if not probe:
109
- raise gr.Error("Could not read any frames from that video.")
110
- width, height = _pick_resolution(probe[0], preset)
111
- ref = _load_frames(video, num_frames, width, height)
112
- full_prompt = _build_prompt(prompt)
113
-
114
-
115
- video_out, audio_out = pipe(
116
- prompt=full_prompt, negative_prompt=negative,
117
- reference_conditions=[LTX2ReferenceCondition(frames=ref, strength=1.0)],
118
- reference_downscale_factor=1,
119
- width=width, height=height, num_frames=num_frames, frame_rate=FPS,
120
- num_inference_steps=NUM_STEPS, guidance_scale=GUIDANCE,
121
- spatio_temporal_guidance_blocks=STG_BLOCKS,
122
- generator=torch.Generator(device="cuda").manual_seed(seed),
123
- output_type="np", return_dict=False,
124
- )
125
- out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
126
- _export(video_out[0], audio_out, out_path)
127
  return out_path, seed
128
 
129
 
 
 
 
 
 
130
  with gr.Blocks(title="LTX-2.3 Beard Removal") as demo:
131
  gr.Markdown(
132
  "# 🪒 LTX-2.3 Beard Removal (Instant Shave)\n"
133
  "Remove beard, mustache and stubble from a person in a video while preserving identity, expression and "
134
- "motion. Using [LTX 2.3 Dev](https://huggingface.co/diffusers/LTX-2.3-Diffusers) with the "
135
- "[Beard-Removal IC-LoRA](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Instant-Shave), via diffusers 🧨."
136
  )
137
  with gr.Row():
138
  with gr.Column():
 
1
  import os
2
+ import subprocess
3
+ import sys
4
 
5
+ # ZeroGPU: torch.compile / dynamo unsupported — disable before any torch import.
6
+ os.environ["TORCH_COMPILE_DISABLE"] = "1"
7
+ os.environ["TORCHDYNAMO_DISABLE"] = "1"
8
 
9
+ # memory-efficient attention
10
+ subprocess.run([sys.executable, "-m", "pip", "install", "xformers==0.0.32.post2", "--no-build-isolation"], check=False)
11
+
12
+ # --- clone + install the NATIVE LTX-2 codebase at the pinned commit the working ZeroGPU spaces use ---
13
+ LTX_REPO_URL = "https://github.com/Lightricks/LTX-2.git"
14
+ LTX_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LTX-2")
15
+ LTX_COMMIT = "ae855f8538843825f9015a419cf4ba5edaf5eec2"
16
+ if not os.path.exists(LTX_REPO_DIR):
17
+ subprocess.run(["git", "clone", LTX_REPO_URL, LTX_REPO_DIR], check=True)
18
+ subprocess.run(["git", "-C", LTX_REPO_DIR, "checkout", LTX_COMMIT], check=True)
19
+ subprocess.run([sys.executable, "-m", "pip", "install", "--force-reinstall", "--no-deps",
20
+ "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-core"),
21
+ "-e", os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines")], check=True)
22
+ sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-pipelines", "src"))
23
+ sys.path.insert(0, os.path.join(LTX_REPO_DIR, "packages", "ltx-core", "src"))
24
+
25
+ import logging
26
  import random
27
  import tempfile
 
 
28
 
29
  import numpy as np
30
  import imageio.v3 as iio
31
+ from PIL import Image, ImageOps
32
+
33
  import torch
34
+ torch._dynamo.config.suppress_errors = True
35
+ torch._dynamo.config.disable = True
36
+
37
+ import spaces
38
  import gradio as gr
39
+ from huggingface_hub import hf_hub_download, snapshot_download
40
+
41
+ # Import LTX modules in the proven order — importing ltx_core.quantization/loader FIRST hits a
42
+ # circular import (fp8_cast <-> loader.fuse_loras). Importing the model modules first forces the
43
+ # correct init order (mirrors the working reference Space).
44
+ from ltx_core.model.video_vae import TilingConfig, get_video_chunks_number, decode_video as _vae_decode_video # noqa: F401
45
+ from ltx_core.model.upsampler import upsample_video as _upsample_video # noqa: F401
46
+ from ltx_core.model.audio_vae import encode_audio as _vae_encode_audio # noqa: F401
47
+ from ltx_core.quantization import QuantizationPolicy
48
+ from ltx_core.loader import LoraPathStrengthAndSDOps, LTXV_LORA_COMFY_RENAMING_MAP
49
+ from ltx_pipelines.ic_lora import ICLoraPipeline
50
+ from ltx_pipelines.utils.media_io import encode_video
51
 
52
+ # --- ZeroGPU loader patch -------------------------------------------------------------
53
+ # The native loader opens safetensors directly on the CUDA device
54
+ # (safe_open(path, device="cuda")), doing the host->device copy in safetensors' own C++
55
+ # (cudaMemcpy) — bypassing torch.Tensor.to, the call ZeroGPU patches to virtualise + pack
56
+ # weights at module scope. Result: "No CUDA GPUs are available" at startup, nothing packs.
57
+ # Patch it to open on CPU then move via torch.Tensor.to (ZeroGPU-virtualisable).
58
+ import safetensors as _safetensors
59
+ import ltx_core.loader.sft_loader as _sft
60
+ from ltx_core.loader.primitives import StateDict as _StateDict
61
 
62
+ def _zerogpu_safe_load(self, path, sd_ops, device=None):
63
+ device = device or torch.device("cpu")
64
+ sd, size, dtype = {}, 0, set()
65
+ model_paths = path if isinstance(path, list) else [path]
66
+ for shard_path in model_paths:
67
+ with _safetensors.safe_open(shard_path, framework="pt", device="cpu") as f:
68
+ for name in f.keys():
69
+ expected = name if sd_ops is None else sd_ops.apply_to_key(name)
70
+ if expected is None:
71
+ continue
72
+ value = f.get_tensor(name).to(device=device) # torch path -> ZeroGPU-virtualised
73
+ kvs = ((expected, value),)
74
+ if sd_ops is not None:
75
+ kvs = sd_ops.apply_to_key_value(expected, value)
76
+ for k, v in kvs:
77
+ size += v.nbytes
78
+ dtype.add(v.dtype)
79
+ sd[k] = v
80
+ return _StateDict(sd=sd, device=device, size=size, dtype=dtype)
81
+
82
+ _sft.SafetensorsStateDictLoader.load = _zerogpu_safe_load
83
+ print("[PATCH] safetensors loader -> CPU-open + torch.to (ZeroGPU-virtualisable)")
84
+ # --------------------------------------------------------------------------------------
85
+
86
+ # --- attention backend patch (FA3 crashes on Blackwell ZeroGPU; use xformers/SDPA) ---
87
+ import torch.nn.functional as F
88
+ from ltx_core.model.transformer import attention as _attn_mod
89
+
90
+ def _sdpa_as_mea(query, key, value, attn_bias=None, scale=None, **kwargs):
91
+ q, k, v = query.transpose(1, 2), key.transpose(1, 2), value.transpose(1, 2)
92
+ return F.scaled_dot_product_attention(q, k, v, scale=scale).transpose(1, 2)
93
+
94
+ # IMPORTANT (ZeroGPU): never query CUDA at module scope. SDPA works on every GPU (incl.
95
+ # Blackwell ZeroGPU, where FA3 crashes), so patch it unconditionally.
96
+ _attn_mod.memory_efficient_attention = _sdpa_as_mea
97
+ print("[ATTN] SDPA (patched at module scope, no CUDA query)")
98
+
99
+ logging.getLogger().setLevel(logging.INFO)
100
+
101
+ # =========================== PER-LORA CONFIG (colorize) ===========================
102
+ TITLE = "LTX-2.3 Beard Removal (native LTX-2)"
103
  LORA_REPO = "Lightricks/LTX-2.3-22b-IC-LoRA-Instant-Shave"
104
  LORA_FILE = "ltx-2.3-22b-ic-lora-instant-shave-0.9.safetensors"
105
  LORA_SCALE = 1.0
106
+ SKIP_STAGE_2 = True
107
+ GRAYSCALE_REF = False
 
 
 
 
 
 
 
108
  RES_PRESETS = {"960×544 (recommended)": (960, 544), "768×448 (fast)": (768, 448)}
109
+ DEFAULT_PRESET = "960×544 (recommended)"
110
  FRAME_CHOICES = [33, 49, 73, 97, 121]
111
+ DEFAULT_FRAMES = 49
112
+
113
+ def build_prompt(p):
114
+ return (
115
+ f"REMOVEBEARD {p.strip()}, completely smooth and clean-shaven face, bare skin, "
116
+ "no beard, no stubble, no facial hair; identity, expression, motion, lighting and scene unchanged."
117
+ )
118
+
119
+ EXAMPLES = [
120
+ ["examples/beard_1.mp4",
121
+ "the same man with a completely smooth clean-shaven face, no beard or stubble, bare skin, relaxed expression in soft indoor light; a quiet room ambience",
122
+ "960×544 (recommended)", 49, 42, False],
123
+ ["examples/beard_2.mp4",
124
+ "the same man in a hooded jacket with a completely smooth clean-shaven face, no beard or stubble, outdoors at night with city lights behind him; cool night ambience",
125
+ "960×544 (recommended)", 49, 42, False],
126
+ ]
127
+ # =================================================================================
128
 
129
+ FPS = 25.0
130
+ MAX_SEED = np.iinfo(np.int32).max
131
+ HF_TOKEN = os.environ.get("HF_TOKEN")
132
+ LTX_MODEL_REPO = "Lightricks/LTX-2.3"
133
+ GEMMA_REPO = "google/gemma-3-12b-it-qat-q4_0-unquantized"
 
134
 
135
 
136
  def _src_fps(path, default=FPS):
 
140
  return default
141
 
142
 
143
+ def _prep_reference(path, width, height, num_frames):
144
+ """Resample to 24fps, aspect-fit/crop to WxH, NF frames; (optionally grayscale); write temp mp4."""
145
+ vid = iio.imread(path, plugin="pyav")
146
+ src_fps = _src_fps(path)
147
+ n = len(vid)
148
  out = []
149
  for i in range(num_frames):
150
+ idx = min(int(round(i / FPS * src_fps)), n - 1)
151
+ im = Image.fromarray(vid[idx]).convert("RGB")
152
+ im = ImageOps.fit(im, (width, height), Image.LANCZOS)
153
+ if GRAYSCALE_REF:
154
+ im = im.convert("L").convert("RGB")
155
+ out.append(np.array(im))
156
+ tmp = tempfile.mktemp(suffix=".mp4")
157
+ iio.imwrite(tmp, np.stack(out), fps=FPS, plugin="pyav", codec="libx264")
158
+ return tmp
159
 
160
 
161
+ def _pick_resolution(path, preset):
162
  w, h = RES_PRESETS[preset]
163
+ try:
164
+ f0 = iio.imread(path, plugin="pyav", index=0)
165
+ if f0.shape[0] > f0.shape[1]: # portrait
166
+ w, h = h, w
167
+ except Exception:
168
+ pass
169
  return w, h
170
 
171
 
172
+ # --- Load native pipeline + IC-LoRA once at module scope (ZeroGPU packs weights here) ---
173
+ print("Downloading checkpoints…")
174
+ checkpoint_path = hf_hub_download(LTX_MODEL_REPO, "ltx-2.3-22b-distilled-1.1.safetensors", token=HF_TOKEN)
175
+ spatial_upsampler_path = hf_hub_download(LTX_MODEL_REPO, "ltx-2.3-spatial-upscaler-x2-1.1.safetensors", token=HF_TOKEN)
176
+ gemma_root = snapshot_download(GEMMA_REPO, token=HF_TOKEN)
177
+ lora_path = hf_hub_download(LORA_REPO, LORA_FILE, token=HF_TOKEN)
178
 
179
+ print("Building ICLoraPipeline…")
180
+ pipeline = ICLoraPipeline(
181
+ distilled_checkpoint_path=checkpoint_path,
182
+ spatial_upsampler_path=spatial_upsampler_path,
183
+ gemma_root=gemma_root,
184
+ loras=[LoraPathStrengthAndSDOps(lora_path, LORA_SCALE, LTXV_LORA_COMFY_RENAMING_MAP)],
185
+ # bf16 (NOT fp8): the IC-LoRA is fused into the transformer at MODULE SCOPE (the GPU
186
+ # worker can't re-open the checkpoint file). fp8_cast()'s fusion runs a custom CUDA kernel
187
+ # that can't be ZeroGPU-virtualised; the bf16 fuse rule is pure torch -> virtualisable.
188
+ quantization=None,
189
+ )
190
 
191
+
192
+ def _preload_pin(ledger, tag):
193
+ if ledger is None:
194
+ return
195
+ for name in ["transformer", "video_encoder", "video_decoder", "audio_encoder",
196
+ "audio_decoder", "vocoder", "spatial_upsampler", "text_encoder",
197
+ "gemma_embeddings_processor"]:
198
+ fn = getattr(ledger, name, None)
199
+ if callable(fn):
200
+ try:
201
+ obj = fn()
202
+ setattr(ledger, name, (lambda o=obj: o))
203
+ print(f"[preload {tag}] {name} ✓")
204
+ except Exception as e:
205
+ print(f"[preload {tag}] {name} skipped: {e}")
206
+
207
+ # Preload stage 1 always; preload stage 2 only when two-stage is used (skip_stage_2=False).
208
+ # Eagerly pinning both ledgers materializes TWO ~46GB transformers — too big for the ZeroGPU pack.
209
+ _preload_pin(getattr(pipeline, "stage_1_model_ledger", None), "stage1")
210
+ if not SKIP_STAGE_2:
211
+ _preload_pin(getattr(pipeline, "stage_2_model_ledger", None), "stage2")
212
+ print("Pipeline ready.")
213
 
214
 
215
  def _duration(*args, **kwargs):
216
+ nf = next((a for a in args if isinstance(a, int) and a in FRAME_CHOICES), DEFAULT_FRAMES)
217
+ return int(60 + nf * 1.2)
 
 
 
218
 
219
 
220
  @spaces.GPU(duration=_duration)
221
+ @torch.inference_mode()
222
+ def shave(video, prompt, preset, num_frames, negative, seed, randomize, progress=gr.Progress(track_tqdm=True)):
223
  if video is None:
224
+ raise gr.Error("Please upload a video.")
225
+ if not prompt.strip():
226
+ raise gr.Error("Describe the result (e.g. 'a brown rabbit on grey rocks, soft birdsong').")
227
+ seed = random.randint(0, MAX_SEED) if randomize else int(seed)
228
  num_frames = int(num_frames)
229
+ width, height = _pick_resolution(video, preset)
230
+ ref_path = _prep_reference(video, width, height, num_frames)
231
+ tiling = TilingConfig.default()
232
+ # skip_stage_2 outputs at half the passed dims -> pass 2x so output matches the preset.
233
+ gen_w, gen_h = (width * 2, height * 2) if SKIP_STAGE_2 else (width, height)
234
+ video_out, audio_out = pipeline(
235
+ prompt=build_prompt(prompt),
236
+ seed=seed, height=gen_h, width=gen_w,
237
+ num_frames=num_frames, frame_rate=FPS,
238
+ images=[], video_conditioning=[(ref_path, 1.0)],
239
+ skip_stage_2=SKIP_STAGE_2, tiling_config=tiling,
240
+ )
241
+ out_path = tempfile.mktemp(suffix=".mp4")
242
+ encode_video(video=video_out, fps=FPS, audio=audio_out, output_path=out_path,
243
+ video_chunks_number=get_video_chunks_number(num_frames, tiling))
 
 
 
 
 
 
244
  return out_path, seed
245
 
246
 
247
+ # --- UI config (match the public Space exactly) ---
248
+ RES_PRESETS = {"960×544 (recommended)": (960, 544), "768×448 (fast)": (768, 448)}
249
+ FRAME_CHOICES = [33, 49, 73, 97, 121]
250
+
251
+
252
  with gr.Blocks(title="LTX-2.3 Beard Removal") as demo:
253
  gr.Markdown(
254
  "# 🪒 LTX-2.3 Beard Removal (Instant Shave)\n"
255
  "Remove beard, mustache and stubble from a person in a video while preserving identity, expression and "
256
+ "motion. Using [LTX 2.3 Dev](https://huggingface.co/Lightricks/LTX-2.3) with the "
257
+ "[Beard-Removal IC-LoRA](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Instant-Shave)."
258
  )
259
  with gr.Row():
260
  with gr.Column():
requirements.txt CHANGED
@@ -1,9 +1,11 @@
1
- git+https://github.com/huggingface/diffusers
2
- transformers
3
  accelerate
4
- peft
5
- safetensors
6
- sentencepiece
7
- imageio
8
- imageio-ffmpeg
9
  av
 
 
 
 
 
1
+ transformers==4.57.6
 
2
  accelerate
3
+ torch==2.8.0
4
+ torchaudio==2.8.0
5
+ einops
6
+ scipy
 
7
  av
8
+ scikit-image>=0.25.2
9
+ flashpack==0.1.2
10
+ imageio[ffmpeg]
11
+ pillow