Spaces:
Running on Zero
Running on Zero
File size: 5,806 Bytes
98c0406 7a12fb1 98c0406 7a12fb1 98c0406 7a12fb1 98c0406 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | """Frozen Pydantic value-objects for the studio's UI state — one validated form per generation mode.
The Gradio views collect raw widget values into these forms at the request boundary (see
:mod:`diffu_studio.controls`), so the streaming services in :mod:`diffu_studio.handlers` take ONE typed form
instead of the 24-plus positional arguments the old god-handlers carried. Each form knows how to lower itself
to the real ``diffu-page`` config it drives, so the knob names live in exactly one place.
"""
from __future__ import annotations
import re
from diffu_page.augment import AugmentConfig
from diffu_page.config import GenConfig
from pydantic import BaseModel, Field
from diffu_studio.configs import SampleSettings, build_augment, build_gen_config
from diffu_studio.page import PageRequest
# Presence gates: on a corpus these are "fraction of pages that get the effect", but in the single-page studio
# a probability is meaningless — for one page + one seed the gate either fires or not, so nudging it looks dead.
# The studio forces each to a deterministic on/off (>0 → always apply), so a knob visibly changes THIS page.
# (The seed still re-rolls WHERE/how the effect lands — see the Reroll button.)
_AUGMENT_GATES = frozenset({
"bed_prob", "microfilm_prob", "correction_prob", "stamp_prob", "fold_prob", "dust_prob",
"clasp_prob", "glove_prob", "hole_prob", "curl_prob", "paraph_prob", "margin_mark_prob",
}) # fmt: skip
def _force(prob: float) -> float:
"""A presence probability → a deterministic gate: any positive value always applies, 0 never does."""
return 1.0 if prob > 0 else 0.0
class AugmentForm(BaseModel):
"""The curated scan-look knobs — mirrors :func:`diffu_studio.configs.build_augment` field-for-field."""
model_config = {"frozen": True}
bed_prob: float = 0.7
microfilm_prob: float = 0.12
correction_prob: float = 0.15
stamp_prob: float = 0.12
fold_prob: float = 0.2
dust_prob: float = 0.35
rotate_max: float = 2.5
wash_max: float = 0.35
ink_fade_max: float = 0.18
skew: float = 0.008
ink_color: str | None = None
vignette_max: float = 0.42
bleed_max: float = 0.13
spine_shadow_max: float = 0.72
spine_warp_max: float = 0.15
gutter_blur_max: float = 3.0
edge_damage_max: float = 0.007
clasp_prob: float = 0.2
glove_prob: float = 0.15
hole_prob: float = 0.06
curl_prob: float = 0.15
paraph_prob: float = 0.0
margin_mark_prob: float = 0.0
foxing_tint: str | None = None
def to_augment(self) -> AugmentConfig:
"""Lower to the real :class:`AugmentConfig`, forcing presence gates deterministic (studio single-page)."""
values = self.model_dump()
for name in _AUGMENT_GATES:
values[name] = _force(values[name])
return build_augment(**values)
class LineForm(BaseModel):
"""Single-line generation state — the text plus the shared sampling settings (no model choice)."""
model_config = {"frozen": True}
text: str
settings: SampleSettings = Field(default_factory=SampleSettings)
class PageForm(BaseModel):
"""The whole-page control state — flat knob values plus a nested :class:`AugmentForm`."""
model_config = {"frozen": True}
text: str = ""
seed: int = 0
page_number: str | None = None
xml_layout: str | None = None
formats: list[str] = Field(default_factory=lambda: ["page", "alto", "json"])
steps: int = 24
cfg_scale: float = 5.0
template: str = "single_column"
n_columns: int = 2
width: int = 1240
height: int = 1754
line_height: int = 100
line_gap: float = 0.18
para_gap: float = 0.4
furniture_prob: float = 0.5
typeset_furniture: bool = False
binding_prob: float = 0.4
insertion_prob: float = 0.012
deletion_prob: float = 0.008
augment: AugmentForm = Field(default_factory=AugmentForm)
def gen_config(self) -> GenConfig:
"""Lower the page knobs to a validated :class:`GenConfig` (raises on out-of-range values)."""
return build_gen_config(
width=self.width,
height=self.height,
template=self.template,
n_columns=self.n_columns,
line_height=self.line_height,
line_gap=self.line_gap,
para_gap=self.para_gap,
furniture_prob=_force(
self.furniture_prob
), # deterministic on/off in the studio (see _AUGMENT_GATES)
typeset_furniture=self.typeset_furniture,
binding_prob=_force(self.binding_prob), # "binding prob" → a real toggle for this page
insertion_prob=self.insertion_prob,
deletion_prob=self.deletion_prob,
steps=self.steps,
cfg_scale=self.cfg_scale,
formats=self.formats,
augment=self.augment.to_augment(),
)
def to_request(self, *, animate: bool) -> PageRequest:
"""Assemble the :class:`PageRequest` the page stream consumes.
Paragraphs are separated by BLANK lines (standard prose convention); single newlines flow together
into one paragraph and re-wrap to the layout. (Previously every newline made its own paragraph, so
a few lines of text got a paragraph gap between each — the page looked far airier than intended.)
"""
return PageRequest(
paragraphs=[
" ".join(block.split()) for block in re.split(r"\n\s*\n", self.text) if block.strip()
],
gen=self.gen_config(),
settings=SampleSettings(steps=self.steps, cfg_scale=self.cfg_scale),
seed=self.seed,
page_number=self.page_number or None,
xml_layout=self.xml_layout,
animate=animate,
)
|