umer1995 commited on
Commit
2229af1
·
verified ·
1 Parent(s): 8b4c5cb

Deploy NFA Track R FLUX.2 Fun CN ZeroGPU (real depth CN)

Browse files
Files changed (4) hide show
  1. README.md +30 -7
  2. app.py +372 -0
  3. config/flux2_control.yaml +5 -0
  4. requirements.txt +19 -0
README.md CHANGED
@@ -1,13 +1,36 @@
1
  ---
2
- title: NFA Track R FLUX2 Fun CN ZeroGPU
3
- emoji: 🏆
4
- colorFrom: indigo
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: NFA Track R FLUX.2 Fun CN ZeroGPU
3
+ emoji: 🎬
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: "5.49.1"
 
8
  app_file: app.py
9
  pinned: false
10
+ license: other
11
+ short_description: Real Fun depth ControlNet (NOT soft image=depth)
12
  ---
13
 
14
+ # Track R FLUX.2 Fun depth ControlNet (ZeroGPU)
15
+
16
+ **Hard rule:** This Space runs **real** ALIMAMA / VideoX-Fun Fun ControlNet Union depth.
17
+ Soft Flux2 `image=depth` is **banned forever** on this path.
18
+
19
+ ## Stack (VRAM choice)
20
+
21
+ | Piece | Value |
22
+ |---|---|
23
+ | Painter + CN | VideoX-Fun `Flux2ControlPipeline` + `FLUX.2-dev-Fun-Controlnet-Union-2602` |
24
+ | Base | `black-forest-labs/FLUX.2-dev` |
25
+ | GPU | `@spaces.GPU(duration=300, size="large")` = **48GB** @ **1×** Pro minutes |
26
+ | Memory mode | VideoX-Fun **`model_cpu_offload_and_qfloat8`** (official low-VRAM Fun CN path) |
27
+ | Escalation | If OOM → redeploy with `size="xlarge"` + `model_cpu_offload` (2× quota) |
28
+
29
+ Requires Space secret **`HF_TOKEN`** (gated FLUX.2-dev license accepted on the account).
30
+
31
+ ## API
32
+
33
+ Same client contract as Plan A soft Space: `/generate_still`
34
+ (`positive`, `negative`, `depth_image`, `seed`, `width`, `height`, `steps`, `guidance`, `cn_strength`)
35
+
36
+ Depth is **required**. Soft depth is never used.
app.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """NFA Track R — Fun depth ControlNet ZeroGPU (VideoX-Fun / ALIMAMA).
2
+
3
+ REAL Fun ControlNet Union depth — NOT soft Flux2 image=depth (banned forever).
4
+
5
+ VRAM choice (documented):
6
+ size=\"large\" (48GB, 1× Pro) + VideoX-Fun model_cpu_offload_and_qfloat8.
7
+ Escalation if OOM: size=\"xlarge\" + model_cpu_offload (2× quota).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import traceback
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ import gradio as gr
18
+ import spaces
19
+ import torch
20
+ from huggingface_hub import hf_hub_download, login, snapshot_download
21
+ from omegaconf import OmegaConf
22
+ from PIL import Image
23
+
24
+ HF_TOKEN = (
25
+ os.environ.get("HF_TOKEN")
26
+ or os.environ.get("HUGGINGFACE_HUB_TOKEN")
27
+ or ""
28
+ ).strip()
29
+ if HF_TOKEN:
30
+ try:
31
+ login(token=HF_TOKEN, add_to_git_credential=False)
32
+ except Exception as exc: # noqa: BLE001
33
+ print(f"[nfa-fun-cn] HF login warning: {exc}", flush=True)
34
+
35
+ BASE_MODEL = os.environ.get(
36
+ "NFA_FLUX2_MODEL_ID", "black-forest-labs/FLUX.2-dev"
37
+ ).strip()
38
+ CN_REPO = os.environ.get(
39
+ "NFA_FUN_CN_REPO", "alibaba-pai/FLUX.2-dev-Fun-Controlnet-Union"
40
+ ).strip()
41
+ CN_FILE = os.environ.get(
42
+ "NFA_FUN_CN_FILE", "FLUX.2-dev-Fun-Controlnet-Union-2602.safetensors"
43
+ ).strip()
44
+ # large=48GB 1×; set NFA_FUN_CN_GPU_SIZE=xlarge only after OOM on large
45
+ GPU_SIZE = (os.environ.get("NFA_FUN_CN_GPU_SIZE") or "large").strip().lower()
46
+ if GPU_SIZE not in ("large", "xlarge"):
47
+ GPU_SIZE = "large"
48
+ GPU_DURATION = int(os.environ.get("NFA_FUN_CN_GPU_DURATION") or "300")
49
+ WEIGHT_DTYPE = torch.bfloat16
50
+ # VideoX-Fun official low-VRAM Fun CN mode for 48GB; offload-only on xlarge
51
+ MEM_MODE = (
52
+ os.environ.get("NFA_FUN_CN_MEM_MODE")
53
+ or (
54
+ "model_cpu_offload"
55
+ if GPU_SIZE == "xlarge"
56
+ else "model_cpu_offload_and_qfloat8"
57
+ )
58
+ ).strip()
59
+
60
+ APP_DIR = Path(__file__).resolve().parent
61
+ CONFIG_PATH = APP_DIR / "config" / "flux2_control.yaml"
62
+ CACHE_ROOT = Path(
63
+ os.environ.get("NFA_FUN_CN_CACHE")
64
+ or (Path.home() / ".cache" / "nfa_fun_cn")
65
+ )
66
+ MODEL_DIR = CACHE_ROOT / "FLUX.2-dev"
67
+
68
+ _PIPE = None
69
+ _CN_FILE_PATH: Path | None = None
70
+ _WEIGHTS_READY = False
71
+
72
+
73
+ def _resolve_cn_path() -> Path:
74
+ global _CN_FILE_PATH
75
+ if _CN_FILE_PATH is not None and _CN_FILE_PATH.is_file():
76
+ return _CN_FILE_PATH
77
+ direct = CACHE_ROOT / CN_FILE
78
+ if direct.is_file():
79
+ _CN_FILE_PATH = direct
80
+ return direct
81
+ nested = list(CACHE_ROOT.rglob(CN_FILE))
82
+ if nested:
83
+ _CN_FILE_PATH = nested[0]
84
+ return nested[0]
85
+ raise FileNotFoundError(f"Fun CN weights missing: {CN_FILE}")
86
+
87
+
88
+ def _ensure_weights() -> None:
89
+ """Download on CPU (must run outside @spaces.GPU so quota is not burned)."""
90
+ global _WEIGHTS_READY, _CN_FILE_PATH
91
+ if _WEIGHTS_READY and MODEL_DIR.is_dir():
92
+ try:
93
+ _resolve_cn_path()
94
+ return
95
+ except FileNotFoundError:
96
+ pass
97
+ CACHE_ROOT.mkdir(parents=True, exist_ok=True)
98
+ token = HF_TOKEN or None
99
+ print(f"[nfa-fun-cn] snapshot {BASE_MODEL} -> {MODEL_DIR}", flush=True)
100
+ snapshot_download(
101
+ repo_id=BASE_MODEL,
102
+ local_dir=str(MODEL_DIR),
103
+ local_dir_use_symlinks=False,
104
+ token=token,
105
+ )
106
+ print(f"[nfa-fun-cn] download {CN_REPO}/{CN_FILE}", flush=True)
107
+ path = hf_hub_download(
108
+ repo_id=CN_REPO,
109
+ filename=CN_FILE,
110
+ local_dir=str(CACHE_ROOT),
111
+ local_dir_use_symlinks=False,
112
+ token=token,
113
+ )
114
+ _CN_FILE_PATH = Path(path)
115
+ _WEIGHTS_READY = True
116
+ print(f"[nfa-fun-cn] weights ready cn={_CN_FILE_PATH}", flush=True)
117
+
118
+
119
+ def _prep_depth(depth_image: Image.Image, width: int, height: int) -> Image.Image:
120
+ img = depth_image.convert("RGB")
121
+ if img.size != (width, height):
122
+ img = img.resize((width, height), Image.Resampling.LANCZOS)
123
+ return img
124
+
125
+
126
+ def _compose_prompt(positive: str, negative: str) -> tuple[str, str]:
127
+ pos = (positive or "").strip()
128
+ neg = (negative or "").strip() or " "
129
+ return pos, neg
130
+
131
+
132
+ def get_pipe():
133
+ """Build VideoX-Fun Flux2ControlPipeline once per warm process."""
134
+ global _PIPE
135
+ if _PIPE is not None:
136
+ return _PIPE
137
+
138
+ _ensure_weights()
139
+ from diffusers import FlowMatchEulerDiscreteScheduler
140
+ from safetensors.torch import load_file
141
+ from videox_fun.models import (
142
+ AutoencoderKLFlux2,
143
+ Flux2ControlTransformer2DModel,
144
+ Mistral3ForConditionalGeneration,
145
+ PixtralProcessor,
146
+ )
147
+ from videox_fun.pipeline import Flux2ControlPipeline
148
+ from videox_fun.utils.fp8_optimization import (
149
+ convert_model_weight_to_float8,
150
+ convert_weight_dtype_wrapper,
151
+ )
152
+ from videox_fun.utils.utils import get_image_latent
153
+
154
+ # stash for generate
155
+ get_pipe._get_image_latent = get_image_latent # type: ignore[attr-defined]
156
+
157
+ model_name = str(MODEL_DIR)
158
+ cn_file = str(_resolve_cn_path())
159
+ config = OmegaConf.load(str(CONFIG_PATH))
160
+ print(
161
+ f"[nfa-fun-cn] load Flux2ControlTransformer + Fun CN "
162
+ f"mem={MEM_MODE} size={GPU_SIZE}",
163
+ flush=True,
164
+ )
165
+ transformer = Flux2ControlTransformer2DModel.from_pretrained(
166
+ model_name,
167
+ subfolder="transformer",
168
+ low_cpu_mem_usage=True,
169
+ torch_dtype=WEIGHT_DTYPE,
170
+ transformer_additional_kwargs=OmegaConf.to_container(
171
+ config["transformer_additional_kwargs"]
172
+ ),
173
+ ).to(WEIGHT_DTYPE)
174
+
175
+ state_dict = load_file(cn_file)
176
+ state_dict = state_dict["state_dict"] if "state_dict" in state_dict else state_dict
177
+ missing, unexpected = transformer.load_state_dict(state_dict, strict=False)
178
+ print(
179
+ f"[nfa-fun-cn] Fun CN loaded missing={len(missing)} unexpected={len(unexpected)}",
180
+ flush=True,
181
+ )
182
+
183
+ vae = AutoencoderKLFlux2.from_pretrained(model_name, subfolder="vae").to(
184
+ WEIGHT_DTYPE
185
+ )
186
+ tokenizer = PixtralProcessor.from_pretrained(model_name, subfolder="tokenizer")
187
+ text_encoder = Mistral3ForConditionalGeneration.from_pretrained(
188
+ model_name,
189
+ subfolder="text_encoder",
190
+ torch_dtype=WEIGHT_DTYPE,
191
+ low_cpu_mem_usage=True,
192
+ )
193
+ scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
194
+ model_name, subfolder="scheduler"
195
+ )
196
+ pipeline = Flux2ControlPipeline(
197
+ vae=vae,
198
+ tokenizer=tokenizer,
199
+ text_encoder=text_encoder,
200
+ transformer=transformer,
201
+ scheduler=scheduler,
202
+ )
203
+
204
+ device = "cuda" if torch.cuda.is_available() else "cpu"
205
+ if MEM_MODE == "model_cpu_offload_and_qfloat8":
206
+ convert_model_weight_to_float8(
207
+ transformer,
208
+ exclude_module_name=["img_in", "txt_in", "timestep"],
209
+ device=device,
210
+ )
211
+ convert_weight_dtype_wrapper(transformer, WEIGHT_DTYPE)
212
+ pipeline.enable_model_cpu_offload(device=device)
213
+ elif MEM_MODE == "sequential_cpu_offload":
214
+ pipeline.enable_sequential_cpu_offload(device=device)
215
+ elif MEM_MODE == "model_cpu_offload":
216
+ pipeline.enable_model_cpu_offload(device=device)
217
+ else:
218
+ pipeline.to(device=device)
219
+
220
+ _PIPE = pipeline
221
+ print("[nfa-fun-cn] Flux2ControlPipeline ready (REAL Fun CN)", flush=True)
222
+ return _PIPE
223
+
224
+
225
+ @spaces.GPU(duration=GPU_DURATION, size=GPU_SIZE)
226
+ def _generate_still_gpu(
227
+ positive: str,
228
+ negative: str,
229
+ depth_image: Image.Image,
230
+ seed: int,
231
+ width: int,
232
+ height: int,
233
+ steps: int,
234
+ guidance: float,
235
+ cn_strength: float,
236
+ ) -> Image.Image:
237
+ """GPU-billed Fun CN infer only — weights must already be on disk."""
238
+ if torch.cuda.is_available():
239
+ free, total = torch.cuda.mem_get_info()
240
+ print(
241
+ f"[nfa-fun-cn] cuda free={free/1e9:.1f}G total={total/1e9:.1f}G "
242
+ f"duration={GPU_DURATION} size={GPU_SIZE} mem={MEM_MODE}",
243
+ flush=True,
244
+ )
245
+
246
+ w = int(width) if width else 1216
247
+ h = int(height) if height else 832
248
+ w -= w % 16
249
+ h -= h % 16
250
+ prompt, neg = _compose_prompt(positive, negative)
251
+ if not prompt:
252
+ raise gr.Error("positive prompt is required")
253
+
254
+ depth = _prep_depth(depth_image, w, h)
255
+ pipe = get_pipe()
256
+ get_image_latent = get_pipe._get_image_latent # type: ignore[attr-defined]
257
+
258
+ # VideoX-Fun control latents (NOT Flux2Pipeline soft image=)
259
+ control_latent = get_image_latent(depth, sample_size=[h, w])[:, :, 0]
260
+ inpaint_image = torch.zeros([1, 3, h, w])
261
+ mask_image = torch.ones([1, 1, h, w]) * 255
262
+
263
+ strength = float(cn_strength)
264
+ if strength <= 0:
265
+ strength = 0.75
266
+ # ALIMAMA recommended band 0.65–0.80; allow Track R packet values
267
+ strength = max(0.05, min(1.5, strength))
268
+
269
+ device = "cuda" if torch.cuda.is_available() else "cpu"
270
+ generator = torch.Generator(device=device).manual_seed(int(seed))
271
+ print(
272
+ f"[nfa-fun-cn] REAL Fun CN generate seed={seed} {w}x{h} steps={steps} "
273
+ f"cn={strength} path=videox_fun_flux2_control",
274
+ flush=True,
275
+ )
276
+ with torch.no_grad():
277
+ out = pipe(
278
+ prompt=prompt,
279
+ negative_prompt=neg,
280
+ height=h,
281
+ width=w,
282
+ generator=generator,
283
+ guidance_scale=float(guidance),
284
+ image=None,
285
+ inpaint_image=inpaint_image,
286
+ mask_image=mask_image,
287
+ control_image=control_latent,
288
+ num_inference_steps=int(steps),
289
+ control_context_scale=strength,
290
+ ).images
291
+ return out[0]
292
+
293
+
294
+ def generate_still(
295
+ positive: str,
296
+ negative: str = "",
297
+ depth_image: Optional[Image.Image] = None,
298
+ seed: int = 42,
299
+ width: int = 1216,
300
+ height: int = 832,
301
+ steps: int = 28,
302
+ guidance: float = 4.0,
303
+ cn_strength: float = 0.75,
304
+ ) -> Image.Image:
305
+ """CPU download + GPU Fun CN. Soft image=depth is never used."""
306
+ try:
307
+ if depth_image is None:
308
+ raise gr.Error(
309
+ "FUN_CN_REQUIRES_DEPTH: depth_image is required for real Fun ControlNet."
310
+ )
311
+ _ensure_weights()
312
+ return _generate_still_gpu(
313
+ positive,
314
+ negative or "",
315
+ depth_image,
316
+ int(seed),
317
+ int(width),
318
+ int(height),
319
+ int(steps),
320
+ float(guidance),
321
+ float(cn_strength),
322
+ )
323
+ except gr.Error:
324
+ raise
325
+ except Exception as exc: # noqa: BLE001
326
+ tb = traceback.format_exc()
327
+ print(tb, flush=True)
328
+ raise gr.Error(f"{type(exc).__name__}: {exc}\n\n{tb[-2500:]}") from exc
329
+
330
+
331
+ with gr.Blocks(title="NFA Track R FLUX.2 Fun CN ZeroGPU") as demo:
332
+ gr.Markdown(
333
+ "## NFA Track R — **Real Fun depth ControlNet** (ZeroGPU)\n"
334
+ f"- Stack: VideoX-Fun `Flux2ControlPipeline` + `{CN_FILE}`\n"
335
+ f"- Base: `{BASE_MODEL}`\n"
336
+ f"- GPU: `size={GPU_SIZE}` duration={GPU_DURATION}s mem=`{MEM_MODE}`\n"
337
+ "- Soft `image=depth` is **banned** on this Space.\n"
338
+ "- First GPU call loads Fun CN (slow once)."
339
+ )
340
+ with gr.Row():
341
+ with gr.Column():
342
+ positive = gr.Textbox(label="positive", lines=12)
343
+ negative = gr.Textbox(label="negative", lines=3)
344
+ depth_image = gr.Image(label="depth_image (required)", type="pil")
345
+ seed = gr.Number(label="seed", value=42, precision=0)
346
+ width = gr.Number(label="width", value=1216, precision=0)
347
+ height = gr.Number(label="height", value=832, precision=0)
348
+ steps = gr.Number(label="steps", value=28, precision=0)
349
+ guidance = gr.Number(label="guidance", value=4.0)
350
+ cn_strength = gr.Number(label="cn_strength", value=0.75)
351
+ btn = gr.Button("Generate (Fun CN)", variant="primary")
352
+ with gr.Column():
353
+ still = gr.Image(label="still")
354
+ btn.click(
355
+ fn=generate_still,
356
+ inputs=[
357
+ positive,
358
+ negative,
359
+ depth_image,
360
+ seed,
361
+ width,
362
+ height,
363
+ steps,
364
+ guidance,
365
+ cn_strength,
366
+ ],
367
+ outputs=[still],
368
+ api_name="generate_still",
369
+ )
370
+
371
+ if __name__ == "__main__":
372
+ demo.queue(max_size=4).launch()
config/flux2_control.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ format: diffusers
2
+ pipeline: flux2
3
+ transformer_additional_kwargs:
4
+ control_layers: [0, 2, 4, 6]
5
+ control_in_dim: 260
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ torchvision
3
+ torchaudio
4
+ accelerate
5
+ safetensors
6
+ sentencepiece
7
+ protobuf
8
+ pillow
9
+ numpy
10
+ omegaconf
11
+ einops
12
+ spaces
13
+ gradio>=5.0.0
14
+ huggingface_hub
15
+ requests
16
+ # Official Fun CN stack (ALIMAMA / VideoX-Fun) — NOT Diffusers soft image=
17
+ videox-fun @ git+https://github.com/aigc-apps/VideoX-Fun.git
18
+ diffusers>=0.36.0
19
+ transformers>=4.46.2