mongle-hf-package / test_text2feed_pipeline.py
Hadimeeee's picture
Upload Mongle RunPod test package
b446b48 verified
Raw
History Blame Contribute Delete
15 kB
"""
Text-to-feed end-to-end test pipeline.
Flow:
Korean character text
-> English visual character prompt
-> SDXL + lora_v3_32bit character image
-> Qwen2.5-VL appearance JSON extraction
-> Korean quest to English scene text
-> SDXL + lora_v3_32bit + lora_bg_v1 feed image
Outputs:
outputs/text2feed/<name>/
character.png
appearance_raw.txt
appearance.json
quest.json
feed.png
results.json
"""
import argparse
import gc
import json
import os
import sys
import traceback
from pathlib import Path
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
ROOT = Path(__file__).resolve().parent
os.chdir(ROOT)
sys.path.insert(0, str(ROOT))
OUTPUT_ROOT = Path("outputs/text2feed")
CHAR_LORA_DIR = "models/lora_v3_32bit"
DEFAULT_CHARACTER_KO = (
"이 친구는 부드러운 체리핑크색 곰 인형이에요. "
"크림색 배를 가지고 있고, 작은 둥근 귀와 분홍색 코, "
"짧고 통통한 팔다리와 포근한 표정을 가지고 있어요."
)
DEFAULT_CHARACTER_EN = (
"soft cherry pink bear plush mascot with a cream white belly, "
"small rounded ears, tiny pink oval nose, simple smiling mouth, "
"short rounded limbs, smooth plush texture"
)
DEFAULT_QUEST_KO = "공원에서 30분 달리기를 완료했어요!"
CHARACTER_NEGATIVE = (
"realistic, photograph, 3d render, human, person, anime, scary, "
"complex background, multiple characters, text, watermark, logo, "
"extra limbs, long limbs, harsh black outline, low quality, blurry"
)
QUEST_SYSTEM = """You are an action-pose scene prompt writer for Mongle Village, a cozy sky island pixel art village.
Convert a Korean quest completion message into a short English scene description for image generation.
Rules:
- The FIRST words must describe the character's visible action pose.
- Keep the activity from the quest very explicit.
- Include body movement cues such as arms swinging, legs moving, holding a book, stirring a pot, walking steps.
- Describe one simple matching environment after the action.
- Set it in a cozy pastel sky island village world.
- Mention only one character.
- Do not write only a landscape/background description.
- Do not make the character standing still unless the quest is resting.
- 16-28 words max.
- Output ONLY the English scene description.
Examples:
Input: 공원에서 30분 달리기를 완료했어요!
Output: running with arms swinging and legs in motion along a fluffy cloud meadow path
Input: 오늘 책 한 권을 다 읽었어요
Output: sitting and holding an open storybook under a blossoming cloud tree beside a cozy cottage
Input: 직접 요리해서 건강한 밥을 먹었어요
Output: cooking with both paws stirring a pot in a cozy cottage kitchen with fresh vegetables
"""
CHARACTER_SYSTEM = """You are a visual prompt writer for a plush-to-pixel-art character generation pipeline.
Convert a Korean character description into a concise English visual prompt.
Rules:
- Focus only on visible appearance: animal type, body color, face, ears, body shape, limbs, texture, accessories.
- Convert personality words into visible expression only.
- If animal type is not specified, choose a soft plush animal that fits the description.
- If color is not specified, choose one pastel color.
- Do not include background, scene, action, story, name, or relationship.
- 25-45 words.
- Output ONLY the English visual prompt.
Example:
Input: 이 친구는 용감하고 사랑스러운 하얀 토끼 인형이에요. 볼이 발그레하고 포근해요.
Output: white bunny plush mascot, rosy cheeks, confident bright eyes, warm gentle smile, soft round chubby body, short rounded limbs, fluffy plush texture
"""
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--name", default="sample_01")
parser.add_argument("--character-ko", default=DEFAULT_CHARACTER_KO)
parser.add_argument(
"--character-text",
default="",
help="English visual character prompt. If omitted, --character-ko is translated first.",
)
parser.add_argument("--quest-ko", default=DEFAULT_QUEST_KO)
parser.add_argument("--quest-en", default="", help="If provided, skips Qwen text translation.")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--force", action="store_true")
parser.add_argument("--force-quest", action="store_true")
parser.add_argument("--force-feed", action="store_true")
parser.add_argument("--no-4bit-vlm", action="store_true")
parser.add_argument("--character-steps", type=int, default=30)
parser.add_argument("--feed-seed", type=int, default=123)
return parser.parse_args()
def character_prompt(character_text: str) -> str:
return (
"monglestyle, "
f"{character_text}, "
"single stuffed animal toy mascot character, full body, centered, "
"front view, cute chibi proportions, 32-bit pixel art sprite, "
"soft pixel shading, clean silhouette, soft brown outline, "
"pure white background"
)
def translate_character(character_ko: str):
print("[character translation] importing load_qwen...", flush=True)
from src.pipeline.persona2prompt import load_qwen, unload_qwen
print("[character translation] importing torch...", flush=True)
import torch
print("Loading Qwen text model for character translation...", flush=True)
model, tokenizer = load_qwen()
messages = [
{"role": "system", "content": CHARACTER_SYSTEM},
{"role": "user", "content": character_ko},
]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=120,
do_sample=False,
temperature=None,
top_p=None,
pad_token_id=tokenizer.eos_token_id,
)
generated = outputs[0][inputs.input_ids.shape[1] :]
character_en = tokenizer.decode(generated, skip_special_tokens=True).strip()
unload_qwen(model, tokenizer)
return character_en
def generate_character(character_text: str, out_path: Path, seed: int, steps: int):
import torch
from diffusers import DPMSolverMultistepScheduler, StableDiffusionXLPipeline
print("Loading SDXL character pipeline...", flush=True)
pipe = StableDiffusionXLPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16,
use_safetensors=True,
)
pipe.load_lora_weights(CHAR_LORA_DIR)
pipe.fuse_lora(lora_scale=0.9)
pipe.unload_lora_weights()
pipe.scheduler = DPMSolverMultistepScheduler.from_config(
pipe.scheduler.config, use_karras_sigmas=True
)
pipe.to("cuda")
pipe.enable_attention_slicing()
prompt = character_prompt(character_text)
print(f"Character prompt: {prompt}", flush=True)
image = pipe(
prompt=prompt,
negative_prompt=CHARACTER_NEGATIVE,
num_inference_steps=steps,
guidance_scale=7.5,
height=1024,
width=1024,
generator=torch.Generator("cuda").manual_seed(seed),
).images[0]
image.save(out_path)
print(f"Character saved: {out_path}", flush=True)
del pipe
gc.collect()
torch.cuda.empty_cache()
def extract_appearance(character_path: Path, out_dir: Path, use_4bit: bool):
from PIL import Image
from test_qwen25_vl_extract import (
extract_json_from_text,
load_model,
normalize_info,
run_extraction,
)
print("Loading Qwen2.5-VL appearance extractor...", flush=True)
model, processor = load_model("Qwen/Qwen2.5-VL-7B-Instruct", use_4bit=use_4bit)
image = Image.open(character_path).convert("RGB")
raw = run_extraction(image, model, processor, max_new_tokens=900)
raw_path = out_dir / "appearance_raw.txt"
raw_path.write_text(raw, encoding="utf-8")
info = normalize_info(extract_json_from_text(raw))
appearance_path = out_dir / "appearance.json"
appearance_path.write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Appearance saved: {appearance_path}", flush=True)
del model, processor
gc.collect()
try:
import torch
torch.cuda.empty_cache()
except Exception:
pass
return info
def translate_quest(quest_ko: str):
print("[quest translation] importing load_qwen...", flush=True)
from src.pipeline.persona2prompt import load_qwen, unload_qwen
print("[quest translation] importing torch...", flush=True)
import torch
print("Loading Qwen text model for quest translation...", flush=True)
model, tokenizer = load_qwen()
messages = [
{"role": "system", "content": QUEST_SYSTEM},
{"role": "user", "content": quest_ko},
]
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer([text], return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=80,
do_sample=False,
temperature=None,
top_p=None,
pad_token_id=tokenizer.eos_token_id,
)
generated = outputs[0][inputs.input_ids.shape[1] :]
quest_en = tokenizer.decode(generated, skip_special_tokens=True).strip()
unload_qwen(model, tokenizer)
return quest_en
def remove_character_bg(character_path: Path, out_path: Path):
from PIL import Image
from rembg import remove
print("Removing character background...", flush=True)
image = Image.open(character_path).convert("RGBA")
result = remove(image)
result.save(out_path)
print(f"Character nobg saved: {out_path}", flush=True)
def generate_feed(quest_en: str, appearance, out_path: Path, seed: int):
from src.feed.feed_generator_1 import generate, load_pipeline, unload_pipeline
print("Loading feed generation pipeline...", flush=True)
pipe = load_pipeline()
image = generate(appearance, quest_en, pipe, seed=seed)
image.save(out_path)
print(f"Feed saved: {out_path}", flush=True)
unload_pipeline(pipe)
def main():
args = parse_args()
out_dir = OUTPUT_ROOT / args.name
out_dir.mkdir(parents=True, exist_ok=True)
status_path = out_dir / "status.log"
def status(message: str):
print(message, flush=True)
with status_path.open("a", encoding="utf-8") as f:
f.write(message + "\n")
status("=" * 60)
status("Text-to-feed pipeline started")
status(f"Output dir: {out_dir}")
character_path = out_dir / "character.png"
character_nobg_path = out_dir / "character_nobg.png"
character_prompt_path = out_dir / "character_prompt.json"
appearance_path = out_dir / "appearance.json"
quest_path = out_dir / "quest.json"
feed_path = out_dir / "feed.png"
results_path = out_dir / "results.json"
if args.character_text:
character_text = args.character_text
status("STEP 0: using provided English character prompt")
elif args.force or not character_prompt_path.exists():
status("STEP 0: translating Korean character description to English prompt")
character_text = translate_character(args.character_ko)
else:
character_text = json.loads(character_prompt_path.read_text(encoding="utf-8"))["character_text"]
status(f"STEP 0: using cached character prompt: {character_prompt_path}")
character_prompt_data = {
"character_ko": args.character_ko,
"character_text": character_text,
}
character_prompt_path.write_text(
json.dumps(character_prompt_data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
status(f"Character EN: {character_text}")
if args.force or not character_path.exists():
status("STEP 1: generating character image")
generate_character(character_text, character_path, args.seed, args.character_steps)
else:
status(f"STEP 1: using cached character: {character_path}")
if args.force or not character_nobg_path.exists():
status("STEP 1.5: removing character background")
remove_character_bg(character_path, character_nobg_path)
else:
status(f"STEP 1.5: using cached character nobg: {character_nobg_path}")
if args.force or not appearance_path.exists():
status("STEP 2: extracting appearance JSON with Qwen2.5-VL")
appearance = extract_appearance(character_path, out_dir, use_4bit=not args.no_4bit_vlm)
else:
appearance = json.loads(appearance_path.read_text(encoding="utf-8"))
status(f"STEP 2: using cached appearance: {appearance_path}")
if args.quest_en:
quest_en = args.quest_en
status("STEP 3: using provided English quest scene")
elif args.force or args.force_quest or not quest_path.exists():
status("STEP 3: translating Korean quest to English scene")
quest_en = translate_quest(args.quest_ko)
else:
quest_en = json.loads(quest_path.read_text(encoding="utf-8"))["quest_en"]
status(f"STEP 3: using cached quest translation: {quest_path}")
quest_data = {
"quest_ko": args.quest_ko,
"quest_en": quest_en,
}
quest_path.write_text(json.dumps(quest_data, ensure_ascii=False, indent=2), encoding="utf-8")
status(f"Quest EN: {quest_en}")
if args.force or args.force_feed or not feed_path.exists():
status("STEP 4: generating feed image")
generate_feed(quest_en, appearance, feed_path, args.feed_seed)
else:
status(f"STEP 4: using cached feed: {feed_path}")
results = {
"name": args.name,
"character_ko": args.character_ko,
"character_text": character_text,
"character_prompt_json": str(character_prompt_path).replace("\\", "/"),
"character_image": str(character_path).replace("\\", "/"),
"character_nobg": str(character_nobg_path).replace("\\", "/"),
"appearance_json": str(appearance_path).replace("\\", "/"),
"appearance": appearance,
"quest": quest_data,
"feed_image": str(feed_path).replace("\\", "/"),
}
results_path.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
status(f"Results saved: {results_path}")
status("Text-to-feed pipeline finished")
if __name__ == "__main__":
try:
main()
except Exception:
fallback_dir = OUTPUT_ROOT / "sample_01"
fallback_dir.mkdir(parents=True, exist_ok=True)
error_path = fallback_dir / "error.log"
error_text = traceback.format_exc()
error_path.write_text(error_text, encoding="utf-8")
print(error_text, flush=True)
print(f"Error saved: {error_path}", flush=True)
raise