data-archetype commited on
Commit
13490a2
·
verified ·
1 Parent(s): 5023a8e

Publish Canter 0.5.0 contrastive PDG support

Browse files
API.md CHANGED
@@ -26,7 +26,7 @@ image = output.image
26
  | Parameter | Default | Description |
27
  | --- | --- | --- |
28
  | `prompts` | required | One prompt string or a sequence containing one prompt per image. |
29
- | `negative_prompts` | `None` | Optional CFG negative prompt string or sequence. The count must match `prompts`. `None` uses the learned unconditional token. |
30
  | `config` | `CanterPipelineConfig()` | Inference and output settings. |
31
  | `initial_noise` | `None` | Optional float32 latent noise tensor with the configured batch and spatial shape. |
32
  | `progress` | `None` | Optional callback receiving completed and total solver updates. |
@@ -212,8 +212,9 @@ operations. The VAE decoder retains its published seed behavior.
212
  ### PNG metadata
213
 
214
  Images downloaded from the Gradio interface contain a `canter` PNG text field
215
- with compact JSON. The object begins with the prompt, effective per-image seed,
216
- width, height, steps, solver, and schedule. It then records PDG, CFG,
 
217
  self-attention gain, logSNR shift, the Euler-Maruyama and ER-SDE noise
218
  multipliers, installed code version, numbered checkpoint release, and weight
219
  dtype.
