Gabriel's picture
parity with local: drop theme override; blank-line paragraphs; negative line-gap; latest studio src
7a12fb1 verified
Raw
History Blame Contribute Delete
5.81 kB
"""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,
)