import random
RUN_COUNT = 200
class DanbooruTagGenV25_Gravure:
def __init__(self):
# ==========================================
# 1. 팔레트 (v28: pastel·summer·jewel·muted·earth 추가)
# ==========================================
self.palettes = {
"dark": ["black", "dark gray", "charcoal", "dark navy"],
"light": ["white", "cream", "ivory", "beige", "light gray"],
"blue": ["navy", "cobalt blue", "sky blue", "denim blue", "teal"],
"warm": ["red", "burgundy", "wine red", "rust", "brown"],
"pink": ["pink", "hot pink", "rose", "blush pink", "mauve"],
"vivid": ["bright red", "electric blue", "neon pink", "purple", "orange"],
"neutral": ["white", "black", "gray", "beige", "off-white"],
# ── v28 신규 ──
"pastel": ["pastel pink", "pastel blue", "pastel yellow", "lavender", "mint"],
"summer": ["coral", "turquoise", "sunny yellow", "aqua", "peach"],
"jewel": ["sapphire blue", "emerald green", "ruby red", "amethyst purple", "gold"],
"muted": ["dusty pink", "muted blue", "sage green", "dusty lavender", "warm gray"],
"earth": ["olive", "khaki", "tan", "camel", "sage green", "forest green"],
}
# ==========================================
# 2. 소재·패턴·디테일 (v28 통합 ★신규)
# ==========================================
# 2-1. 프리미엄 소재 — 의상 앞에 붙는 확률적 수식
self.premium_materials = ["leather", "silk", "satin", "velvet", "latex", "sheer", "mesh", "lace"]
# 2-2. 패턴 — 의상 앞에 붙는 확률적 수식
self.patterns = [
"plaid", "striped", "floral print", "polka dot", "checkered",
"leopard print", "houndstooth", "argyle", "tie-dye", "snake print",
]
# 2-3. 디테일 — 의상 뒤에 붙는 수식 (get_color_garment 내부에서 사용)
self.details_trim = ["lace trim", "ribbon trim", "bow", "frills", "shirring", "ruffle trim", "lace-up"]
# 2-4. 피트감 — 의상 앞에 붙는 수식 (oversized/loose 계열)
# {fit_prefix} {garment} 형태로 조합
self.fit_oversized = [
"oversized", "oversized", # 빈도 강조
"baggy", "extra large",
]
self.fit_loose = [
"loose", "loose",
"ill-fitting", "flowing",
]
# 피트감 적용 가능한 의류 키워드
self.fit_applicable = [
"shirt", "dress", "hoodie", "sweater", "cardigan",
"blouse", "top", "uniform", "kimono", "yukata",
"pajamas", "negligee", "robe",
]
# ==========================================
# 3. 수영복 풀 — 비키니 / 원피스 서브풀 분리
# ==========================================
self.swimwear_bikini = [
"string bikini", "micro bikini", "side-tie bikini", "bandeau bikini",
"o-ring bikini", "slingshot swimsuit", "highleg bikini", "lowleg bikini",
"front-tie bikini top", "strapless bikini", "sailor bikini",
"single-shoulder bikini", "frilled bikini", "ruffle bikini",
"sports bikini", "layered bikini", "cow print bikini", "striped bikini",
"miku collar bikini", "thong bikini",
# 고유명사 (색상 prefix 없이 사용)
"gold bikini",
# 파츠 분리 — v30: micro bikini 버전으로 통일
"micro bikini top only", "micro bikini bottom only", "tankini", "bandeau",
]
self.swimwear_onepiece = [
"monokini", "strapless one-piece swimsuit", "one-piece swimsuit",
"school swimsuit",
"old school swimsuit", "competition school swimsuit",
"competition swimsuit", "highleg swimsuit",
# 비키니 위에 레이어
"bikini under clothes",
]
self.bikini_tops = [
"string bikini top", "bandeau bikini top", "strapless bikini top",
"front-tie bikini top", "sports bikini top", "frilled bikini top",
"o-ring bikini top", "side-tie bikini top",
]
self.bikini_bottoms = [
"string bikini bottom", "micro bikini bottom", "side-tie bikini bottom",
"highleg bikini bottom", "lowleg bikini bottom", "thong bikini bottom",
"ruffled bikini bottom", "frilled bikini bottom",
]
# ==========================================
# 4. 란제리·속옷·잠옷
# ==========================================
self.bra_pool = [
"lace bra", "string bra", "cupless bra", "front-tie bra", "strapless bra",
"sports bra", "balconette bra", "push-up bra", "sheer bra", "mesh bra",
"open-cup bra",
# v29 신규
"bustier", "corset",
]
self.panties_pool = [
"lace panties", "string panties", "multi-strapped panties",
"side-tie panties", "highleg panties", "thong",
"boyshorts", "mesh panties", "sheer panties", "micro thong",
"crotchless panties", "garter belt",
# v29 신규
"strapless bottom", "g-string", "pearl thong",
]
self.sleepwear_pool = [
"babydoll", "chemise", "negligee", "see-through negligee", "camisole",
"transparent lingerie", "lingerie",
"satin pajamas", "lace nightgown", "slip dress",
"sheer slip", "silk robe",
# v29 신규
"teddy",
]
self.casual_items = [
"oversized shirt", "yukata", "open kimono", "off-shoulder top",
"hoodie", "jersey dress", "knit sweater", "cardigan",
"denim jacket", "robe", "negligee robe", "oversized hoodie",
# v32: 드레스 계열 (그라비아에서 비중 높음)
"bodycon dress", # 몸에 달라붙는 드레스
"halter neck dress",
"wrap dress", # 허리 묶는 스타일
"micro dress", # 초미니
"off-shoulder dress",
"knit dress", # 겨울 그라비아 단골
]
self.naked_items = [
"naked shirt", "naked apron", "naked ribbon", "naked jacket",
"naked sweater", "naked coat", "naked kimono", "naked hoodie",
"naked overalls", "naked cape",
"undone sarashi",
# v30 신규
"fundoshi",
"sarashi only",
"naked blanket",
# v32 신규
"naked tie", # 넥타이만, 오피스레이디 naked 버전
"naked cardigan",
"naked suspenders",
]
self.exclusive_items = [
"bondage tape", "pasties", "body chain", "see-through negligee",
"sheer bodysuit", "micro thong", "cupless bra", "crotchless panties",
"strappy lingerie", "fishnet bodysuit", "transparent lingerie",
"mesh bodysuit", "open-cup bra",
# 신규
"harness", "body harness",
# v30: nipple cover 대표 항목만 유지 (변형은 nipple_cover_pool에서)
"maebari", "bandaid on nipples",
]
# v30: pasties·maebari·nipple-cover 세부 풀
self.pasties_pool = [
"pasties", "heart pasties", "star pasties",
"x pasties", "flower pasties", "circle pasties",
]
self.maebari_pool = [
"maebari", "heart maebari", "star maebari",
]
self.nipple_cover_pool = (
self.pasties_pool + self.maebari_pool + ["bandaid on nipples"]
)
# v30: reverse outfit 서브풀
self.reverse_outfit_pool = [
"reverse leotard",
"reverse school swimsuit",
"reverse bodysuit",
"reverse outfit",
"reverse dress",
]
# ==========================================
# 5. 레그웨어
# ==========================================
self.legwear_pool = [
"thighhighs", "leggings", "bodystocking", "kneehighs",
"over-kneehighs", "fishnet stockings", "stockings", "pantyhose",
"fishnet thighhighs", "black thighhighs", "white thighhighs",
# v29 신규
"fishnets", "garter straps",
]
# garter belt 조합은 색상 래핑 없이 직접 추가 (복합 태그)
self.legwear_garter_combos = [
"garter belt, stockings",
"garter belt, thighhighs",
]
# ==========================================
# 6. 악세사리 (v28 통합 ★신규)
# ==========================================
self.neck_jewelry = ["choker", "necklace", "pearl necklace", "multiple necklaces", "collar", "chain necklace"]
self.earring_types = ["earrings", "hoop earrings", "stud earrings", "drop earrings", "dangling earrings"]
self.wrist_jewelry = ["bracelet", "bangle", "wrist cuffs", "anklet"]
self.hair_acc = ["hair clip", "scrunchie", "hair ornament", "hair ribbon", "hair bow"]
self.shoulder_tags = ["bare shoulders", "off shoulder", "one shoulder"]
self.ribbon_tags = [
"hair bow", "neck ribbon", "bowtie", "waist ribbon", "back bow",
"chest ribbon", "wrist ribbon", "ribbon trim", "hat ribbon",
]
# 카테고리별 악세사리 적용 확률 제어
self.acc_categories = {"swimwear", "lingerie", "sleepwear", "costume", "exclusive", "casual", "naked", "gym", "towel"}
# ==========================================
# 7. 코스튬 데이터 (SF·사이버펑크·mtu_virus 계열 제외)
# ==========================================
self.costumes_data = {
"nurse": (["nurse", "white shirt", "taut clothes", "nurse cap"], 1, ["light", "neutral", "pink"]),
"maid": (["maid", "maid headdress", "white apron", "taut clothes"], 0, ["dark", "light"]),
"police": (["police", "police uniform", "taut shirt", "mini skirt", "peaked cap"], 1, ["dark", "blue"]),
"flight_attendant":(["flight attendant", "stewardess uniform", "pencil skirt", "necktie", "taut clothes"], 1, ["dark", "blue", "warm"]),
"bunny": (["bunny girl", "bunny suit", "fishnet stockings", "bowtie", "bunny ears"], 1, ["dark", "light", "pink", "vivid"]),
"cheerleader": (["cheerleader", "crop top", "pleated skirt", "pom poms"], 1, ["vivid", "warm", "blue", "pink"]),
"harem": (["harem outfit", "arabian clothes", "gold trim"], 0, ["warm", "vivid", "jewel", "pastel"]),
"school_japanese": (["japanese school uniform", "serafuku", "sailor collar", "sailor uniform", "neckerchief", "pleated skirt", "kneehighs"], 5, ["dark", "blue", "neutral", "light"]),
"school_gyaru": (["gyaru", "school uniform", "plaid skirt"], 2, ["warm", "vivid", "earth", "neutral"]),
"office_lady": (["office lady", "business suit", "pencil skirt", "pantyhose", "white shirt", "taut clothes"], 1, ["dark", "neutral", "blue"]),
"miko": (["miko", "miko outfit", "hakama", "japanese clothes", "bare shoulders"], 2, ["warm", "vivid", "dark", "blue"]),
"nun": (["nun", "nun habit", "wimple", "cross necklace", "high slit"], None, ["dark", "neutral"]),
"race_queen": (["race queen", "leotard", "thighhighs", "knee boots"], 1, ["vivid", "blue", "warm"]),
"kunoichi": (["kunoichi", "ninja", "ninja mask", "fishnet bodysuit", "taut clothes"], None, ["dark"]),
# v32 신규 — 스포츠 계열
"tennis_player": (["tennis uniform", "polo shirt", "tennis skirt", "visor cap", "taut clothes"], 0, ["light", "vivid", "pink", "blue", "neutral"]),
"golf_player": (["golf wear", "polo shirt", "golf skirt", "sun visor", "taut clothes"], 0, ["neutral", "vivid", "pastel", "earth", "light"]),
# v30 신규 — reverse 계열
"reverse_bunnysuit": (["reverse bunnysuit", "bunny ears", "bowtie", "fishnet stockings", "wrist cuffs"], 1, ["dark", "light", "pink", "vivid"]),
"reverse_outfit": (["REVERSE_PICK"], None, ["dark", "light", "pink", "vivid", "blue"]),
}
self.costume_weights = {
# 페티시 핵심 ↑
"nurse": 9, "maid": 9, "bunny": 9,
"race_queen": 7, "nun": 7, "miko": 7, "cheerleader": 7,
# 중간
"police": 6, "flight_attendant": 5, "harem": 5,
"kunoichi": 5,
# taut clothes 계열
"office_lady": 4,
# school 계열 (general·sailor 삭제·병합, japanese 6으로 상향)
"school_japanese": 6, "school_gyaru": 4,
# v30 신규
"reverse_bunnysuit": 6, "reverse_outfit": 5,
# v32 신규
"tennis_player": 6, "golf_player": 5,
}
# 코스튬별 전용 악세사리 보너스
self.costume_acc_bonus = {
"maid": ["hair ribbon", "hair bow"],
"nurse": [],
"bunny": ["wrist cuffs"],
"police": ["handcuffs", "whistle"],
"cheerleader": ["hair ribbon", "hair bow"],
"harem": ["belly chain", "anklet", "arm cuffs", "coin jewelry"],
"school_japanese": ["hair ribbon"],
"school_gyaru": ["hair clip"],
"office_lady": ["glasses"],
"race_queen": ["umbrella"],
"miko": ["hair ornament", "hair bow"],
"nun": ["rosary", "cross necklace"],
"flight_attendant": ["pilot hat", "gloves"],
"kunoichi": [],
"reverse_bunnysuit": ["wrist cuffs"],
"reverse_outfit": [],
# v32 신규
"tennis_player": ["wristband", "hair ribbon"],
"golf_player": ["gloves", "hair clip"],
}
# ==========================================
# 8. impossible / cutout
# ==========================================
self.impossible_tags = {
"bodysuit": "impossible bodysuit",
"dress": "impossible dress",
"shirt": "impossible shirt",
"swimsuit": "impossible swimsuit",
}
self.cutout_tags = [
"above-cleavage cutout", "arm cutout", "armpit cutout", "ass cutout",
"breast cutouts", "back cutout", "cleavage cutout", "stomach cutout",
"navel cutout", "thigh cutout", "front slit", "back slit", "side slit"
]
self.underboob_triggers = [
"crop top", "slingshot swimsuit", "babydoll", "camisole",
"cupless bra", "chemise", "string bra", "cheerleader", "off-shoulder top"
]
self.sideboob_triggers = [
"side-tie bikini", "string bra", "negligee", "babydoll",
"open kimono", "string bikini", "slingshot swimsuit", "camisole", "bunny suit"
]
# ==========================================
# 9. 포즈 시스템
# ==========================================
# ── 포즈: 카테고리별 단일 구체 풀 (부가 포즈 없음 — 일관성 우선) ──
self.poses_sitting = [
"sitting", "seiza", "wariza", "yokozuwari",
"butterfly sitting", "crossed legs",
"hugging own legs", "knees up",
# 신규
"spread legs, sitting", "m legs",
# v32: 그라비아 앉은 포즈
"sitting, leaning back on both hands", # 손 뒤로 짚고 기대기 (상체 강조)
"w sit", # 무릎 양옆으로 접어 앉기
"sitting, one leg extended", # 한 다리 쭉 뻗고 앉기
"hugging own knees, looking up", # 무릎 끌어안고 올려다보기
]
self.poses_lying = [
"on back", "on side", "on stomach",
"reclining", "lying on side", "lying on stomach",
# 신규
"lying, leg up",
"spread legs, on back",
# 복합
"upside-down, lying, on back",
"upside-down, lying, on side",
# v32: 그라비아 교과서 누운 포즈
"lying on stomach, chin on hands", # 엎드려 턱 괴기
"lying on side, elbow prop", # 팔꿈치 짚고 옆으로 눕기
"lying on side, top leg raised", # 위쪽 다리만 들어올리기
]
self.poses_standing = [
"standing", "on tiptoe", "legs apart",
"standing on one leg",
# ── 신규 standing 추가 ──
"contrapposto",
"hand on own hip",
"arms raised",
"hands behind back",
"leaning against wall",
"back to viewer",
"back to viewer, looking back over shoulder",
# v32: 그라비아 서있는 포즈
"pigeon-toed standing", # 안짱다리 서기, 귀여운 느낌
"leaning forward, hands on knees", # 앞으로 숙여 무릎에 손
"hand on wall, leaning", # 벽에 손 짚고 기대기 (샤워 bg 궁합)
]
self.poses_kneeling = [
"kneeling", "on one knee", "all fours",
# 신규
"paw pose, kneeling",
"paw pose, all fours",
"ass up, all fours",
"top-down bottom-up, all fours",
# v32
"kneeling, torso twist", # 무릎 꿇고 상체만 비틀기, 등·허리 라인 강조
]
self.poses_straddling = [
"straddling", "thigh straddling", "upright straddle",
# 신규
"leg lock, straddling",
]
self.poses_special = [
"bent over", "arched back", "presenting",
"leaning forward", "looking back",
"hip thrust", "leg up",
# 신규
"spread legs", "leg lock",
"top-down bottom-up, presenting",
"top-down bottom-up, arched back",
# ── 등·뒤 강조 ──
"back arch, on stomach",
# ── 스트레칭 ──
"stretching", "arms up, stretching", "leg stretch",
"back stretch", "side stretch",
# v31: presenting 세분화
"presenting ass",
"presenting breasts",
"presenting armpit",
"presenting foot",
# v31: glass 계열
"breasts on glass, against glass",
]
# ass up: 독립 풀 — kneeling/special과 조합해서 bg별로 선택적 편입
self.poses_ass_up = [
"ass up",
"ass up, on knees",
"ass up, arched back",
]
self.poses_by_bg = {
"beach": self.poses_lying + self.poses_sitting + self.poses_special,
"bed": self.poses_lying + self.poses_kneeling + self.poses_straddling + self.poses_ass_up + ["hugging pillow"],
"bedroom": self.poses_lying + self.poses_kneeling + self.poses_special + self.poses_straddling + self.poses_ass_up + ["hugging pillow"],
"hotel room": self.poses_lying + self.poses_kneeling + self.poses_special + self.poses_straddling + self.poses_ass_up + ["hugging pillow"],
"messy room": self.poses_lying + self.poses_kneeling + self.poses_special + self.poses_ass_up,
"onsen": self.poses_kneeling + self.poses_sitting + ["looking back", "arched back"],
"bathtub": self.poses_lying + self.poses_kneeling + self.poses_sitting,
"shower (place)": self.poses_standing + self.poses_special + ["hand on wall, leaning"],
"gym": self.poses_kneeling + self.poses_special + self.poses_standing + self.poses_ass_up,
"locker room": self.poses_sitting + self.poses_special + ["looking back"] + self.poses_ass_up + self.poses_straddling,
"changing room": self.poses_sitting + self.poses_special + ["looking back"] + self.poses_ass_up + self.poses_straddling,
"rei no pool": self.poses_lying + self.poses_kneeling + self.poses_special,
"rooftop": self.poses_standing + self.poses_special,
"poolside": self.poses_lying + self.poses_sitting + self.poses_special,
"outdoor bath": self.poses_kneeling + self.poses_sitting + ["looking back", "arched back"],
"sofa": self.poses_lying + self.poses_sitting + self.poses_straddling + self.poses_ass_up,
"rain": self.poses_standing + self.poses_special,
"nightpool": self.poses_lying + self.poses_kneeling + self.poses_sitting,
"veranda": self.poses_standing + self.poses_special + self.poses_sitting,
"racetrack": self.poses_standing + self.poses_special,
"tatami room": self.poses_lying + self.poses_kneeling + self.poses_sitting + self.poses_straddling + self.poses_ass_up,
# v32 신규
"convenience store": self.poses_standing + self.poses_special,
"school classroom": self.poses_sitting + self.poses_standing + self.poses_special,
"default": self.poses_lying + self.poses_kneeling + self.poses_standing + self.poses_special,
}
# ==========================================
# 10. 표정
# ==========================================
self.expressions_peeking = ["expressionless", "nervous", "shy", "embarrassed", "surprised"]
self.expressions_seductive = [
# 미소/눈빛
"seductive smile", "smirk", "naughty face", "light smile",
"doyagao", "wink", "half-closed eyes",
# 입
"biting lip", "parted lips", "tongue out", "licking lips",
# 시선/동작
"looking up", "hair tie in mouth",
# 감정
"pout",
]
# ==========================================
# 11. 액션
# ==========================================
self.actions_by_type = {
"top": ["shirt lift", "upshirt", "downblouse", "lifting own shirt"],
"bottom": ["downpants", "pulling own shorts", "pantyshot", "upshorts"],
"swimsuit": ["adjusting swimwear", "pulling own swimsuit", "wedgie"],
"underwear":["pulling own panties", "adjusting bra", "bra lift"],
"towel": ["holding towel", "pulling towel", "clutching towel"],
"legwear": ["putting on legwear", "taking off legwear", "adjusting legwear", "pulling down legwear"],
"general": ["lifting own clothes", "opened by self", "pulling own clothes", "adjusting clothes"],
# water bg 전용 — bg 필터 후에만 pool에 추가
"water": ["bathing", "showering"],
# ── 신규: pov·마사지 ──
"pov": ["pov hands", "pov", "grabbing from behind, pov", "holding from behind, pov"],
"massage": ["massage", "back massage", "shoulder massage", "thigh massage", "oiling body"],
# v31: glass 전용
"glass": ["pressing against glass", "leaning on glass", "hands on glass"],
}
# ==========================================
# 12. 배경 · 인터랙션
# ==========================================
self.bg_gravure = [
"rei no pool", "beach", "onsen", "bathtub", "shower (place)",
"bedroom", "messy room", "bed", "gym", "locker room", "changing room",
"hotel room", "rooftop", "poolside", "outdoor bath", "sofa", "rain", "nightpool",
"veranda", "racetrack",
"tatami room",
# v32 신규
"convenience store", # 교복 코스튬과 시너지
"school classroom", # school 코스튬 전용 bg
]
self.interactions = {
"bed": ["lying on bed", "sitting on bed", "kneeling on bed"],
"onsen": ["soaking in water", "steam", "wooden bucket", "bathing"],
"beach": ["sitting on sand", "splashing water"],
"bathtub": ["sitting in bathtub", "lying in bathtub", "bathing"],
"shower (place)": ["standing under shower", "showering", "washing hair"],
"hotel room": ["lying on bed", "sitting on bed", "leaning on window sill"],
"rooftop": ["leaning on railing", "sitting on rooftop", "arms spread"],
"poolside": ["lying on deck chair", "sitting on poolside", "sunbathing"],
"outdoor bath": ["soaking in water", "steam", "bathing"],
"sofa": ["lying on sofa", "sitting on sofa", "leaning on armrest"],
"rain": ["standing in rain", "arms outstretched"],
"nightpool": ["soaking in water", "floating", "sitting on pool edge"],
"bedroom": ["lying on bed", "sitting on bed", "leaning on wall", "sitting on floor"],
"messy room": ["sitting on floor", "lying on floor", "leaning against wall"],
"gym": ["stretching on mat", "using barbell", "sitting on bench", "leaning on mirror"],
"locker room": ["sitting on bench", "leaning on locker", "changing clothes"],
"changing room": ["looking in mirror", "adjusting clothes", "sitting on stool"],
"rei no pool": ["sitting on pool edge", "lying by pool", "leaning on lane rope"],
"veranda": ["leaning on railing", "sitting on veranda", "looking out", "arms on railing"],
"racetrack": ["leaning on car", "waving", "standing by barrier"],
"tatami room": ["lying on futon", "sitting on futon", "seiza on tatami", "kneeling on tatami"],
# v32 신규
"convenience store": ["leaning on shelf", "standing in aisle", "looking at camera", "holding product"],
"school classroom": ["sitting on desk", "leaning on desk", "standing by blackboard", "sitting on chair"],
}
# ==========================================
# 13. 카메라·조명 (v28 통합 ★신규)
# ==========================================
# ── bg별 조명 가중치 (bg→조명 어울림 순서대로 가중치 부여) ──
# (lighting_pool, weights)
self.lighting_by_bg = {
"beach": (["sunlight", "golden hour", "backlighting", "soft lighting"], [35, 30, 25, 10]),
"poolside": (["sunlight", "golden hour", "backlighting", "soft lighting"], [35, 30, 20, 15]),
"rei no pool": (["soft lighting", "backlighting", "cinematic lighting", "golden hour"],[30, 25, 25, 20]),
"nightpool": (["rim light", "cinematic lighting", "side lighting", "soft lighting"], [35, 30, 20, 15]),
"outdoor bath": (["sunlight", "golden hour", "soft lighting", "backlighting"], [30, 30, 25, 15]),
"onsen": (["soft lighting", "warm lighting", "golden hour", "side lighting"], [30, 30, 25, 15]),
"bathtub": (["soft lighting", "warm lighting", "rim light", "side lighting"], [35, 25, 20, 20]),
"shower (place)": (["backlighting", "rim light", "soft lighting", "cinematic lighting"], [30, 30, 25, 15]),
"bedroom": (["soft lighting", "warm lighting", "rim light", "cinematic lighting"],[30, 30, 20, 20]),
"hotel room": (["soft lighting", "warm lighting", "rim light", "side lighting"], [30, 25, 25, 20]),
"bed": (["soft lighting", "warm lighting", "cinematic lighting", "rim light"],[35, 30, 20, 15]),
"messy room": (["soft lighting", "warm lighting", "side lighting", "rim light"], [30, 25, 25, 20]),
"sofa": (["soft lighting", "warm lighting", "rim light", "cinematic lighting"],[30, 30, 20, 20]),
"gym": (["side lighting", "rim light", "cinematic lighting", "soft lighting"],[30, 30, 25, 15]),
"locker room": (["side lighting", "soft lighting", "rim light", "cinematic lighting"],[30, 30, 20, 20]),
"changing room": (["soft lighting", "side lighting", "rim light", "warm lighting"], [30, 25, 25, 20]),
"rooftop": (["backlighting", "golden hour", "sunlight", "cinematic lighting"], [30, 30, 25, 15]),
"rain": (["backlighting", "side lighting", "cinematic lighting", "soft lighting"],[35, 25, 25, 15]),
"veranda": (["golden hour", "sunlight", "backlighting", "soft lighting"], [35, 25, 25, 15]),
"racetrack": (["sunlight", "golden hour", "cinematic lighting", "backlighting"], [30, 30, 25, 15]),
"tatami room": (["soft lighting", "warm lighting", "golden hour", "side lighting"], [35, 30, 20, 15]),
# v32 신규
"convenience store": (["fluorescent lamp", "soft lighting", "side lighting", "cinematic lighting"], [40, 25, 20, 15]),
"school classroom": (["soft lighting", "side lighting", "cinematic lighting", "warm lighting"], [35, 30, 20, 15]),
"default": (["soft lighting", "cinematic lighting", "rim light", "warm lighting", "side lighting"],[25, 20, 20, 20, 15]),
}
self.camera_tech = [
"depth of field", "bokeh", "blurry background", "sharp focus",
# ── v28 신규 ──
"film grain", "chromatic aberration", "motion blur",
# ── silhouette (노출 카테고리에서는 필터로 제외) ──
"silhouette",
]
# ==========================================
# 14. 피부 질감 태그 풀
# ==========================================
# bg별로 build_skin_tags()에서 동적 선택 — 여기선 보조 풀만 정의
self.skin_secondary = [
"navel", "skindentation", "blush", "ribs",
"inner thigh", "armpit", "toned body",
]
# 마사지 전용 강제 태그
self.skin_massage_forced = ["glossy skin", "wet skin", "oiled body"]
# ── 프레이밍 카테고리별 (v28 ★신규) ──
# 가중치를 반영하기 위해 리스트 중복으로 표현
self.framings_by_category = {
# cowboy shot·upper body 메인 / lower body 서브 / full body 소수
"swimwear": ["cowboy shot", "cowboy shot", "cowboy shot", "upper body", "lower body", "full body"],
"lingerie": ["cowboy shot", "cowboy shot", "upper body", "upper body", "lower body", "close-up", "portrait"],
"sleepwear": ["cowboy shot", "cowboy shot", "upper body", "upper body", "lower body", "close-up", "portrait"],
"costume": ["cowboy shot", "cowboy shot", "cowboy shot", "upper body", "lower body", "full body"],
"gym": ["cowboy shot", "cowboy shot", "upper body", "lower body", "full body"],
"casual": ["cowboy shot", "cowboy shot", "upper body", "upper body", "lower body"],
"naked": ["cowboy shot", "cowboy shot", "upper body", "upper body", "lower body", "close-up"],
"towel": ["cowboy shot", "cowboy shot", "upper body", "close-up"],
"exclusive": ["cowboy shot", "upper body", "upper body", "close-up", "portrait"],
}
# ── 앵글 (가중치 적용) ──
self.angles = ["from above", "from below", "from side", "from behind", "dutch angle",
"wide angle", "close-up", "peeking", "peeking over shoulder", "peeking through legs"]
self.angle_wt = [13, 17, 15, 12, 8, 8, 8, 8, 6, 5]
self.surgical_mask_costumes = {"nurse", "police", "office_lady", "flight_attendant"}
# =========================
# 유틸
# =========================
def pick(self, x): return random.choice(x)
def w(self, a, wt): return random.choices(a, weights=wt, k=1)[0]
def unique(self, tags): return list(dict.fromkeys(tags))
def _decorate(self, item):
"""색상 없이 소재·패턴·디테일만 적용 (lingerie 색상 통일용)"""
if random.random() < 0.10:
item = f"latex {item}"
elif random.random() < 0.12:
item = f"{self.pick(self.premium_materials)} {item}"
elif random.random() < 0.18:
item = self.maybe_pattern(item)
if random.random() < 0.35:
item = f"{item} with {self.pick(self.details_trim)}"
if random.random() < 0.20:
item = f"{item}, {self.pick(self.cutout_tags)}"
return item
# ── 패턴 래퍼 (v28) ──
def maybe_pattern(self, item, chance=0.18):
"""chance 확률로 패턴을 앞에 붙임."""
if random.random() < chance:
return f"{self.pick(self.patterns)} {item}"
return item
# ── 의상 생성 (소재·패턴·디테일·impossible 통합) ──
def get_color_garment(self, palette_name, item, can_impossible=True):
color = self.pick(self.palettes.get(palette_name, self.palettes["neutral"]))
# 소재 → 패턴 순으로 적용 (중복 방지: 소재 붙으면 패턴 스킵)
# v31: blanket·ribbon·cape 등 소재 수식이 어색한 아이템은 스킵
_no_material = any(kw in item for kw in ["blanket", "ribbon", "cape", "apron"])
if not _no_material:
if random.random() < 0.10:
item = f"latex {item}"
elif random.random() < 0.12:
item = f"{self.pick(self.premium_materials)} {item}"
elif random.random() < 0.18:
item = self.maybe_pattern(item)
# 피트감 (oversized/loose) — 적용 가능한 의류에만, 15% 확률
if any(kw in item for kw in self.fit_applicable) and random.random() < 0.15:
r = random.random()
if r < 0.5:
fit = self.pick(self.fit_oversized)
item = f"{fit} {item}"
else:
fit = self.pick(self.fit_loose)
item = f"{fit} {item}"
# impossible (bodysuit/dress/shirt/swimsuit에만)
_is_impossible = False
if can_impossible and random.random() < 0.20:
for k, v in self.impossible_tags.items():
if k in item:
item = v
_is_impossible = True
break
# 디테일 트리밍 — impossible 적용 시 스킵, blanket 계열도 스킵
if "towel" not in item and "blanket" not in item and not _is_impossible:
if random.random() < 0.35:
item = f"{item} with {self.pick(self.details_trim)}"
if random.random() < 0.20:
item = f"{item}, {self.pick(self.cutout_tags)}"
return f"{color} {item}"
def apply_censorship(self, tags, bg, exposure_states, is_towel=False):
c_tags = []
tag_str = " ".join(tags)
_topless_active = "topless" in exposure_states or any(t in tag_str for t in ["no bra", "no shirt", "breasts out", "open shirt"])
_bottomless_active = "bottomless" in exposure_states or "no panties" in tag_str
is_exposed = _topless_active or _bottomless_active or is_towel
if is_exposed:
both = _topless_active and _bottomless_active
base = 0.6 if both else 0.45
if random.random() < base: c_tags.append("convenient censoring")
if random.random() < base - 0.1: c_tags.append("hair over breasts")
if random.random() < base - 0.1: c_tags.append("hair over crotch")
# v30: blanket censor / towel censor (누운 포즈·침대·naked blanket일 때 우선)
_is_blanket = "naked blanket" in tag_str
_is_bed_bg = bg in {"bed", "bedroom", "hotel room", "sofa", "messy room"}
if _is_blanket or (_is_bed_bg and random.random() < 0.18):
c_tags.append("blanket censor")
elif is_towel and random.random() < 0.30:
c_tags.append("towel censor")
if "onsen" in bg or "outdoor bath" in bg: c_tags.append("steam censor")
elif "bathtub" in bg: c_tags.append("soap censor")
return c_tags
# ── 악세사리 빌더 — costume_acc_bonus는 항상 적용, 일반 acc는 50% 확률 ──
def build_acc(self, category, c_type=None):
acc = []
# costume_acc_bonus: 50% 스킵과 무관하게 항상 처리
if c_type and c_type in self.costume_acc_bonus:
for bonus in self.costume_acc_bonus[c_type]:
if random.random() < 0.65:
acc.append(bonus)
# 일반 악세사리: 50% 확률로 스킵
if random.random() > 0.50:
return acc
if category not in self.acc_categories:
return acc
if random.random() < 0.45:
pool = self.neck_jewelry
if category in ("lingerie", "exclusive", "sleepwear"):
pool = ["choker", "choker", "necklace", "pearl necklace", "collar", "chain necklace"]
elif category == "towel":
pool = ["necklace", "pearl necklace", "choker", "chain necklace"]
acc.append(self.pick(pool))
if random.random() < 0.35: acc.append(self.pick(self.earring_types))
if category != "towel":
if random.random() < 0.28: acc.append(self.pick(self.wrist_jewelry))
if random.random() < 0.30: acc.append(self.pick(self.hair_acc))
if random.random() < 0.28: acc.append(self.pick(self.shoulder_tags))
if random.random() < 0.25: acc.append(self.pick(self.ribbon_tags))
return acc
# ── 피부 질감 태그 빌더 ──
def build_skin_tags(self, category, bg, exposure_states, tags, framing=""):
"""카테고리·배경·액션 문맥에서 피부 질감 태그를 반환.
마사지 액션 감지 시 glossy skin + wet skin + oiled body 강제 적용."""
tag_str = " ".join(tags)
# ── 마사지 감지 → 강제 적용 ──
if any(kw in tag_str for kw in ["massage", "oiling body"]):
return list(self.skin_massage_forced)
# ── 65% 확률로 발동 ──
if random.random() > 0.65:
return []
result = []
# ── tan/tanlines: 야외 수영 bg에서만 20% 확률 ──
if bg in {"beach", "poolside", "rei no pool", "outdoor bath"}:
if random.random() < 0.20:
result.append("tanlines")
# ── bg별 1차 피부 태그 ──
if bg in {"onsen", "bathtub", "shower (place)"}:
primary = self.w(["wet skin", "rosy skin", "steamy"], [45, 30, 25])
elif bg in {"gym", "locker room", "changing room"}:
primary = self.w(["sweaty", "shiny skin"], [55, 45])
elif bg in {"nightpool", "rain"}:
primary = self.w(["wet skin", "shiny skin"], [60, 40])
elif bg in {"bed", "bedroom", "hotel room", "sofa", "messy room"}:
pool = ["blush", "pale skin", "shiny skin"]
if category in {"lingerie", "exclusive"}:
pool += ["body blush", "shiny skin"]
primary = self.pick(pool)
elif bg in {"beach", "poolside", "rei no pool", "outdoor bath"}:
primary = self.w(["shiny skin", "pale skin", "blush"], [50, 30, 20])
else:
primary = self.pick(["blush", "pale skin", "shiny skin"])
result.append(primary)
# ── 25% 확률로 보조 태그 1개 ──
if random.random() < 0.25:
secondary_pool = [s for s in self.skin_secondary if s not in result]
result.append(self.pick(secondary_pool))
# ── collarbone: 상체 포커스 프레이밍일 때만 ──
if framing in {"upper body", "portrait", "close-up"} and random.random() < 0.35:
result.append("collarbone")
return result
# ── derive_focus (v28 ★신규) ──
def derive_focus(self, framing, pose_tags, outfit_tags, category, exposure_states, bg=""):
"""포즈·의상·프레이밍 문맥에 맞게 focus 후보를 수집한 뒤 1개만 반환."""
candidates = []
ps = " ".join(pose_tags)
os = " ".join(outfit_tags)
# ── portrait / close-up: 눈·얼굴 위주 ──
if framing in ("portrait", "close-up"):
candidates.append("eye focus")
# 상체 노출 있을 때만 클eavage 추가
if "topless" in exposure_states or any(t in os for t in ["no bra", "no shirt", "breasts out", "open shirt"]):
candidates.append(self.pick(["breast focus", "cleavage"]))
if any(kw in os for kw in self.underboob_triggers):
candidates.append("underboob")
if any(kw in os for kw in self.sideboob_triggers):
candidates.append("sideboob")
# ── lower body: 하체·다리·발 위주, 상체 포커스 완전 차단 ──
elif framing == "lower body":
if any(k in os for k in ["thighhigh", "stockings", "kneehigh", "fishnets",
"garter straps", "bodystocking", "pantyhose", "leggings"]):
candidates.append("thigh focus")
# foot focus
_shoe_kws = ["shoes", "boots", "sneakers", "heels", "sandals", "pumps", "loafers", "platform"]
_has_shoes = any(kw in os for kw in _shoe_kws)
_water_bgs = {"onsen", "bathtub", "shower (place)", "bed", "outdoor bath",
"nightpool", "beach", "rei no pool", "poolside", "rain"}
if not _has_shoes and bg not in _water_bgs:
_legwear_foot = ["thighhigh", "stockings", "kneehigh", "fishnets",
"garter straps", "bodystocking", "pantyhose", "socks"]
if any(kw in os for kw in _legwear_foot) or random.random() < 0.30:
candidates.append("foot focus")
if any(k in ps for k in ["ass up", "top-down bottom-up", "presenting",
"bent over", "spread legs", "leg up", "m legs",
"presenting ass"]):
candidates.append("ass focus")
if any(k in ps for k in ["presenting foot"]):
candidates.append("foot focus")
if any(k in os for k in ["ass cutout", "side slit", "back slit"]):
candidates.append("ass focus")
# 후보 없으면 lower body 기본 풀
if not candidates:
candidates = ["thigh focus", "ass focus", "skindentation"]
# ── upper body: 상체 위주 ──
elif framing == "upper body":
if "topless" in exposure_states or any(t in os for t in ["no bra", "no shirt", "breasts out", "open shirt"]):
candidates.append(self.pick(["breast focus", "cleavage"]))
if any(kw in os for kw in self.underboob_triggers):
candidates.append("underboob")
if any(kw in os for kw in self.sideboob_triggers):
candidates.append("sideboob")
if any(k in ps for k in ["looking back", "back to viewer",
"back to viewer, looking back over shoulder"]):
candidates.append("back focus")
# 후보 없으면 상체 기본 풀
if not candidates:
candidates = ["cleavage", "armpit focus", "skindentation"]
# ── cowboy shot / full body: 전방위 ──
else:
if any(k in ps for k in ["looking back", "on stomach", "lying on stomach",
"back to viewer", "back to viewer, looking back over shoulder",
"back arch, on stomach"]):
candidates.append("back focus")
if any(k in os for k in ["thighhigh", "stockings", "kneehigh", "fishnets",
"garter straps", "bodystocking", "pantyhose", "leggings"]):
candidates.append("thigh focus")
# foot focus: full body에서만
if framing == "full body":
_shoe_kws = ["shoes", "boots", "sneakers", "heels", "sandals", "pumps", "loafers", "platform"]
_has_shoes = any(kw in os for kw in _shoe_kws)
_water_bgs = {"onsen", "bathtub", "shower (place)", "bed", "outdoor bath",
"nightpool", "beach", "rei no pool", "poolside", "rain"}
if not _has_shoes and bg not in _water_bgs:
_legwear_foot = ["thighhigh", "stockings", "kneehigh", "fishnets",
"garter straps", "bodystocking", "pantyhose", "socks"]
if any(kw in os for kw in _legwear_foot) or random.random() < 0.30:
candidates.append("foot focus")
if "topless" in exposure_states or any(t in os for t in ["no bra", "no shirt", "breasts out", "open shirt"]):
candidates.append(self.pick(["breast focus", "cleavage"]))
if any(k in ps for k in ["ass up", "top-down bottom-up", "presenting", "bent over",
"presenting ass"]):
candidates.append("ass focus")
if any(k in ps for k in ["presenting breasts"]):
candidates.append("breast focus")
if any(k in ps for k in ["presenting armpit"]):
candidates.append("armpit focus")
if any(k in ps for k in ["presenting foot"]):
candidates.append("foot focus")
if any(k in os for k in ["ass cutout", "side slit", "back slit"]):
candidates.append("ass focus")
if any(kw in os for kw in self.underboob_triggers):
candidates.append("underboob")
if any(kw in os for kw in self.sideboob_triggers):
candidates.append("sideboob")
# 후보가 있으면 1개만, 없으면 범용 기본 풀에서 1개
if candidates:
return [self.pick(candidates)]
return [self.pick(["cleavage", "armpit focus", "ass focus", "skindentation"])]
# ==========================================
# 의상 빌더
# ==========================================
def build_outfit(self, category):
items = []
exposure_states = []
# 카테고리별 topless/bottomless 허용 범위 정의
# topless 허용: lingerie, sleepwear, casual, exclusive, swimwear(낮은 확률)
# bottomless 허용: lingerie, sleepwear, casual, exclusive
topless_categories = {"lingerie", "sleepwear", "casual", "exclusive", "swimwear", "naked"}
bottomless_categories = {"lingerie", "sleepwear", "casual", "exclusive", "naked"}
if category not in ["towel", "exclusive"]:
r = random.random()
if category in topless_categories and category in bottomless_categories:
# 둘 다 가능한 카테고리 (lingerie, sleepwear, casual)
if r < 0.18: exposure_states = ["topless"]
elif r < 0.30: exposure_states = ["bottomless"]
elif r < 0.35: exposure_states = ["topless", "bottomless"]
elif category in topless_categories:
# swimwear: topless만, 더 낮은 확률
if r < 0.10: exposure_states = ["topless"]
# costume/gym: 노출 없음
header = ""
c_type = None
if category == "swimwear":
palette = self.pick(["vivid", "pink", "blue", "warm", "neutral", "pastel", "summer"])
r = random.random()
if r < 0.20:
# 상하의 분리 레이어링 — 색상 1개로 고정해서 충돌 방지
fixed_color = self.pick(self.palettes[palette])
top_item = self.pick(self.bikini_tops)
bot_item = self.pick(self.bikini_bottoms)
items.append(f"{fixed_color} {top_item}")
items.append(f"{fixed_color} {bot_item}")
elif r < 0.55:
# 비키니 계열 (35%)
_bk = self.pick(self.swimwear_bikini)
if _bk == "gold bikini":
items.append(_bk) # 고유명사 — 색상 prefix 없이
else:
items.append(self.get_color_garment(palette, _bk))
else:
# 원피스/모노키니 계열 (45%) — 이 구간에서 가중치 부여
items.append(self.get_color_garment(palette, self.pick(self.swimwear_onepiece)))
header = "[수영복 화보]"
# v30: areola slip — 비키니 계열(상의 있을 때)에 10% 확률
if "topless" not in exposure_states:
outfit_tmp2 = " ".join(items)
_is_bikini_type = any(kw in outfit_tmp2 for kw in ["bikini", "bandeau", "tankini"])
if _is_bikini_type and random.random() < 0.10:
items.append("areola slip")
# nipple cover (pasties/maebari/bandaid): 8% 확률, areola slip과 상호 배타
elif _is_bikini_type and random.random() < 0.08:
items.append(self.pick(self.nipple_cover_pool))
# v32: sarong/beach cover-up 레이어링 — beach 계열 bg에서 25% 확률
# (bg는 build_outfit 호출 시점에 미결정이므로 후처리 플래그만 세팅)
if random.random() < 0.25:
items.append(self.pick(["sarong", "beach cover-up", "pareo"]))
elif category == "gym":
palette = self.pick(["vivid", "dark", "blue", "neutral", "warm"])
gym_tops = ["sports bra", "tank top", "gym shirt", "crop top", "athletic top", "jersey"]
gym_bottoms = ["buruma", "bike shorts", "shorts", "compression shorts", "gym shorts"]
items += [
self.get_color_garment(palette, self.pick(gym_tops)),
self.get_color_garment(palette, self.pick(gym_bottoms)),
]
header = "[그라비아·체육복]"
elif category == "towel":
_towel_color = self.pick(["white", "ivory", "beige", "light gray", "cream"])
items.append(f"{_towel_color} naked towel")
header = "[그라비아·타월]"
elif category == "exclusive":
sampled = random.sample(self.exclusive_items, k=random.randint(1, 2))
for p in sampled:
# pasties/maebari 선택 시 세부 풀로 확장
if p in ("pasties", "maebari"):
items.append(self.pick(self.nipple_cover_pool))
else:
items.append(self.get_color_garment(self.pick(["dark", "vivid", "pink"]), p, can_impossible=True))
header = "[익스클루시브·노출]"
elif category == "sleepwear":
palette = self.pick(["light", "pink", "pastel"])
items.append(self.get_color_garment(palette, self.pick(self.sleepwear_pool)))
if random.random() < 0.50:
items.append(self.get_color_garment("dark", self.pick(self.panties_pool)))
header = "[그라비아·잠옷]"
elif category == "costume":
keys = list(self.costume_weights.keys())
weights = [self.costume_weights[k] for k in keys]
c_type = random.choices(keys, weights=weights, k=1)[0]
fixed_tags, colorable_idx, palette_override = self.costumes_data[c_type]
c_tags = fixed_tags.copy()
# v30: reverse_outfit — 서브풀에서 하나 선택
if c_type == "reverse_outfit":
picked_rv = self.pick(self.reverse_outfit_pool)
c_tags = [picked_rv]
# reverse 계열은 색상 수식 추가
palette = self.pick(palette_override)
color = self.pick(self.palettes[palette])
c_tags[0] = f"{color} {c_tags[0]}"
items.extend(c_tags)
# v31: nipple cover 자동 추가
items.append(self.pick(self.nipple_cover_pool))
header = "[코스튬·Reverse Outfit]"
return items, header, exposure_states, c_type
if colorable_idx is not None and palette_override:
palette = self.pick(palette_override)
color = self.pick(self.palettes[palette])
c_tags[colorable_idx] = f"{color} {c_tags[colorable_idx]}"
items.extend(c_tags)
if c_type == "harem" and random.random() < 0.5:
items.append("mouth veil")
# v31: reverse_bunnysuit — nipple cover 자동 추가
if c_type == "reverse_bunnysuit":
items.append(self.pick(self.nipple_cover_pool))
# school_gyaru 전용: grey pencil skirt or cardigan/sweater around waist (상호 배타) + loose socks
if c_type == "school_gyaru":
if random.random() < 0.30:
# grey pencil skirt 적용 → plaid skirt 제거
items = [i for i in items if "plaid skirt" not in i]
items.append("grey pencil skirt")
else:
# pencil skirt 없을 때 cardigan/sweater around waist
items.append(self.pick(["cardigan around waist", "sweater around waist"]))
if random.random() < 0.50:
items.append("loose socks")
if c_type in self.surgical_mask_costumes and random.random() < 0.08:
items.append("surgical mask")
if c_type.startswith("school_"):
header = "[코스튬·School Uniform]"
elif c_type == "reverse_bunnysuit":
header = "[코스튬·Reverse Bunnysuit]"
elif c_type == "tennis_player":
header = "[코스튬·Tennis Girl]"
elif c_type == "golf_player":
header = "[코스튬·Golf Girl]"
else:
header = f"[코스튬·{c_type.replace('_', ' ').title()}]"
elif category == "naked":
item = self.pick(self.naked_items)
is_japanese = any(kw in item for kw in ["kimono", "sarashi", "fundoshi"])
palette = "light" if is_japanese else self.pick(["light", "neutral", "pink", "earth", "muted"])
items.append(self.get_color_garment(palette, item, can_impossible=False))
# naked 계열은 하의 없음이 기본
items.append("no pants")
# 50% 확률로 팬티 레이어링 (일본 의상·fundoshi 제외)
if random.random() < 0.50 and not is_japanese:
items.append(self.get_color_garment("dark", self.pick(self.panties_pool)))
header = "[네이키드·그라비아]"
elif category == "casual":
item = self.pick(self.casual_items)
is_japanese = any(kw in item for kw in ["yukata", "kimono"])
is_robe = any(kw in item for kw in ["robe", "hoodie", "cardigan", "jacket"])
palette = "light" if is_japanese else self.pick(["light", "neutral", "pink", "earth", "muted"])
items.append(self.get_color_garment(palette, item, can_impossible=False))
if any(kw in item for kw in ["shirt", "top", "hoodie", "sweater", "cardigan", "jacket"]):
items.append("no pants")
elif is_robe:
items.append("no pants")
if random.random() < 0.4 and not is_japanese:
items.append(self.get_color_garment("dark", self.pick(self.bra_pool)))
if random.random() < 0.3 and not is_japanese:
items.append(self.get_color_garment("dark", self.pick(self.panties_pool)))
header = "[캐주얼·그라비아]"
else: # lingerie
palette = self.pick(["dark", "dark", "pink", "jewel", "pastel", "light", "warm"])
fixed_color = self.pick(self.palettes[palette])
items += [
f"{fixed_color} {self._decorate(self.pick(self.bra_pool))}",
f"{fixed_color} {self._decorate(self.pick(self.panties_pool))}",
]
header = "[그라비아·섹시]"
# 레그웨어 (타월·익스클루시브 제외)
if category not in ["towel", "exclusive"] and random.random() < 0.55:
if random.random() < 0.15:
# garter 조합 — 색상 래핑 없이 직접 추가
items.append(self.pick(self.legwear_garter_combos))
else:
items.append(self.get_color_garment("neutral", self.pick(self.legwear_pool)))
# 노출 상태 적용
if "topless" in exposure_states:
items = [i for i in items if not any(kw in i for kw in ["shirt", "bra", "top", "vest", "arabian", "bustier", "corset"])]
# topless 표현 다양화
topless_tag = self.pick([
"topless", "topless", "topless", # 기본 빈도 유지
"no bra", "no shirt", "breasts out", "open shirt",
])
items.append(topless_tag)
if "bottomless" in exposure_states:
items = [i for i in items if not any(kw in i for kw in ["buruma", "panties", "shorts", "pants"])]
bottomless_tag = self.pick(["bottomless", "bottomless", "no panties"])
items.append(bottomless_tag)
# ── see-through 패스 (lingerie·sleepwear·exclusive·naked·casual 65%) ──
# naked 계열(naked shirt 등)이나 란제리·잠옷 의상에 see-through 수식 또는 태그 추가
_see_through_categories = {"lingerie", "sleepwear", "exclusive", "naked", "casual"}
if category in _see_through_categories and random.random() < 0.65:
_see_through_applicable = [
"shirt", "bra", "panties", "negligee", "chemise", "camisole",
"babydoll", "bodysuit", "dress", "slip", "robe", "nightgown",
"kimono", "hoodie", "sweater", "coat", "jacket", "apron",
"overalls", "cape", "teddy", "lingerie",
]
# 개별 아이템에 see-through prefix를 붙일 수 있으면 붙이고, 없으면 독립 태그로 추가
patched = False
new_items = []
for it in items:
if (not patched
and not any(skip in it for skip in ["topless", "bottomless", "no pants", "no bra", "no shirt", "breasts out", "open shirt", "no panties"])
and not any(mat in it for mat in ["latex", "leather", "velvet"]) # v31: 불투명 소재 스킵
and any(kw in it for kw in _see_through_applicable)
and "see-through" not in it):
it = f"see-through {it}"
patched = True
new_items.append(it)
items = new_items
if not patched:
items.append("see-through")
return items, header, exposure_states, c_type
# ==========================================
# 메인 생성 로직
# ==========================================
def generate_one(self):
category = self.w(
["costume", "swimwear", "lingerie", "sleepwear", "gym", "casual", "towel", "exclusive", "naked"],
[20, 22, 18, 10, 8, 5, 6, 6, 5]
)
outfit_tags, header, exposure_states, c_type = self.build_outfit(category)
# ── 배경 결정 ──
if category == "towel":
bg = self.pick(["onsen", "bathtub", "shower (place)", "bedroom", "hotel room"])
elif category == "gym":
bg = self.pick(["gym", "locker room", "changing room", "shower (place)"])
elif category == "sleepwear":
bg = self.pick(["bedroom", "bed", "hotel room", "sofa"])
elif category == "lingerie":
bg = self.pick(["bedroom", "bed", "hotel room", "sofa", "messy room", "veranda"])
elif category == "exclusive":
bg = self.pick(["bedroom", "bed", "hotel room", "sofa", "shower (place)"])
elif category == "swimwear":
_swim_outfit_str = " ".join(outfit_tags)
# v30: micro bikini top/bottom only → 일반 outfit bg 사용
if any(kw in _swim_outfit_str for kw in ["micro bikini top only", "micro bikini bottom only"]):
bg = self.pick(["bedroom", "hotel room", "sofa", "messy room", "bed", "rooftop", "veranda"])
else:
bg = self.pick(["beach", "rei no pool", "poolside", "nightpool", "outdoor bath"])
elif category == "casual":
outfit_tmp = " ".join(outfit_tags)
if any(kw in outfit_tmp for kw in ["yukata", "kimono"]):
bg = self.pick(["onsen", "outdoor bath", "bedroom", "tatami room"])
else:
bg = self.pick(["bedroom", "hotel room", "sofa", "messy room", "shower (place)"])
elif category == "naked":
outfit_tmp = " ".join(outfit_tags)
if any(kw in outfit_tmp for kw in ["kimono", "sarashi", "fundoshi"]):
bg = self.pick(["bedroom", "tatami room", "onsen", "outdoor bath", "veranda"])
elif "naked blanket" in outfit_tmp:
bg = self.pick(["bedroom", "bed", "hotel room", "sofa"])
else:
bg = self.pick(["bedroom", "hotel room", "sofa", "messy room", "bed",
"onsen", "outdoor bath", "bathtub", "shower (place)"])
elif category == "costume" and c_type:
if c_type == "school_japanese":
bg = self.pick(["bedroom", "locker room", "tatami room", "veranda", "school classroom"])
elif c_type == "school_gyaru":
bg = self.pick(["bedroom", "locker room", "veranda", "school classroom", "convenience store"])
elif c_type == "miko":
bg = self.pick(["onsen", "outdoor bath", "tatami room", "veranda"])
elif c_type == "nun":
bg = self.pick(["bedroom", "veranda"])
elif c_type == "race_queen":
bg = self.pick(["racetrack", "rooftop", "hotel room"])
elif c_type in {"tennis_player", "golf_player"}:
bg = self.pick(["rooftop", "veranda", "hotel room", "locker room", "changing room"])
elif c_type == "harem":
bg = self.pick(["bedroom", "hotel room", "onsen"])
elif c_type in {"nurse", "police", "flight_attendant"}:
bg = self.pick(["bedroom", "hotel room", "locker room", "changing room", "rooftop"])
elif c_type in {"maid", "bunny", "cheerleader"}:
bg = self.pick(["bedroom", "hotel room", "sofa", "locker room", "veranda"])
elif c_type in {"reverse_bunnysuit", "reverse_outfit"}:
bg = self.pick(["bedroom", "hotel room", "sofa", "locker room", "messy room"])
else:
bg = self.pick(["bedroom", "hotel room", "locker room", "changing room"])
else:
bg = self.pick(self.bg_gravure)
tags = list(outfit_tags)
# ── 악세사리 (v28 ★신규) ──
tags += self.build_acc(category, c_type)
# ── 프레이밍 (v28 ★신규) ──
framing_pool = self.framings_by_category.get(category, ["cowboy shot", "full body"])
framing = self.pick(framing_pool)
tags.append(framing)
# ── 포즈: 프레이밍에 맞게 base_pool 필터링 후 1개 선택 ──
base_pool = self.poses_by_bg.get(bg, self.poses_by_bg["default"])
if framing == "upper body":
# 상체샷: 누운 포즈·엉덩이 강조·전신 하체 포즈 제거
_ub_block = {"on back", "on side", "on stomach", "reclining",
"lying on side", "lying on stomach", "lying, leg up",
"spread legs, on back", "upside-down, lying, on back",
"upside-down, lying, on side",
"ass up", "ass up, on knees", "ass up, arched back",
"ass up, all fours", "paw pose, all fours",
"top-down bottom-up, all fours", "top-down bottom-up, presenting",
"top-down bottom-up, arched back",
"spread legs, sitting", "m legs", "spread legs", "leg lock",
"presenting", "bent over",
# v31 fix: presenting 세분화 하체 포즈
"presenting ass", "presenting foot",
"leg stretch", "back arch, on stomach"}
base_pool = [p for p in base_pool if p not in _ub_block] or base_pool
elif framing == "lower body":
# 하체샷: 상체 위주 포즈 제거, 다리·엉덩이 강조 우대
_lb_block = {"standing", "on tiptoe", "contrapposto", "hand on own hip",
"arms raised", "hands behind back", "leaning against wall",
"back to viewer", "back to viewer, looking back over shoulder",
"stretching", "arms up, stretching", "back stretch", "side stretch",
"seiza", "wariza", "yokozuwari", "butterfly sitting",
"crossed legs", "hugging own legs"}
base_pool = [p for p in base_pool if p not in _lb_block] or base_pool
elif framing in ("close-up", "portrait"):
# 클로즈업·포트레이트: 극단적 전신 포즈 제거, 앉기·기대기 위주
_cu_block = {"ass up", "ass up, on knees", "ass up, arched back",
"ass up, all fours", "paw pose, all fours",
"top-down bottom-up, all fours", "top-down bottom-up, presenting",
"top-down bottom-up, arched back",
"spread legs, on back", "upside-down, lying, on back",
"upside-down, lying, on side",
"back arch, on stomach", "leg stretch", "back stretch",
# v31 fix: 하체 중심 presenting은 클로즈업에서 제외
"presenting ass", "presenting foot",
"leg lock", "spread legs", "m legs"}
base_pool = [p for p in base_pool if p not in _cu_block] or base_pool
elif framing == "cowboy shot":
# 카우보이샷: 극단적 엎드리기·top-down-bottom-up 제거
_cs_block = {"ass up, all fours", "paw pose, all fours",
"top-down bottom-up, all fours", "top-down bottom-up, presenting",
"top-down bottom-up, arched back",
"upside-down, lying, on back", "upside-down, lying, on side",
"on stomach", "lying on stomach", "back arch, on stomach"}
base_pool = [p for p in base_pool if p not in _cs_block] or base_pool
# full body: 필터 없음 — 전체 허용
base_pose = self.pick(base_pool)
tags.append(base_pose)
# ── 앵글 (가중치 적용) ──
angle = self.w(self.angles, self.angle_wt)
is_peeking = "peeking" in angle
tags.append(angle)
# ── 표정·시선 ──
if is_peeking:
tags.append("-3::looking at viewer::")
tags.append("expressionless" if random.random() < 0.8
else self.pick([e for e in self.expressions_peeking if e != "expressionless"]))
else:
tags.append(self.pick(self.expressions_seductive))
if random.random() < 0.75:
tags.append("looking at viewer")
# ── 액션 풀 ──
outfit_str = " ".join(outfit_tags) # 의상 태그만 참조 (framing/pose/angle 오염 방지)
_is_topless = "topless" in exposure_states or any(t in outfit_str for t in ["no bra", "no shirt", "breasts out", "open shirt"])
_is_bottomless = "bottomless" in exposure_states or "no panties" in outfit_str
pool = self.actions_by_type["general"][:]
if not _is_topless:
if any(kw in outfit_str for kw in ["shirt", "blouse", "arabian"]): pool += self.actions_by_type["top"]
if any(kw in outfit_str for kw in ["bra", "lingerie"]): pool += self.actions_by_type["underwear"]
if not _is_bottomless:
if any(kw in outfit_str for kw in ["buruma", "shorts"]): pool += self.actions_by_type["bottom"]
if any(kw in outfit_str for kw in ["panties"]): pool += self.actions_by_type["underwear"]
if any(kw in outfit_str for kw in self.legwear_pool): pool += self.actions_by_type["legwear"]
if "swimsuit" in outfit_str or "bikini" in outfit_str: pool += self.actions_by_type["swimsuit"]
if "towel" in outfit_str:
pool = self.actions_by_type["towel"] + self.actions_by_type["general"]
# v31: breasts on glass 포즈일 때 glass 액션 강제 치환
_has_glass_pose = "breasts on glass" in " ".join(tags)
if _has_glass_pose:
pool = self.actions_by_type["glass"] + self.actions_by_type["general"]
# water 액션: 해당 bg일 때만 pool에 추가
water_action_bgs = {"onsen", "bathtub", "shower (place)", "outdoor bath",
"beach", "rei no pool", "poolside", "nightpool"}
if bg in water_action_bgs:
pool += self.actions_by_type["water"]
# pov/massage: 상호 배타 — 한 쪽만 풀에 추가
_has_pov_bg = bg in {"bed", "bedroom", "hotel room", "sofa", "messy room"}
_has_massage_bg = bg in {"bed", "bedroom", "hotel room", "sofa", "onsen", "outdoor bath"}
if _has_pov_bg or _has_massage_bg or random.random() < 0.20:
if random.random() < 0.55: # 55%→pov, 45%→massage
pool += self.actions_by_type["pov"]
else:
pool += self.actions_by_type["massage"]
action_count = 1
tags += random.sample(list(set(pool)), k=min(action_count, len(pool)))
# ── armpit 콤보: 겨드랑이 드러내는 의상 + arms raised 계열 포즈일 때 25% 확률 ──
_armpit_outfits = [
"bikini", "sports bra", "tank top", "slingshot", "strapless",
"camisole", "crop top", "bare shoulders", "miko", "tubetop",
"tube top", "halter", "monokini"
]
_armpit_poses = {"arms raised", "arms up, stretching", "stretching", "back stretch", "side stretch"}
_outfit_has_armpit = any(kw in outfit_str for kw in _armpit_outfits)
_pose_has_arms_up = base_pose in _armpit_poses
if _outfit_has_armpit and _pose_has_arms_up and random.random() < 0.37:
tags += ["spread armpit", "armpit crease", "armpit focus"]
# ── 수중/침대 bg: 레그웨어·신발 제거 ──
water_or_bed_bgs = {"onsen", "bathtub", "shower (place)", "bed", "outdoor bath", "nightpool"}
if bg in water_or_bed_bgs and "impossible" not in " ".join(tags):
tags = [t for t in tags if not any(kw in t for kw in
["shoes", "boots", "sneakers"] + self.legwear_pool)]
# ── wet 태그 ──
water_bgs = {"onsen", "bathtub", "shower (place)", "beach", "rei no pool",
"poolside", "outdoor bath", "rain", "nightpool"}
if bg in water_bgs:
tags = [t for t in tags if "sweat" not in t]
tags += ["wet", "water drops", "wet hair"]
_tag_str_wet = " ".join(tags)
_wet_exposed = ("topless" in exposure_states or "bottomless" in exposure_states
or any(t in _tag_str_wet for t in ["no bra", "no shirt", "breasts out", "open shirt", "no panties"]))
if _wet_exposed:
tags += ["wet skin"]
else:
tags.append("wet clothes")
# swimwear wet → see-through 처리 (40% 확률)
if category == "swimwear" and random.random() < 0.40:
tags.append("see-through swimsuit")
elif category == "gym" and random.random() < 0.5:
tags += ["sweat", "sweatdrops"]
# ── 피부 질감 ──
tags += self.build_skin_tags(category, bg, exposure_states, tags, framing)
# ── 옥상 바람 ──
if bg == "rooftop" and random.random() < 0.6:
tags += ["wind", "wind lift"]
# ── nightpool 야경 ──
if bg == "nightpool":
tags += ["night", self.pick(["pool lights", "neon lights", "city lights reflection"])]
# ── 조명·카메라 (v28 확장 풀) ──
# bg별 가중치 조명 선택
_lt_pool, _lt_wt = self.lighting_by_bg.get(bg, self.lighting_by_bg["default"])
lighting = self.w(_lt_pool, _lt_wt)
cam_tech = self.pick(self.camera_tech)
# silhouette 필터: 노출 카테고리에서는 제외
if cam_tech == "silhouette" and exposure_states:
cam_tech = self.pick([t for t in self.camera_tech if t != "silhouette"])
# motion blur 필터: 란제리·익스클루시브에서는 제외
if cam_tech == "motion blur" and category in ("lingerie", "exclusive", "sleepwear", "towel"):
cam_tech = self.pick([t for t in self.camera_tech if t != "motion blur"])
tags += [lighting, cam_tech]
# ── 검열 ──
tags += self.apply_censorship(tags, bg, exposure_states, is_towel=(category == "towel"))
# ── 배경·인터랙션 ──
tags.append(bg)
for key, val in self.interactions.items():
if bg == key:
tags.append(self.pick(val))
# ── derive_focus (v28 ★신규) ──
focus_list = self.derive_focus(framing, [base_pose], outfit_tags, category, exposure_states, bg)
tags += focus_list
final_tags = [t.replace("_", " ") for t in self.unique(tags)]
return header, ", ".join(final_tags)
def run(self):
for i in range(1, RUN_COUNT + 1):
h, t = self.generate_one()
print(f"{i}. {h}")
print(f" {t}\n")
DanbooruTagGenV25_Gravure().run()