dagloop5 commited on
Commit
3759021
·
verified ·
1 Parent(s): 2d71524

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +58 -4
app.py CHANGED
@@ -33,6 +33,14 @@ PLACEMENT = os.environ.get("H3_PLACEMENT", "pack").lower()
33
  ATTENTION = os.environ.get("H3_ATTENTION", "_native_cudnn").lower()
34
  GPU_SIZE = os.environ.get("H3_GPU_SIZE", "xlarge")
35
 
 
 
 
 
 
 
 
 
36
  # Must stay identical to the conditioner's table: the *label* goes over the wire, so a canvas that half does not
37
  # know is rejected there and surfaces as a failure here. This is the workflow's "Target Dimension" node.
38
  CANVASES = {
@@ -101,6 +109,7 @@ FILM = None
101
  FILM_ERROR: str | None = None
102
  LOAD_ERROR: str | None = None
103
  LOADED_IN: float | None = None
 
104
 
105
 
106
  def status() -> str:
@@ -113,8 +122,8 @@ def status() -> str:
113
  film = "FILM **ready**" if FILM is not None else f"FILM **off** ({FILM_ERROR})"
114
  return (
115
  f"Ready · transformer + VAEs **bfloat16, unquantized** · placement `{PLACEMENT}` · attention "
116
- f"`{ATTENTION}` · {h3_aoti.status()} · {film} · loaded in {LOADED_IN:.0f}s · conditioner "
117
- f"`{CONDITIONER_SPACE}`"
118
  )
119
 
120
 
@@ -125,7 +134,7 @@ def load_models() -> str | None:
125
  touched. Both autoencoders carry `_keep_in_fp32_modules` over every module and stay float32: a bfloat16 audio
126
  VAE decodes the soundtrack roughly 20 dB too quiet.
127
  """
128
- global PIPE, FILM, FILM_ERROR, LOAD_ERROR, LOADED_IN
129
 
130
  if PIPE is not None or LOAD_ERROR is not None:
131
  return LOAD_ERROR
@@ -145,6 +154,21 @@ def load_models() -> str | None:
145
  pipe.load_components(dtype=torch.bfloat16)
146
  pipe.transformer.set_attention_backend(ATTENTION)
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  # Still startup, still free: an AoTI package carries no weights and opens its archive lazily inside the GPU
149
  # worker.
150
  import h3_aoti
@@ -278,6 +302,8 @@ def _generate(
278
  sharpen,
279
  multiplier,
280
  seed,
 
 
281
  ):
282
  """The only thing on GPU time: the denoise loop, the two decoders and the workflow's post chain.
283
  The mp4 is muxed here rather than in the caller: a `@spaces.GPU` return crosses a process boundary by pickling,
@@ -290,6 +316,13 @@ def _generate(
290
  global FILM
291
 
292
  booked = time.time()
 
 
 
 
 
 
 
293
  if PLACEMENT == "lazy":
294
  PIPE.to("cuda")
295
  elif PLACEMENT == "pack":
@@ -361,6 +394,8 @@ def generate(
361
  interpolation=DEFAULT_INTERPOLATION,
362
  seed=42,
363
  upsample=False,
 
 
364
  progress=gr.Progress(track_tqdm=True),
365
  ):
366
  """One request through the PlagueKind graph. Every parameter but the prompt carries the default its UI
@@ -414,6 +449,8 @@ def generate(
414
  float(sharpen),
415
  multiplier,
416
  int(seed),
 
 
417
  )
418
  # The same call `spaces` will book the worker with, so the report can show the fit against the measurement.
419
  booked_seconds = get_duration(*call)
@@ -429,7 +466,8 @@ def generate(
429
  post.append(f"FILM {multiplier}x -> {fps} fps" if multiplier > 1 else f"{fps} fps")
430
  report = (
431
  f"`{width}x{height}`, {num_frames} frames ({num_frames / FPS:.3f} s) -> {out_frames} frames at {fps} fps · "
432
- f"{int(steps)} steps of `{schedule_key}` · {' · '.join(post)} · seed {int(seed)}\n\n"
 
433
  f"conditioner {condition_seconds:.0f}s ({plan['num_text_tokens']} tokens"
434
  f"{', upsampled' if refined else ''}) · denoise + decode {denoise_seconds:.0f}s "
435
  f"({denoise_seconds / max(1, int(steps)):.1f} s/step) · post {post_seconds:.0f}s · "
@@ -537,6 +575,20 @@ with gr.Blocks(title="PlagueKind · MiniMax-H3") as demo:
537
  value=DEFAULT_STEPS,
538
  info="PlagueKind: 15-20 on the linear_quadratic grid.",
539
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  schedule = gr.Dropdown(
541
  label="Sigma schedule",
542
  choices=list(SCHEDULES),
@@ -583,6 +635,8 @@ with gr.Blocks(title="PlagueKind · MiniMax-H3") as demo:
583
  interpolation,
584
  seed,
585
  upsample,
 
 
586
  ]
587
 
588
  # Two blocks rather than one with empty keyframe cells: the positional order below matches `generate`'s
 
33
  ATTENTION = os.environ.get("H3_ATTENTION", "_native_cudnn").lower()
34
  GPU_SIZE = os.environ.get("H3_GPU_SIZE", "xlarge")
35
 
36
+ LORA_REPO = os.environ.get("H3_LORA_REPO", "dagloop5/LoRA")
37
+ LORA_FILES = {
38
+ "loraa": os.environ.get("H3_LORA_A_FILE", "Mylo_lora_epoch31.safetensors"),
39
+ "lorab": os.environ.get("H3_LORA_B_FILE", "VBVR_H3_attn_only.safetensors"),
40
+ }
41
+ DEFAULT_LORA_A_STRENGTH = 0.0
42
+ DEFAULT_LORA_B_STRENGTH = 0.0
43
+
44
  # Must stay identical to the conditioner's table: the *label* goes over the wire, so a canvas that half does not
45
  # know is rejected there and surfaces as a failure here. This is the workflow's "Target Dimension" node.
46
  CANVASES = {
 
109
  FILM_ERROR: str | None = None
110
  LOAD_ERROR: str | None = None
111
  LOADED_IN: float | None = None
112
+ LORA_STATUS: str | None = None
113
 
114
 
115
  def status() -> str:
 
122
  film = "FILM **ready**" if FILM is not None else f"FILM **off** ({FILM_ERROR})"
123
  return (
124
  f"Ready · transformer + VAEs **bfloat16, unquantized** · placement `{PLACEMENT}` · attention "
125
+ f"`{ATTENTION}` · {h3_aoti.status()} · {film} · {LORA_STATUS or 'no LoRA'} · loaded in {LOADED_IN:.0f}s · "
126
+ f"conditioner `{CONDITIONER_SPACE}`"
127
  )
128
 
129
 
 
134
  touched. Both autoencoders carry `_keep_in_fp32_modules` over every module and stay float32: a bfloat16 audio
135
  VAE decodes the soundtrack roughly 20 dB too quiet.
136
  """
137
+ global PIPE, FILM, FILM_ERROR, LOAD_ERROR, LOADED_IN, LORA_STATUS
138
 
139
  if PIPE is not None or LOAD_ERROR is not None:
140
  return LOAD_ERROR
 
154
  pipe.load_components(dtype=torch.bfloat16)
155
  pipe.transformer.set_attention_backend(ATTENTION)
156
 
157
+ # Approach B: attach both LoRA adapters as PEFT layers on the transformer, inactive (weight 0) until a
158
+ # request asks for them. `load_lora_adapter` is the model-level loader (`PeftAdapterMixin`), used because
159
+ # `MiniMaxH3ModularPipeline` has no pipeline-level `load_lora_weights` of its own.
160
+ if LORA_REPO.lower() not in ("", "off", "none"):
161
+ try:
162
+ for name, filename in LORA_FILES.items():
163
+ pipe.transformer.load_lora_adapter(LORA_REPO, weight_name=filename, adapter_name=name)
164
+ pipe.transformer.set_adapters(list(LORA_FILES), weights=[0.0] * len(LORA_FILES))
165
+ LORA_STATUS = "LoRAs loaded: " + ", ".join(
166
+ f"`{name}` ({LORA_REPO}/{filename})" for name, filename in LORA_FILES.items()
167
+ )
168
+ except Exception as error:
169
+ LORA_STATUS = f"LoRA load failed: {type(error).__name__}: {error}"
170
+ print(f"[gen] {LORA_STATUS}", flush=True)
171
+
172
  # Still startup, still free: an AoTI package carries no weights and opens its archive lazily inside the GPU
173
  # worker.
174
  import h3_aoti
 
302
  sharpen,
303
  multiplier,
304
  seed,
305
+ lora_a_strength,
306
+ lora_b_strength,
307
  ):
308
  """The only thing on GPU time: the denoise loop, the two decoders and the workflow's post chain.
309
  The mp4 is muxed here rather than in the caller: a `@spaces.GPU` return crosses a process boundary by pickling,
 
316
  global FILM
317
 
318
  booked = time.time()
319
+
320
+ # Approach B: blend the two resident LoRA adapters for this request. Cheap — `set_adapters` only updates each
321
+ # PEFT layer's active-adapter list and scale, no weight math — so it's safe to call on every request. Guarded
322
+ # in case loading failed or was disabled at startup, when the transformer carries no adapters at all.
323
+ if hasattr(PIPE.transformer, "peft_config"):
324
+ PIPE.transformer.set_adapters(list(LORA_FILES), weights=[float(lora_a_strength), float(lora_b_strength)])
325
+
326
  if PLACEMENT == "lazy":
327
  PIPE.to("cuda")
328
  elif PLACEMENT == "pack":
 
394
  interpolation=DEFAULT_INTERPOLATION,
395
  seed=42,
396
  upsample=False,
397
+ lora_a_strength=DEFAULT_LORA_A_STRENGTH,
398
+ lora_b_strength=DEFAULT_LORA_B_STRENGTH,
399
  progress=gr.Progress(track_tqdm=True),
400
  ):
401
  """One request through the PlagueKind graph. Every parameter but the prompt carries the default its UI
 
449
  float(sharpen),
450
  multiplier,
451
  int(seed),
452
+ float(lora_a_strength),
453
+ float(lora_b_strength),
454
  )
