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,
        )