Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
# ===== LINE 1: MUST BE SPACES FOR ZEROGPU =====
|
| 2 |
try:
|
| 3 |
import spaces
|
|
@@ -127,20 +128,28 @@ SCENES = [
|
|
| 127 |
"bedroom with white sheets and linen curtains",
|
| 128 |
]
|
| 129 |
|
| 130 |
-
|
|
|
|
| 131 |
"black lace lingerie set",
|
| 132 |
-
"unbuttoned oversized white shirt,
|
| 133 |
-
"
|
| 134 |
"black latex corset and sheer stockings",
|
| 135 |
"sheer slip dress",
|
| 136 |
-
"white towel
|
| 137 |
"cropped knit sweater and panties",
|
| 138 |
"string bikini",
|
| 139 |
-
"black blazer
|
| 140 |
-
"
|
| 141 |
-
"fully nude",
|
| 142 |
-
"fully nude, gold body chain",
|
| 143 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
POSES = [
|
| 146 |
"caught mid task, glancing at the camera",
|
|
@@ -185,7 +194,7 @@ FACES = [
|
|
| 185 |
"monolid eyes, glossy lips", "heavy lashes, sultry stare", "freckles, bright eyes",
|
| 186 |
"beauty mark under one eye", "strong brows, calm gaze",
|
| 187 |
]
|
| 188 |
-
DEFAULT_FACE = "adult Japanese woman late 20s, long straight black hair, soft almond eyes, full lips"
|
| 189 |
|
| 190 |
# Tease then reveal: frame 1 states
|
| 191 |
TEASE_STATES = [
|
|
@@ -195,15 +204,16 @@ TEASE_STATES = [
|
|
| 195 |
"towel slipping down",
|
| 196 |
]
|
| 197 |
|
| 198 |
-
FIELDS = ["chef", "style", "scene", "outfit", "pose", "light", "camera", "woman"]
|
| 199 |
|
| 200 |
# ---------------------------------------------------------------------------
|
| 201 |
# Hard locks
|
| 202 |
# ---------------------------------------------------------------------------
|
| 203 |
ADULT_TAGS = "masterpiece, best quality, 1girl, solo, adult woman 21+, late 20s, fictional character"
|
|
|
|
| 204 |
BODY_LOCK = (
|
| 205 |
"tall, slim body, small frame, tiny waist, "
|
| 206 |
-
"
|
| 207 |
)
|
| 208 |
|
| 209 |
BASE_NEGATIVES = (
|
|
@@ -214,7 +224,8 @@ BASE_NEGATIVES = (
|
|
| 214 |
"fat, chubby, obese, overweight, belly fat, thick thighs, thick body, "
|
| 215 |
"beard, mustache, penis, dick, balls, "
|
| 216 |
"bad hands, extra fingers, deformed, "
|
| 217 |
-
"blurry, lowres, watermark, text, censored, ugly, celebrity, real person"
|
|
|
|
| 218 |
)
|
| 219 |
|
| 220 |
FORBIDDEN_KEYWORDS = []
|
|
@@ -254,7 +265,7 @@ def check_prompt_safety(prompt: str):
|
|
| 254 |
# ---------------------------------------------------------------------------
|
| 255 |
def random_woman(rng=random):
|
| 256 |
return (
|
| 257 |
-
f"adult {rng.choice(ETHNICITIES)} woman late 20s, "
|
| 258 |
f"{rng.choice(HAIRS)}, {rng.choice(FACES)}"
|
| 259 |
)
|
| 260 |
|
|
@@ -283,7 +294,8 @@ def roll_recipe(prev=None, locks=None, new_woman=True, rng=random):
|
|
| 283 |
else:
|
| 284 |
recipe["chef"] = rng.choice(chef_pool_for_style(recipe["style"]))
|
| 285 |
recipe["scene"] = prev["scene"] if keep("scene") else rng.choice(SCENES)
|
| 286 |
-
recipe["outfit"] = prev["outfit"] if keep("outfit") else rng.choice(
|
|
|
|
| 287 |
recipe["pose"] = prev["pose"] if keep("pose") else rng.choice(POSES)
|
| 288 |
recipe["light"] = prev["light"] if keep("light") else rng.choice(LIGHTS)
|
| 289 |
recipe["camera"] = prev["camera"] if keep("camera") else rng.choice(CAMERAS)
|
|
@@ -305,12 +317,19 @@ def build_frames(recipe, tease_reveal=False, extra=""):
|
|
| 305 |
"""Return list of (label, positive, negative). Adult tags + body lock always present."""
|
| 306 |
style = STYLES.get(recipe.get("style"), STYLES["Photoreal"])
|
| 307 |
negative = _join(BASE_NEGATIVES, style["neg"])
|
| 308 |
-
common_head = _join(style["prefix"], ADULT_TAGS, recipe.get("woman") or DEFAULT_FACE, BODY_LOCK)
|
| 309 |
tail = _join(recipe.get("scene"), recipe.get("light"), "confident eye contact", extra)
|
| 310 |
|
| 311 |
if not tease_reveal:
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
# Same seed, two frames, both full body so the reveal reads as a pair
|
| 316 |
frame1 = _join(
|
|
@@ -329,7 +348,7 @@ def recipe_card(recipe, seed=None):
|
|
| 329 |
return "_No roll yet. Hit **Spin**._"
|
| 330 |
lines = ["### Recipe"]
|
| 331 |
for field in FIELDS:
|
| 332 |
-
lines.append(f"- **{field.title()}**: {recipe.get(field, '')}")
|
| 333 |
if seed is not None:
|
| 334 |
lines.append(f"- **Seed**: {seed}")
|
| 335 |
return "\n".join(lines)
|
|
@@ -586,7 +605,7 @@ def create_ui():
|
|
| 586 |
with gr.Blocks(title="Roulette Lab") as demo:
|
| 587 |
gr.Markdown(
|
| 588 |
"# Roulette Lab\n"
|
| 589 |
-
"Spin a random recipe. Lock what you like, spin again, and only the unlocked slots change. "
|
| 590 |
"Fictional adult women 21+ only. Visitor images are not saved unless the Space secret "
|
| 591 |
"`LOG_VISITOR_UPLOADS=1` is set."
|
| 592 |
)
|
|
@@ -601,9 +620,9 @@ def create_ui():
|
|
| 601 |
preview_btn = gr.Button("Spin card only (no render)")
|
| 602 |
card = gr.Markdown(recipe_card({}))
|
| 603 |
gr.Markdown("**Lock slots** (locked slots survive the next spin)")
|
| 604 |
-
lock_boxes = [gr.Checkbox(label=f"Lock {f}", value=False) for f in FIELDS]
|
| 605 |
new_woman = gr.Checkbox(label="New woman each roll", value=True)
|
| 606 |
-
tease_reveal = gr.Checkbox(label="Tease then reveal (same seed, 2 frames)", value=False)
|
| 607 |
extra = gr.Textbox(label="Extra tags (optional)", placeholder="e.g. glossy lips, rain on skin")
|
| 608 |
with gr.Accordion("Settings", open=False):
|
| 609 |
size_name = gr.Radio(list(SIZE_PRESETS), value="Portrait 832x1216", label="Size")
|
|
@@ -647,4 +666,5 @@ if __name__ == "__main__":
|
|
| 647 |
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
|
| 648 |
app = create_ui()
|
| 649 |
app.queue(max_size=12)
|
| 650 |
-
app.launch(ssr_mode=False)
|
|
|
|
|
|
| 1 |
+
```python
|
| 2 |
# ===== LINE 1: MUST BE SPACES FOR ZEROGPU =====
|
| 3 |
try:
|
| 4 |
import spaces
|
|
|
|
| 128 |
"bedroom with white sheets and linen curtains",
|
| 129 |
]
|
| 130 |
|
| 131 |
+
# Every spin makes a pair with the same seed: one clothed, one nude
|
| 132 |
+
CLOTHED_OUTFITS = [
|
| 133 |
"black lace lingerie set",
|
| 134 |
+
"unbuttoned oversized white shirt, deep cleavage",
|
| 135 |
+
"silk robe, deep cleavage",
|
| 136 |
"black latex corset and sheer stockings",
|
| 137 |
"sheer slip dress",
|
| 138 |
+
"white towel wrapped around her",
|
| 139 |
"cropped knit sweater and panties",
|
| 140 |
"string bikini",
|
| 141 |
+
"black blazer, deep cleavage",
|
| 142 |
+
"tight low cut red dress",
|
|
|
|
|
|
|
| 143 |
]
|
| 144 |
+
NUDE_OUTFITS = [
|
| 145 |
+
"fully nude, bare breasts, nipples",
|
| 146 |
+
"fully nude, gold body chain, bare breasts",
|
| 147 |
+
"topless, bare breasts, denim shorts",
|
| 148 |
+
"completely naked, bare breasts, nipples, nothing covering her",
|
| 149 |
+
"nude except black thigh high stockings, bare breasts",
|
| 150 |
+
"fully nude, wet skin, bare breasts",
|
| 151 |
+
]
|
| 152 |
+
OUTFITS = CLOTHED_OUTFITS
|
| 153 |
|
| 154 |
POSES = [
|
| 155 |
"caught mid task, glancing at the camera",
|
|
|
|
| 194 |
"monolid eyes, glossy lips", "heavy lashes, sultry stare", "freckles, bright eyes",
|
| 195 |
"beauty mark under one eye", "strong brows, calm gaze",
|
| 196 |
]
|
| 197 |
+
DEFAULT_FACE = "adult Japanese Asian woman late 20s, long straight black hair, soft almond eyes, full lips"
|
| 198 |
|
| 199 |
# Tease then reveal: frame 1 states
|
| 200 |
TEASE_STATES = [
|
|
|
|
| 204 |
"towel slipping down",
|
| 205 |
]
|
| 206 |
|
| 207 |
+
FIELDS = ["chef", "style", "scene", "outfit", "nude_look", "pose", "light", "camera", "woman"]
|
| 208 |
|
| 209 |
# ---------------------------------------------------------------------------
|
| 210 |
# Hard locks
|
| 211 |
# ---------------------------------------------------------------------------
|
| 212 |
ADULT_TAGS = "masterpiece, best quality, 1girl, solo, adult woman 21+, late 20s, fictional character"
|
| 213 |
+
ASIAN_LOCK = "beautiful Asian woman, Asian facial features, Asian eyes, dark hair"
|
| 214 |
BODY_LOCK = (
|
| 215 |
"tall, slim body, small frame, tiny waist, "
|
| 216 |
+
"huge breasts, very large breasts, heavy full round breasts, busty, deep cleavage, slender thighs"
|
| 217 |
)
|
| 218 |
|
| 219 |
BASE_NEGATIVES = (
|
|
|
|
| 224 |
"fat, chubby, obese, overweight, belly fat, thick thighs, thick body, "
|
| 225 |
"beard, mustache, penis, dick, balls, "
|
| 226 |
"bad hands, extra fingers, deformed, "
|
| 227 |
+
"blurry, lowres, watermark, text, censored, ugly, celebrity, real person, "
|
| 228 |
+
"caucasian, white woman, european, blonde hair, small breasts, flat chest"
|
| 229 |
)
|
| 230 |
|
| 231 |
FORBIDDEN_KEYWORDS = []
|
|
|
|
| 265 |
# ---------------------------------------------------------------------------
|
| 266 |
def random_woman(rng=random):
|
| 267 |
return (
|
| 268 |
+
f"adult {rng.choice(ETHNICITIES)} Asian woman late 20s, "
|
| 269 |
f"{rng.choice(HAIRS)}, {rng.choice(FACES)}"
|
| 270 |
)
|
| 271 |
|
|
|
|
| 294 |
else:
|
| 295 |
recipe["chef"] = rng.choice(chef_pool_for_style(recipe["style"]))
|
| 296 |
recipe["scene"] = prev["scene"] if keep("scene") else rng.choice(SCENES)
|
| 297 |
+
recipe["outfit"] = prev["outfit"] if keep("outfit") else rng.choice(CLOTHED_OUTFITS)
|
| 298 |
+
recipe["nude_look"] = prev["nude_look"] if keep("nude_look") else rng.choice(NUDE_OUTFITS)
|
| 299 |
recipe["pose"] = prev["pose"] if keep("pose") else rng.choice(POSES)
|
| 300 |
recipe["light"] = prev["light"] if keep("light") else rng.choice(LIGHTS)
|
| 301 |
recipe["camera"] = prev["camera"] if keep("camera") else rng.choice(CAMERAS)
|
|
|
|
| 317 |
"""Return list of (label, positive, negative). Adult tags + body lock always present."""
|
| 318 |
style = STYLES.get(recipe.get("style"), STYLES["Photoreal"])
|
| 319 |
negative = _join(BASE_NEGATIVES, style["neg"])
|
| 320 |
+
common_head = _join(style["prefix"], ADULT_TAGS, ASIAN_LOCK, recipe.get("woman") or DEFAULT_FACE, BODY_LOCK)
|
| 321 |
tail = _join(recipe.get("scene"), recipe.get("light"), "confident eye contact", extra)
|
| 322 |
|
| 323 |
if not tease_reveal:
|
| 324 |
+
# Pair: same seed, same woman, same pose. Frame 1 clothed, frame 2 nude.
|
| 325 |
+
clothed = _join(common_head, recipe.get("outfit") or CLOTHED_OUTFITS[0], "clothed, covered nipples",
|
| 326 |
+
recipe.get("pose"), recipe.get("camera"), tail)
|
| 327 |
+
nude = _join(common_head, recipe.get("nude_look") or NUDE_OUTFITS[0],
|
| 328 |
+
recipe.get("pose"), recipe.get("camera"), tail)
|
| 329 |
+
return [
|
| 330 |
+
("Clothed", clothed, _join(negative, "nude, naked, topless, nipples, bare breasts")),
|
| 331 |
+
("Nude", nude, negative),
|
| 332 |
+
]
|
| 333 |
|
| 334 |
# Same seed, two frames, both full body so the reveal reads as a pair
|
| 335 |
frame1 = _join(
|
|
|
|
| 348 |
return "_No roll yet. Hit **Spin**._"
|
| 349 |
lines = ["### Recipe"]
|
| 350 |
for field in FIELDS:
|
| 351 |
+
lines.append(f"- **{field.replace('_', ' ').title()}**: {recipe.get(field, '')}")
|
| 352 |
if seed is not None:
|
| 353 |
lines.append(f"- **Seed**: {seed}")
|
| 354 |
return "\n".join(lines)
|
|
|
|
| 605 |
with gr.Blocks(title="Roulette Lab") as demo:
|
| 606 |
gr.Markdown(
|
| 607 |
"# Roulette Lab\n"
|
| 608 |
+
"Spin a random recipe. Every spin makes two pictures with the same seed: one clothed, one nude. Lock what you like, spin again, and only the unlocked slots change. "
|
| 609 |
"Fictional adult women 21+ only. Visitor images are not saved unless the Space secret "
|
| 610 |
"`LOG_VISITOR_UPLOADS=1` is set."
|
| 611 |
)
|
|
|
|
| 620 |
preview_btn = gr.Button("Spin card only (no render)")
|
| 621 |
card = gr.Markdown(recipe_card({}))
|
| 622 |
gr.Markdown("**Lock slots** (locked slots survive the next spin)")
|
| 623 |
+
lock_boxes = [gr.Checkbox(label=f"Lock {f.replace('_', ' ')}", value=False) for f in FIELDS]
|
| 624 |
new_woman = gr.Checkbox(label="New woman each roll", value=True)
|
| 625 |
+
tease_reveal = gr.Checkbox(label="Tease then reveal instead of clothed and nude (same seed, 2 frames)", value=False)
|
| 626 |
extra = gr.Textbox(label="Extra tags (optional)", placeholder="e.g. glossy lips, rain on skin")
|
| 627 |
with gr.Accordion("Settings", open=False):
|
| 628 |
size_name = gr.Radio(list(SIZE_PRESETS), value="Portrait 832x1216", label="Size")
|
|
|
|
| 666 |
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
|
| 667 |
app = create_ui()
|
| 668 |
app.queue(max_size=12)
|
| 669 |
+
app.launch(ssr_mode=False)
|
| 670 |
+
```
|