455
  # The same call `spaces` will book the worker with, so the report can show the fit against the measurement.
456
  booked_seconds = get_duration(*call)
 
466
  post.append(f"FILM {multiplier}x -> {fps} fps" if multiplier > 1 else f"{fps} fps")
467
  report = (
468
  f"`{width}x{height}`, {num_frames} frames ({num_frames / FPS:.3f} s) -> {out_frames} frames at {fps} fps · "
469
+ f"{int(steps)} steps of `{schedule_key}` · {' · '.join(post)} · seed {int(seed)} · "
470
+ f"LoRA A {float(lora_a_strength):.2f} / B {float(lora_b_strength):.2f}\n\n"
471
  f"conditioner {condition_seconds:.0f}s ({plan['num_text_tokens']} tokens"
472
  f"{', upsampled' if refined else ''}) · denoise + decode {denoise_seconds:.0f}s "
473
  f"({denoise_seconds / max(1, int(steps)):.1f} s/step) · post {post_seconds:.0f}s · "
 
575
  value=DEFAULT_STEPS,
576
  info="PlagueKind: 15-20 on the linear_quadratic grid.",
577
  )
578
+ lora_a_strength = gr.Slider(
579
+ label="Anthro Enhancer LoRA",
580
+ minimum=0.0,
581
+ maximum=2.0,
582
+ step=0.05,
583
+ value=DEFAULT_LORA_A_STRENGTH,
584
+ )
585
+ lora_b_strength = gr.Slider(
586
+ label="Reasoning Enhancer LoRA",
587
+ minimum=0.0,
588
+ maximum=2.0,
589
+ step=0.05,
590
+ value=DEFAULT_LORA_B_STRENGTH,
591
+ )
592
  schedule = gr.Dropdown(
593
  label="Sigma schedule",
594
  choices=list(SCHEDULES),
 
635
  interpolation,
636
  seed,
637
  upsample,
638
+ lora_a_strength,
639
+ lora_b_strength,
640
  ]
641
 
642
  # Two blocks rather than one with empty keyframe cells: the positional order below matches `generate`'s