| """ |
| 몽글마을 피드 이미지 생성기 v2 (이미지 입력 파이프라인 전용) |
| |
| feed_generator_1.py와의 차이점: |
| - 캐릭터 외형을 프롬프트 앞쪽에 배치 (SDXL 토큰 가중치 활용) |
| - appearance_str에 limbs 포함 (비전형 캐릭터 신체 반영) |
| - NEGATIVE에 human/person 추가 |
| - char LoRA 0.9 / bg LoRA 1.1 (외형 강제력 상향) |
| - guidance_scale 8.5 (프롬프트 추종력 강화) |
| - 양쪽 인코더 모두 scene 정보 전달 |
| """ |
| import os as _os; _os.chdir(_os.path.dirname(_os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))))) |
| import os, gc |
| from pathlib import Path |
| os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1" |
|
|
| from diffusers import StableDiffusionXLPipeline, DPMSolverMultistepScheduler |
| import torch |
|
|
| CHARACTER_LORA = "models/lora_v3_32bit/pytorch_lora_weights.safetensors" |
| BG_LORA = "models/lora_bg_v1/pytorch_lora_weights.safetensors" |
|
|
| NEGATIVE = ( |
| "realistic, 3d render, photograph, blurry, dark, gloomy, " |
| "watercolor, sketch, painterly, smooth illustration, " |
| "modern city, urban, scary, violence, text, watermark, " |
| "multiple characters, tiling, repeated, " |
| "human, person, girl, boy, man, woman, humanoid, human body, human face, " |
| "skin, hair, clothes, dress, shirt" |
| ) |
|
|
|
|
| def _to_appearance_str(appearance) -> str: |
| if isinstance(appearance, str): |
| return appearance.strip() |
| if not isinstance(appearance, dict): |
| return str(appearance).strip() |
|
|
| animal = appearance.get("animal_type", "bear") |
| color = appearance.get("body_color", "") |
| shape = appearance.get("body_shape", "") |
| eye = appearance.get("eye_style", "") |
| nose = appearance.get("nose_mouth") or appearance.get("nose_shape", "") |
| cheeks = appearance.get("cheeks", "") |
| ears = appearance.get("ear_shape", "") |
| limbs = appearance.get("limbs", "") |
| texture = appearance.get("texture", "") |
| acc = appearance.get("accessories", "") |
| secondary = appearance.get("secondary_colors", []) |
|
|
| parts = [f"a {color} pixel art stuffed {animal} character".strip()] |
| if shape: parts.append(shape) |
| if isinstance(secondary, list): |
| parts.extend(s for s in secondary[:2] if s and len(str(s).split()) <= 3) |
| if eye: parts.append(eye) |
| if cheeks and "no" not in cheeks.lower(): parts.append(cheeks) |
| if ears and "not visible" not in ears.lower(): parts.append(ears) |
| if nose and "not visible" not in nose.lower(): parts.append(nose) |
| if limbs and "not visible" not in limbs.lower(): parts.append(limbs) |
| if texture: parts.append(texture) |
| if isinstance(acc, list): |
| acc = ", ".join(str(a) for a in acc if a) |
| if acc and str(acc).lower() not in ("none", ""): parts.append(acc) |
|
|
| return ", ".join(p for p in parts if p) |
|
|
|
|
| def build_prompt(appearance, quest_en: str): |
| """ |
| 두 인코더 역할 분리: |
| prompt (CLIP-L) → 장면/배경 담당: scene 앞에 배치 |
| prompt_2 (OpenCLIP-bigG) → 캐릭터 외형 담당: appearance 앞에 배치 |
| """ |
| appearance_str = _to_appearance_str(appearance) |
|
|
| animal = "plush mascot" |
| if isinstance(appearance, dict): |
| animal = appearance.get("animal_type", "plush mascot") |
|
|
| |
| |
| prompt = ( |
| f"monglestyle, {quest_en}, " |
| f"stuffed {animal} plush toy mascot, " |
| f"cozy pastel sky island village, " |
| f"32-bit pixel art scene, detailed background, outdoor scenery, pastel colors" |
| ) |
| |
| |
| prompt_2 = ( |
| f"monglestyle, stuffed {animal} plush toy mascot, " |
| f"{appearance_str}, " |
| f"full body in action, {quest_en}, " |
| f"pixel art background scene" |
| ) |
| return prompt, prompt_2 |
|
|
|
|
| def load_pipeline(): |
| print("SDXL 로드 중 (캐릭터 LoRA + 배경 LoRA)...") |
| pipe = StableDiffusionXLPipeline.from_pretrained( |
| "stabilityai/stable-diffusion-xl-base-1.0", |
| torch_dtype=torch.float16, |
| use_safetensors=True, |
| ).to("cuda") |
|
|
| if not Path(CHARACTER_LORA).exists(): |
| raise FileNotFoundError(f"Character LoRA not found: {CHARACTER_LORA}") |
| if not Path(BG_LORA).exists(): |
| raise FileNotFoundError(f"Background LoRA not found: {BG_LORA}") |
|
|
| pipe.load_lora_weights(CHARACTER_LORA, adapter_name="character") |
| pipe.load_lora_weights(BG_LORA, adapter_name="bg") |
| pipe.set_adapters( |
| ["character", "bg"], |
| adapter_weights=[0.9, 1.1], |
| ) |
| pipe.scheduler = DPMSolverMultistepScheduler.from_config( |
| pipe.scheduler.config, use_karras_sigmas=True |
| ) |
| pipe.enable_attention_slicing() |
| print("로드 완료!\n") |
| return pipe |
|
|
|
|
| def generate(appearance, quest_en: str, pipe, seed: int = 42): |
| prompt, prompt_2 = build_prompt(appearance, quest_en) |
| print(f" prompt : {prompt}") |
| print(f" prompt_2: {prompt_2}") |
|
|
| return pipe( |
| prompt=prompt, |
| prompt_2=prompt_2, |
| negative_prompt=NEGATIVE, |
| num_inference_steps=25, |
| guidance_scale=8.5, |
| height=1024, |
| width=1024, |
| generator=torch.Generator("cuda").manual_seed(seed), |
| ).images[0] |
|
|
|
|
| def unload_pipeline(pipe): |
| del pipe |
| gc.collect() |
| torch.cuda.empty_cache() |
| print("파이프라인 VRAM 해제 완료") |
|
|