@@ -287,8 +288,9 @@ output = pipe(
287
  Step indices run from `0` through `steps - 1`.
288
 
289
  An explicit negative prompt replaces the learned unconditional token on CFG
290
- branches. Negative prompts require enabled CFG or a CFG-dependent PDG mode.
291
- For prompt batches, supply one negative prompt per positive prompt.
 
292
 
293
  ## PDG
294
 
@@ -342,6 +344,7 @@ contrast, and oversaturation.
342
  | --- | --- | --- |
343
  | `PdgMode.NONE` | `"none"` | No PDG branch. Required when PDG is disabled. |
344
  | `PdgMode.FULL` | `"full"` | Guides from the middle-skipped path toward the full main path. |
 
345
  | `PdgMode.THREE_QUARTER` | `"three_quarter"` | Guides from the 75 percent SPRINT path toward the full main path. |
346
  | `PdgMode.ALTERNATE_PDG_FIRST` | `"alternate_pdg_first"` | Alternates PDG on even updates and CFG on odd updates within the PDG window. |
347
  | `PdgMode.ALTERNATE_CFG_FIRST` | `"alternate_cfg_first"` | Alternates CFG on even updates and PDG on odd updates within the PDG window. |
@@ -349,8 +352,12 @@ contrast, and oversaturation.
349
  | `PdgMode.PDG_WITH_ALTERNATING_CFG` | `"pdg_with_alternating_cfg"` | Applies PDG on every active update and adds CFG on odd updates. |
350
  | `PdgMode.CFG_TO_PDG` | `"cfg_to_pdg"` | Uses CFG before the PDG window, then uses full-path PDG inside the window. |
351
 
352
- The five compound modes require `cfg.scale`. `start_step=0` with
353
- `PdgMode.CFG_TO_PDG` begins directly with PDG.
 
 
 
 
354
 
355
  ## Self-attention gain
356
 
@@ -457,7 +464,7 @@ schedule = output.schedule
457
  | Parameter | Default | Description |
458
  | --- | --- | --- |
459
  | `prompts` | required | One string or a sequence of strings. |
460
- | `negative_prompts` | `None` | Optional negative prompt string or sequence for CFG branches. The count must match `prompts`. |
461
  | `config` | `CanterInferenceConfig()` | Latent inference settings. |
462
  | `initial_noise` | `None` | Optional float32 noise with shape `[batch, 128, height / 16, width / 16]`. |
463
  | `progress` | `None` | Optional callback receiving `(completed_updates, total_updates)`. |
 
26
  | Parameter | Default | Description |
27
  | --- | --- | --- |
28
  | `prompts` | required | One prompt string or a sequence containing one prompt per image. |
29
+ | `negative_prompts` | `None` | Optional negative prompt string or sequence for CFG and contrastive PDG. The count must match `prompts`. `None` uses the learned unconditional token. |
30
  | `config` | `CanterPipelineConfig()` | Inference and output settings. |
31
  | `initial_noise` | `None` | Optional float32 latent noise tensor with the configured batch and spatial shape. |
32
  | `progress` | `None` | Optional callback receiving completed and total solver updates. |
 
212
  ### PNG metadata
213
 
214
  Images downloaded from the Gradio interface contain a `canter` PNG text field
215
+ with compact JSON. The object begins with the prompt, optional negative prompt,
216
+ effective per-image seed, width, height, steps, solver, and schedule. It then
217
+ records PDG, CFG,
218
  self-attention gain, logSNR shift, the Euler-Maruyama and ER-SDE noise
219
  multipliers, installed code version, numbered checkpoint release, and weight
220
  dtype.
 
288
  Step indices run from `0` through `steps - 1`.
289
 
290
  An explicit negative prompt replaces the learned unconditional token on CFG
291
+ and contrastive PDG branches. Negative prompts require enabled CFG or a PDG
292
+ mode that consumes contrastive text. For prompt batches, supply one negative
293
+ prompt per positive prompt.
294
 
295
  ## PDG
296
 
 
344
  | --- | --- | --- |
345
  | `PdgMode.NONE` | `"none"` | No PDG branch. Required when PDG is disabled. |
346
  | `PdgMode.FULL` | `"full"` | Guides from the middle-skipped path toward the full main path. |
347
+ | `PdgMode.FULL_CONTRASTIVE` | `"full_contrastive"` | Uses negative text on the middle-skipped path, or learned unconditional text when no negative prompt is supplied. |
348
  | `PdgMode.THREE_QUARTER` | `"three_quarter"` | Guides from the 75 percent SPRINT path toward the full main path. |
349
  | `PdgMode.ALTERNATE_PDG_FIRST` | `"alternate_pdg_first"` | Alternates PDG on even updates and CFG on odd updates within the PDG window. |
350
  | `PdgMode.ALTERNATE_CFG_FIRST` | `"alternate_cfg_first"` | Alternates CFG on even updates and PDG on odd updates within the PDG window. |
 
352
  | `PdgMode.PDG_WITH_ALTERNATING_CFG` | `"pdg_with_alternating_cfg"` | Applies PDG on every active update and adds CFG on odd updates. |
353
  | `PdgMode.CFG_TO_PDG` | `"cfg_to_pdg"` | Uses CFG before the PDG window, then uses full-path PDG inside the window. |
354
 
355
+ The five compound modes require `cfg.scale`. `PdgMode.FULL_CONTRASTIVE` uses
356
+ the PDG scale only. `start_step=0` with `PdgMode.CFG_TO_PDG` begins directly
357
+ with PDG.
358
+
359
+ `pdg_branch_conditioning(mode)` returns `PdgBranchConditioning.POSITIVE` or
360
+ `PdgBranchConditioning.CONTRASTIVE` for the selected PDG alternative path.
361
 
362
  ## Self-attention gain
363
 
 
464
  | Parameter | Default | Description |
465
  | --- | --- | --- |
466
  | `prompts` | required | One string or a sequence of strings. |
467
+ | `negative_prompts` | `None` | Optional negative prompt string or sequence for CFG and contrastive PDG branches. The count must match `prompts`; blank text uses learned unconditional conditioning. |
468
  | `config` | `CanterInferenceConfig()` | Latent inference settings. |
469
  | `initial_noise` | `None` | Optional float32 noise with shape `[batch, 128, height / 16, width / 16]`. |
470
  | `progress` | `None` | Optional callback receiving `(completed_updates, total_updates)`. |
README.md CHANGED
@@ -111,8 +111,11 @@ bundled one-eighth-scale latent-RGB projection. Busy preview work is skipped,
111
  so the sampler never waits for browser publication. The native one-eighth-size
112
  preview is sent directly and scaled for display by the browser. The UI checkbox
113
  below the size preset disables previews.
114
- Downloaded PNG files contain the prompt, effective per-image settings, Canter
115
- code version, and numbered checkpoint release as JSON metadata.
 
 
 
116
 
117
  The server listens on port 7860. To select the bind address explicitly:
118
 
 
111
  so the sampler never waits for browser publication. The native one-eighth-size
112
  preview is sent directly and scaled for display by the browser. The UI checkbox
113
  below the size preset disables previews.
114
+ The negative prompt field appears when CFG or a contrastive-text PDG mode is
115
+ active. Blank negative text uses Canter's learned unconditional conditioning.
116
+ Downloaded PNG files contain the prompt, optional negative prompt, effective
117
+ per-image settings, Canter code version, and numbered checkpoint release as
118
+ JSON metadata.
119
 
120
  The server listens on port 7860. To select the bind address explicitly:
121
 
RELEASES.md CHANGED
@@ -10,6 +10,11 @@ The current package also supplies the small latent-RGB preview projection;
10
  preview behavior therefore follows the installed code rather than changing an
11
  older denoiser checkpoint tag.
12
 
 
 
 
 
 
13
  Canter `0.4.1` makes the dense backend apply to the bundled text encoder as
14
  well as denoiser text attention. Dense text encoding now uses padded PyTorch
15
  scaled dot-product attention and does not require the explicit CUDA
 
10
  preview behavior therefore follows the installed code rather than changing an
11
  older denoiser checkpoint tag.
12
 
13
+ Canter `0.5.0` adds contrastive PDG. Its middle-skipped guidance path uses an
14
+ explicit negative prompt when supplied and learned unconditional text when the
15
+ negative prompt is blank or absent. The Python API, Gradio interface, and
16
+ ComfyUI nodes use the same branch-selection rule.
17
+
18
  Canter `0.4.1` makes the dense backend apply to the bundled text encoder as
19
  well as denoiser text attention. Dense text encoding now uses padded PyTorch
20
  scaled dot-product attention and does not require the explicit CUDA
TECHNICAL_REPORT.md CHANGED
@@ -424,9 +424,11 @@ At resolutions around the \\(1024^2\\) aspect-ratio buckets, a PDG scale near 2.
424
  and a self-attention gain near -0.03 gives substantially better results than
425
  reducing PDG while leaving the gain at zero.
426
 
427
- PDG can be combined with CFG, including CFG with negative prompts. The release
428
- also provides several CFG/PDG interaction modes, constant, linear, and power
429
- PDG curves, and independent start and stop steps for both guidance methods.
 
 
430
  See [API and inference parameters](API.md) for the complete interface.
431
 
432
  ## Dataset
 
424
  and a self-attention gain near -0.03 gives substantially better results than
425
  reducing PDG while leaving the gain at zero.
426
 
427
+ Contrastive PDG conditions the middle-skipped path with either a negative
428
+ prompt or Canter's learned unconditional text. PDG can also be combined with
429
+ CFG. The release provides several CFG/PDG interaction modes, constant, linear,
430
+ and power PDG curves, and independent start and stop steps for both guidance
431
+ methods.
432
  See [API and inference parameters](API.md) for the complete interface.
433
 
434
  ## Dataset
canter/__init__.py CHANGED
@@ -16,9 +16,11 @@ from .inference import (
16
  CanterLatentOutput,
17
  CanterVelocityModel,
18
  CfgGuidance,
 
19
  PdgCurve,
20
  PdgGuidance,
21
  PdgMode,
 
22
  )
23
  from .loading import (
24
  CanterComponents,
@@ -79,6 +81,7 @@ __all__ = [
79
  "CfgGuidance",
80
  "LatentRgbPreviewer",
81
  "LatentRgbProjection",
 
82
  "PdgCurve",
83
  "PdgGuidance",
84
  "PdgMode",
@@ -92,5 +95,6 @@ __all__ = [
92
  "WeightDType",
93
  "__version__",
94
  "build_schedule",
 
95
  "solve",
96
  ]
 
16
  CanterLatentOutput,
17
  CanterVelocityModel,
18
  CfgGuidance,
19
+ PdgBranchConditioning,
20
  PdgCurve,
21
  PdgGuidance,
22
  PdgMode,
23
+ pdg_branch_conditioning,
24
  )
25
  from .loading import (
26
  CanterComponents,
 
81
  "CfgGuidance",
82
  "LatentRgbPreviewer",
83
  "LatentRgbProjection",
84
+ "PdgBranchConditioning",
85
  "PdgCurve",
86
  "PdgGuidance",
87
  "PdgMode",
 
95
  "WeightDType",
96
  "__version__",
97
  "build_schedule",
98
+ "pdg_branch_conditioning",
99
  "solve",
100
  ]
canter/inference.py CHANGED
@@ -42,6 +42,7 @@ class PdgMode(Enum):
42
 
43
  NONE = "none"
44
  FULL = "full"
 
45
  THREE_QUARTER = "three_quarter"
46
  ALTERNATE_PDG_FIRST = "alternate_pdg_first"
47
  ALTERNATE_CFG_FIRST = "alternate_cfg_first"
@@ -308,25 +309,38 @@ def _pdg_uses_cfg(mode: PdgMode) -> bool:
308
  | PdgMode.CFG_TO_PDG
309
  ):
310
  return True
311
- case PdgMode.NONE | PdgMode.FULL | PdgMode.THREE_QUARTER:
 
 
 
 
 
312
  return False
313
  case _ as unreachable:
314
  raise RuntimeError(f"Unsupported PDG mode: {unreachable}")
315
 
316
 
317
- def _pdg_tweak_uses_unconditional(mode: PdgMode) -> bool:
318
- """Return whether a single active PDG branch uses unconditional text."""
 
 
 
 
 
 
 
319
 
320
  match mode:
321
  case (
322
- PdgMode.ALTERNATE_PDG_FIRST
 
323
  | PdgMode.ALTERNATE_CFG_FIRST
324
  | PdgMode.COMBINED_CFG_PDG
325
  | PdgMode.PDG_WITH_ALTERNATING_CFG
326
  ):
327
- return True
328
  case PdgMode.NONE | PdgMode.FULL | PdgMode.THREE_QUARTER | PdgMode.CFG_TO_PDG:
329
- return False
330
  case _ as unreachable:
331
  raise RuntimeError(f"Unsupported PDG mode: {unreachable}")
332
 
@@ -406,7 +420,8 @@ class CanterGuidedVelocity:
406
  if active.pdg:
407
  tweak_text = (
408
  self._unconditional()
409
- if _pdg_tweak_uses_unconditional(self.config.pdg.mode)
 
410
  else self.conditional
411
  )
412
  tweak = self._predict(
@@ -433,7 +448,12 @@ class CanterGuidedVelocity:
433
  pdg_active = self.config.pdg.enabled and _inside(step, self.pdg_window)
434
  mode = self.config.pdg.mode
435
  match mode:
436
- case PdgMode.NONE | PdgMode.FULL | PdgMode.THREE_QUARTER:
 
 
 
 
 
437
  return _GuidanceStep(cfg_active, pdg_active, mode)
438
  case PdgMode.ALTERNATE_PDG_FIRST:
439
  if pdg_active:
@@ -520,7 +540,7 @@ class CanterGuidedVelocity:
520
 
521
  if self.unconditional is None:
522
  raise RuntimeError(
523
- "An active CFG-dependent branch is missing unconditional text."
524
  )
525
  return self.unconditional
526
 
@@ -529,7 +549,7 @@ class CanterGuidedVelocity:
529
  """Map a concrete public PDG mode to one frozen model path."""
530
 
531
  match mode:
532
- case PdgMode.FULL:
533
  return CanterPath.SKIP_MIDDLE
534
  case PdgMode.THREE_QUARTER:
535
  return CanterPath.THREE_QUARTER
@@ -549,7 +569,12 @@ def _needs_unconditional_text(config: CanterGuidanceConfig) -> bool:
549
  """Return whether any configured inference step can use unconditional text."""
550
 
551
  return bool(config.cfg.enabled) or (
552
- bool(config.pdg.enabled) and _pdg_uses_cfg(config.pdg.mode)
 
 
 
 
 
553
  )
554
 
555
 
@@ -793,5 +818,7 @@ def _negative_prompt_batch(
793
  if _prompt_batch_is_blank(values):
794
  return None
795
  if not required:
796
- raise ValueError("Negative prompts require an active CFG branch.")
 
 
797
  return values
 
42
 
43
  NONE = "none"
44
  FULL = "full"
45
+ FULL_CONTRASTIVE = "full_contrastive"
46
  THREE_QUARTER = "three_quarter"
47
  ALTERNATE_PDG_FIRST = "alternate_pdg_first"
48
  ALTERNATE_CFG_FIRST = "alternate_cfg_first"
 
309
  | PdgMode.CFG_TO_PDG
310
  ):
311
  return True
312
+ case (
313
+ PdgMode.NONE
314
+ | PdgMode.FULL
315
+ | PdgMode.FULL_CONTRASTIVE
316
+ | PdgMode.THREE_QUARTER
317
+ ):
318
  return False
319
  case _ as unreachable:
320
  raise RuntimeError(f"Unsupported PDG mode: {unreachable}")
321
 
322
 
323
+ class PdgBranchConditioning(Enum):
324
+ """Text conditioning supplied to an active PDG alternative path."""
325
+
326
+ POSITIVE = "positive"
327
+ CONTRASTIVE = "contrastive"
328
+
329
+
330
+ def pdg_branch_conditioning(mode: PdgMode) -> PdgBranchConditioning:
331
+ """Return the text branch used by an active PDG alternative path."""
332
 
333
  match mode:
334
  case (
335
+ PdgMode.FULL_CONTRASTIVE
336
+ | PdgMode.ALTERNATE_PDG_FIRST
337
  | PdgMode.ALTERNATE_CFG_FIRST
338
  | PdgMode.COMBINED_CFG_PDG
339
  | PdgMode.PDG_WITH_ALTERNATING_CFG
340
  ):
341
+ return PdgBranchConditioning.CONTRASTIVE
342
  case PdgMode.NONE | PdgMode.FULL | PdgMode.THREE_QUARTER | PdgMode.CFG_TO_PDG:
343
+ return PdgBranchConditioning.POSITIVE
344
  case _ as unreachable:
345
  raise RuntimeError(f"Unsupported PDG mode: {unreachable}")
346
 
 
420
  if active.pdg:
421
  tweak_text = (
422
  self._unconditional()
423
+ if pdg_branch_conditioning(self.config.pdg.mode)
424
+ is PdgBranchConditioning.CONTRASTIVE
425
  else self.conditional
426
  )
427
  tweak = self._predict(
 
448
  pdg_active = self.config.pdg.enabled and _inside(step, self.pdg_window)
449
  mode = self.config.pdg.mode
450
  match mode:
451
+ case (
452
+ PdgMode.NONE
453
+ | PdgMode.FULL
454
+ | PdgMode.FULL_CONTRASTIVE
455
+ | PdgMode.THREE_QUARTER
456
+ ):
457
  return _GuidanceStep(cfg_active, pdg_active, mode)
458
  case PdgMode.ALTERNATE_PDG_FIRST:
459
  if pdg_active:
 
540
 
541
  if self.unconditional is None:
542
  raise RuntimeError(
543
+ "An active contrastive branch is missing negative or unconditional text."
544
  )
545
  return self.unconditional
546
 
 
549
  """Map a concrete public PDG mode to one frozen model path."""
550
 
551
  match mode:
552
+ case PdgMode.FULL | PdgMode.FULL_CONTRASTIVE:
553
  return CanterPath.SKIP_MIDDLE
554
  case PdgMode.THREE_QUARTER:
555
  return CanterPath.THREE_QUARTER
 
569
  """Return whether any configured inference step can use unconditional text."""
570
 
571
  return bool(config.cfg.enabled) or (
572
+ bool(config.pdg.enabled)
573
+ and (
574
+ _pdg_uses_cfg(config.pdg.mode)
575
+ or pdg_branch_conditioning(config.pdg.mode)
576
+ is PdgBranchConditioning.CONTRASTIVE
577
+ )
578
  )
579
 
580
 
 
818
  if _prompt_batch_is_blank(values):
819
  return None
820
  if not required:
821
+ raise ValueError(
822
+ "Negative prompts require active CFG or contrastive PDG guidance."
823
+ )
824
  return values
canter/version.py CHANGED
@@ -1,4 +1,4 @@
1
  """Single source of truth for the installable Canter code version."""
2
 
3
- __version__ = "0.4.1"
4
  CANTER_VERSION = __version__
 
1
  """Single source of truth for the installable Canter code version."""
2
 
3
+ __version__ = "0.5.0"
4
  CANTER_VERSION = __version__
canter/webui_app.py CHANGED
@@ -28,6 +28,7 @@ from .webui_runtime import (
28
  _unloaded_model_status,
29
  random_seed,
30
  snap_image_dimension,
 
31
  update_pdg_clean_scale,
32
  )
33
 
@@ -49,6 +50,7 @@ _SCHEDULE_CHOICES = (
49
  )
50
  _PDG_MODE_CHOICES = (
51
  ("Full path drop", PdgMode.FULL.value),
 
52
  ("Three-quarter path", PdgMode.THREE_QUARTER.value),
53
  ("Alternate: PDG first", PdgMode.ALTERNATE_PDG_FIRST.value),
54
  ("Alternate: CFG first", PdgMode.ALTERNATE_CFG_FIRST.value),
@@ -333,6 +335,7 @@ class _BasicInputs:
333
  """Prompt and basic generation controls rendered above advanced settings."""
334
 
335
  prompt: gr.Textbox
 
336
  image_count: gr.Number
337
  size_preset: gr.Dropdown
338
  preview_enabled: gr.Checkbox
@@ -370,6 +373,7 @@ class _WebInputs:
370
  pdg_power: gr.Slider
371
  pdg_start_step: gr.Number
372
  pdg_stop_step: gr.Textbox
 
373
 
374
  def ordered(self) -> tuple[gr.Component, ...]:
375
  """Return components in the order accepted by ``generate``."""
@@ -399,6 +403,7 @@ class _WebInputs:
399
  self.pdg_start_step,
400
  self.pdg_stop_step,
401
  self.preview_enabled,
 
402
  )
403
 
404
 
@@ -441,10 +446,14 @@ def _prompt_input() -> gr.Textbox:
441
  )
442
 
443
 
444
- def _basic_inputs(prompt: gr.Textbox) -> _BasicInputs:
 
 
 
445
  """Render prompt, image-count, preset, shape, and seed controls."""
446
 
447
  prompt.render()
 
448
  image_count = gr.Number(
449
  minimum=1,
450
  maximum=_MAX_IMAGE_COUNT,
@@ -488,6 +497,7 @@ def _basic_inputs(prompt: gr.Textbox) -> _BasicInputs:
488
  )
489
  return _BasicInputs(
490
  prompt=prompt,
 
491
  image_count=image_count,
492
  size_preset=size_preset,
493
  preview_enabled=preview_enabled,
@@ -729,6 +739,19 @@ def _pdg_inputs() -> tuple[
729
  return enabled, mode, curve, noisy_scale, clean_scale, power, start, stop
730
 
731
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
  def _create_inputs(
733
  basic: _BasicInputs,
734
  ) -> _WebInputs:
@@ -782,6 +805,7 @@ def _create_inputs(
782
  pdg_power=pdg_power,
783
  pdg_start_step=pdg_start,
784
  pdg_stop_step=pdg_stop,
 
785
  )
786
 
787
 
@@ -1053,7 +1077,8 @@ def _create_workspace(
1053
  with gr.Row(elem_id="canter-workspace"):
1054
  with gr.Column(scale=3, min_width=360, elem_id="canter-controls"):
1055
  prompt = _prompt_input()
1056
- basic = _basic_inputs(prompt)
 
1057
  with gr.Row():
1058
  generate = gr.Button(
1059
  "Generate",
@@ -1157,6 +1182,15 @@ def _connect_control_callbacks(
1157
  api_name=None,
1158
  api_visibility="private",
1159
  )
 
 
 
 
 
 
 
 
 
1160
  controls.api.click(
1161
  fn=None,
1162
  inputs=None,
 
28
  _unloaded_model_status,
29
  random_seed,
30
  snap_image_dimension,
31
+ update_negative_prompt_visibility,
32
  update_pdg_clean_scale,
33
  )
34
 
 
50
  )
51
  _PDG_MODE_CHOICES = (
52
  ("Full path drop", PdgMode.FULL.value),
53
+ ("Contrastive PDG", PdgMode.FULL_CONTRASTIVE.value),
54
  ("Three-quarter path", PdgMode.THREE_QUARTER.value),
55
  ("Alternate: PDG first", PdgMode.ALTERNATE_PDG_FIRST.value),
56
  ("Alternate: CFG first", PdgMode.ALTERNATE_CFG_FIRST.value),
 
335
  """Prompt and basic generation controls rendered above advanced settings."""
336
 
337
  prompt: gr.Textbox
338
+ negative_prompt: gr.Textbox
339
  image_count: gr.Number
340
  size_preset: gr.Dropdown
341
  preview_enabled: gr.Checkbox
 
373
  pdg_power: gr.Slider
374
  pdg_start_step: gr.Number
375
  pdg_stop_step: gr.Textbox
376
+ negative_prompt: gr.Textbox
377
 
378
  def ordered(self) -> tuple[gr.Component, ...]:
379
  """Return components in the order accepted by ``generate``."""
 
403
  self.pdg_start_step,
404
  self.pdg_stop_step,
405
  self.preview_enabled,
406
+ self.negative_prompt,
407
  )
408
 
409
 
 
446
  )
447
 
448
 
449
+ def _basic_inputs(
450
+ prompt: gr.Textbox,
451
+ negative_prompt: gr.Textbox,
452
+ ) -> _BasicInputs:
453
  """Render prompt, image-count, preset, shape, and seed controls."""
454
 
455
  prompt.render()
456
+ negative_prompt.render()
457
  image_count = gr.Number(
458
  minimum=1,
459
  maximum=_MAX_IMAGE_COUNT,
 
497
  )
498
  return _BasicInputs(
499
  prompt=prompt,
500
+ negative_prompt=negative_prompt,
501
  image_count=image_count,
502
  size_preset=size_preset,
503
  preview_enabled=preview_enabled,
 
739
  return enabled, mode, curve, noisy_scale, clean_scale, power, start, stop
740
 
741
 
742
+ def _negative_prompt_input() -> gr.Textbox:
743
+ """Create an initially hidden negative prompt below the positive prompt."""
744
+
745
+ return gr.Textbox(
746
+ value="",
747
+ label="Negative prompt",
748
+ lines=3,
749
+ visible=False,
750
+ interactive=False,
751
+ render=False,
752
+ )
753
+
754
+
755
  def _create_inputs(
756
  basic: _BasicInputs,
757
  ) -> _WebInputs:
 
805
  pdg_power=pdg_power,
806
  pdg_start_step=pdg_start,
807
  pdg_stop_step=pdg_stop,
808
+ negative_prompt=basic.negative_prompt,
809
  )
810
 
811
 
 
1077
  with gr.Row(elem_id="canter-workspace"):
1078
  with gr.Column(scale=3, min_width=360, elem_id="canter-controls"):
1079
  prompt = _prompt_input()
1080
+ negative_prompt = _negative_prompt_input()
1081
+ basic = _basic_inputs(prompt, negative_prompt)
1082
  with gr.Row():
1083
  generate = gr.Button(
1084
  "Generate",
 
1182
  api_name=None,
1183
  api_visibility="private",
1184
  )
1185
+ for component in (inputs.cfg_enabled, inputs.pdg_enabled, inputs.pdg_mode):
1186
+ component.change(
1187
+ fn=update_negative_prompt_visibility,
1188
+ inputs=(inputs.cfg_enabled, inputs.pdg_enabled, inputs.pdg_mode),
1189
+ outputs=inputs.negative_prompt,
1190
+ queue=False,
1191
+ api_name=None,
1192
+ api_visibility="private",
1193
+ )
1194
  controls.api.click(
1195
  fn=None,
1196
  inputs=None,
canter/webui_runtime.py CHANGED
@@ -22,9 +22,11 @@ from PIL import Image
22
  from .inference import (
23
  CanterInferenceConfig,
24
  CfgGuidance,
 
25
  PdgCurve,
26
  PdgGuidance,
27
  PdgMode,
 
28
  )
29
  from .pipeline import (
30
  CanterOutputType,
@@ -64,6 +66,7 @@ class _ImagePipeline(Protocol):
64
  self,
65
  prompts: str | Sequence[str],
66
  *,
 
67
  config: CanterPipelineConfig,
68
  initial_noise: None,
69
  progress: SolverProgress | None,
@@ -84,6 +87,7 @@ class CanterWebRequest:
84
  """Validated sequential image request and frozen base configuration."""
85
 
86
  prompt: str
 
87
  image_count: int
88
  config: CanterPipelineConfig
89
 
@@ -287,6 +291,7 @@ class CanterWebController:
287
  pdg_start_step: int | float,
288
  pdg_stop_step: int | float | str | None,
289
  preview_enabled: bool,
 
290
  browser_request: gr.Request | None = None,
291
  ) -> Generator[_GenerationUpdate, None, None]:
292
  """Yield each batch-one image as soon as its decoding completes."""
@@ -316,6 +321,7 @@ class CanterWebController:
316
  pdg_power=pdg_power,
317
  pdg_start_step=pdg_start_step,
318
  pdg_stop_step=pdg_stop_step,
 
319
  )
320
  _log_generation_request(request)
321
  pipeline = self._ensure_loaded()
@@ -353,6 +359,7 @@ class CanterWebController:
353
  image_config = _image_config(request, image_index)
354
  output = pipeline(
355
  request.prompt,
 
356
  config=image_config,
357
  initial_noise=None,
358
  progress=_step_progress(
@@ -395,6 +402,7 @@ class CanterWebController:
395
  image = _attach_png_metadata(
396
  output.images[0],
397
  prompt=request.prompt,
 
398
  config=image_config,
399
  metadata=pipeline.metadata,
400
  )
@@ -812,6 +820,21 @@ def _validated_prompt(prompt: str) -> str:
812
  return prompt
813
 
814
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  def _pdg_mode_uses_cfg(mode: PdgMode) -> bool:
816
  """Return whether one public PDG mode consumes the CFG scale."""
817
 
@@ -824,7 +847,7 @@ def _pdg_mode_uses_cfg(mode: PdgMode) -> bool:
824
  | PdgMode.CFG_TO_PDG
825
  ):
826
  return True
827
- case PdgMode.FULL | PdgMode.THREE_QUARTER:
828
  return False
829
  case PdgMode.NONE:
830
  raise ValueError("The enabled web PDG selection cannot use mode none.")
@@ -832,6 +855,29 @@ def _pdg_mode_uses_cfg(mode: PdgMode) -> bool:
832
  raise RuntimeError(f"Unsupported PDG mode: {unreachable}")
833
 
834
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
835
  def update_pdg_clean_scale(
836
  curve: str,
837
  noisy_scale: int | float,
@@ -876,12 +922,25 @@ def build_web_request(
876
  pdg_power: int | float,
877
  pdg_start_step: int | float,
878
  pdg_stop_step: int | float | str | None,
 
879
  ) -> CanterWebRequest:
880
  """Validate raw Gradio values and construct the frozen Canter dataclasses."""
881
 
882
  resolved_pdg_mode = _choice(pdg_mode, _PDG_MODES, "PDG mode")
883
  active_pdg_mode = resolved_pdg_mode if pdg_enabled else PdgMode.NONE
884
  uses_cfg_scale = pdg_enabled and _pdg_mode_uses_cfg(resolved_pdg_mode)
 
 
 
 
 
 
 
 
 
 
 
 
885
  resolved_cfg_scale = (
886
  _number(cfg_scale, "CFG scale") if cfg_enabled or uses_cfg_scale else None
887
  )
@@ -952,6 +1011,7 @@ def build_web_request(
952
  )
953
  return CanterWebRequest(
954
  prompt=_validated_prompt(prompt),
 
955
  image_count=resolved_image_count,
956
  config=CanterPipelineConfig(
957
  inference=inference,
@@ -1006,6 +1066,7 @@ def _cfg_metadata(cfg: CfgGuidance) -> dict[str, object]:
1006
  def _png_metadata_json(
1007
  *,
1008
  prompt: str,
 
1009
  config: CanterPipelineConfig,
1010
  metadata: CanterPipelineMetadata,
1011
  ) -> str:
@@ -1016,6 +1077,7 @@ def _png_metadata_json(
1016
  raise RuntimeError("PNG metadata requires the resolved per-image seed.")
1017
  payload: dict[str, object] = {
1018
  "prompt": prompt,
 
1019
  "seed": inference.seed,
1020
  "width": inference.width,
1021
  "height": inference.height,
@@ -1038,6 +1100,7 @@ def _attach_png_metadata(
1038
  image: Image.Image,
1039
  *,
1040
  prompt: str,
 
1041
  config: CanterPipelineConfig,
1042
  metadata: CanterPipelineMetadata,
1043
  ) -> Image.Image:
@@ -1045,6 +1108,7 @@ def _attach_png_metadata(
1045
 
1046
  image.info[_PNG_METADATA_KEY] = _png_metadata_json(
1047
  prompt=prompt,
 
1048
  config=config,
1049
  metadata=metadata,
1050
  )
 
22
  from .inference import (
23
  CanterInferenceConfig,
24
  CfgGuidance,
25
+ PdgBranchConditioning,
26
  PdgCurve,
27
  PdgGuidance,
28
  PdgMode,
29
+ pdg_branch_conditioning,
30
  )
31
  from .pipeline import (
32
  CanterOutputType,
 
66
  self,
67
  prompts: str | Sequence[str],
68
  *,
69
+ negative_prompts: str | Sequence[str] | None,
70
  config: CanterPipelineConfig,
71
  initial_noise: None,
72
  progress: SolverProgress | None,
 
87
  """Validated sequential image request and frozen base configuration."""
88
 
89
  prompt: str
90
+ negative_prompt: str | None
91
  image_count: int
92
  config: CanterPipelineConfig
93
 
 
291
  pdg_start_step: int | float,
292
  pdg_stop_step: int | float | str | None,
293
  preview_enabled: bool,
294
+ negative_prompt: str = "",
295
  browser_request: gr.Request | None = None,
296
  ) -> Generator[_GenerationUpdate, None, None]:
297
  """Yield each batch-one image as soon as its decoding completes."""
 
321
  pdg_power=pdg_power,
322
  pdg_start_step=pdg_start_step,
323
  pdg_stop_step=pdg_stop_step,
324
+ negative_prompt=negative_prompt,
325
  )
326
  _log_generation_request(request)
327
  pipeline = self._ensure_loaded()
 
359
  image_config = _image_config(request, image_index)
360
  output = pipeline(
361
  request.prompt,
362
+ negative_prompts=request.negative_prompt,
363
  config=image_config,
364
  initial_noise=None,
365
  progress=_step_progress(
 
402
  image = _attach_png_metadata(
403
  output.images[0],
404
  prompt=request.prompt,
405
+ negative_prompt=request.negative_prompt,
406
  config=image_config,
407
  metadata=pipeline.metadata,
408
  )
 
820
  return prompt
821
 
822
 
823
+ def _validated_negative_prompt(prompt: str, *, enabled: bool) -> str | None:
824
+ """Resolve blank negative text to learned unconditional conditioning."""
825
+
826
+ if not isinstance(prompt, str):
827
+ raise TypeError("negative_prompt must be a string.")
828
+ if not prompt.strip():
829
+ return None
830
+ if not enabled:
831
+ raise ValueError(
832
+ "A negative prompt requires active CFG or a PDG mode that consumes "
833
+ "contrastive text."
834
+ )
835
+ return prompt
836
+
837
+
838
  def _pdg_mode_uses_cfg(mode: PdgMode) -> bool:
839
  """Return whether one public PDG mode consumes the CFG scale."""
840
 
 
847
  | PdgMode.CFG_TO_PDG
848
  ):
849
  return True
850
+ case PdgMode.FULL | PdgMode.FULL_CONTRASTIVE | PdgMode.THREE_QUARTER:
851
  return False
852
  case PdgMode.NONE:
853
  raise ValueError("The enabled web PDG selection cannot use mode none.")
 
855
  raise RuntimeError(f"Unsupported PDG mode: {unreachable}")
856
 
857
 
858
+ def update_negative_prompt_visibility(
859
+ cfg_enabled: bool,
860
+ pdg_enabled: bool,
861
+ pdg_mode: str,
862
+ ) -> dict[str, object]:
863
+ """Show negative text only when the selected guidance can consume it."""
864
+
865
+ resolved_mode = _choice(pdg_mode, _PDG_MODES, "PDG mode")
866
+ visible = bool(cfg_enabled) or (
867
+ bool(pdg_enabled)
868
+ and (
869
+ _pdg_mode_uses_cfg(resolved_mode)
870
+ or pdg_branch_conditioning(resolved_mode)
871
+ is PdgBranchConditioning.CONTRASTIVE
872
+ )
873
+ )
874
+ match visible:
875
+ case True:
876
+ return gr.update(visible=True, interactive=True)
877
+ case False:
878
+ return gr.update(value="", visible=False, interactive=False)
879
+
880
+
881
  def update_pdg_clean_scale(
882
  curve: str,
883
  noisy_scale: int | float,
 
922
  pdg_power: int | float,
923
  pdg_start_step: int | float,
924
  pdg_stop_step: int | float | str | None,
925
+ negative_prompt: str = "",
926
  ) -> CanterWebRequest:
927
  """Validate raw Gradio values and construct the frozen Canter dataclasses."""
928
 
929
  resolved_pdg_mode = _choice(pdg_mode, _PDG_MODES, "PDG mode")
930
  active_pdg_mode = resolved_pdg_mode if pdg_enabled else PdgMode.NONE
931
  uses_cfg_scale = pdg_enabled and _pdg_mode_uses_cfg(resolved_pdg_mode)
932
+ uses_negative_prompt = cfg_enabled or (
933
+ pdg_enabled
934
+ and (
935
+ uses_cfg_scale
936
+ or pdg_branch_conditioning(resolved_pdg_mode)
937
+ is PdgBranchConditioning.CONTRASTIVE
938
+ )
939
+ )
940
+ resolved_negative_prompt = _validated_negative_prompt(
941
+ negative_prompt,
942
+ enabled=uses_negative_prompt,
943
+ )
944
  resolved_cfg_scale = (
945
  _number(cfg_scale, "CFG scale") if cfg_enabled or uses_cfg_scale else None
946
  )
 
1011
  )
1012
  return CanterWebRequest(
1013
  prompt=_validated_prompt(prompt),
1014
+ negative_prompt=resolved_negative_prompt,
1015
  image_count=resolved_image_count,
1016
  config=CanterPipelineConfig(
1017
  inference=inference,
 
1066
  def _png_metadata_json(
1067
  *,
1068
  prompt: str,
1069
+ negative_prompt: str | None,
1070
  config: CanterPipelineConfig,
1071
  metadata: CanterPipelineMetadata,
1072
  ) -> str:
 
1077
  raise RuntimeError("PNG metadata requires the resolved per-image seed.")
1078
  payload: dict[str, object] = {
1079
  "prompt": prompt,
1080
+ "negative_prompt": negative_prompt,
1081
  "seed": inference.seed,
1082
  "width": inference.width,
1083
  "height": inference.height,
 
1100
  image: Image.Image,
1101
  *,
1102
  prompt: str,
1103
+ negative_prompt: str | None,
1104
  config: CanterPipelineConfig,
1105
  metadata: CanterPipelineMetadata,
1106
  ) -> Image.Image:
 
1108
 
1109
  image.info[_PNG_METADATA_KEY] = _png_metadata_json(
1110
  prompt=prompt,
1111
+ negative_prompt=negative_prompt,
1112
  config=config,
1113
  metadata=metadata,
1114
  )
dist/canter-0.5.0-py3-none-any.whl ADDED
Binary file (93.8 kB). View file