Newer
Older
rpg / apps / game / tools / aiart / gen_b4.py
#!/usr/bin/env python3
"""B4: батч AI-генерации кандидатов-плейсхолдеров (SD1.5, GPU fp16).

Модель: Retro Diffusion (LostMedia/RetroDiffusion, Public Domain Mark 1.0) —
SD1.5-файнтюн пиксель-арта, слушает промпты одиночных объектов (проба 2026-09-09:
очаг/часы — отличные, мот/свиток читаются). Листовая модель B3
(SD_PixelArt_SpriteSheet_Generator) рисует только 4 фигурки персонажей —
для объектов непригодна. RTX 3090 fp16: ~1.3 с/вид, семена бесплатны.

Список объектов — LIST (id, промпт): интерактивы-плейсхолдеры (InteractableViews),
снаряд плевка и иконки сумки 8×8 (арт-библия §4). Слоты назначает post_b4.mjs.

Запуск (интерпретатор — venv-aiaudio, там torch cu130):
  ~/.cache/rpg-ai/venv-aiaudio/bin/python gen_b4.py          # все объекты, SEEDS семян
  ... gen_b4.py signpost hearth:7                             # конкретные id[:seed]
Вывод: aiart/review-b4/raw/<id>_<seed>.png (512×512 сырьё).
"""
import sys
import time
from pathlib import Path

import torch
from diffusers import StableDiffusionPipeline

MODEL_ID = 'LostMedia/RetroDiffusion'
STEPS = 20
CFG = 7.0
SEEDS = (1, 2, 3, 4)
NEG = ('blurry, photo, realistic, smooth gradients, text, watermark, '
       'multiple objects, cropped, character, person, '
       'warm background, brown background, orange background, sunset lighting')

LIST = {
    # --- интерактивы-плейсхолдеры (InteractableViews) ---
    'signpost': 'pixel art of a weathered wooden road signpost with an arrow board, dark wood',
    'mote': 'pixel art of a small mound of gray ash with a glowing warm ember inside',
    'tone_tree': 'pixel art of a dead tree trunk wrapped in black cables with small copper bells, on a plain pale gray background',
    'resonator': 'pixel art of a copper bell resonator machine on a rusty steel frame, on a plain pale gray background',
    'bell_rope': 'pixel art of a hanging bell pull rope with a wooden handle grip',
    'hearth': 'pixel art of a stone hearth fireplace with a warm fire inside',
    'note': 'pixel art of a small crumpled paper note lying flat on the ground',
    'counter': 'pixel art of a wooden market stall counter with crates and goods on top, side view',
    'spit': 'pixel art of a small blob of gray ash, a projectile',
    # --- иконки сумки 8×8 (items.ts) ---
    'icon_clock': 'pixel art icon of a small copper pocket watch',
    'icon_cloth': 'pixel art icon of a folded waxed cloth mask',
    'icon_bellflower': 'pixel art icon of a small blue bellflower',
    'icon_salt': 'pixel art icon of a small pouch of salt',
    'icon_flask': 'pixel art icon of a small copper water flask',
    'icon_mote': 'pixel art icon of a small warm glowing ash mote',
    'icon_map': 'pixel art icon of a rolled paper map scroll',
}


def main() -> None:
    args = sys.argv[1:]
    wanted = {a.split(':')[0] for a in args} if args else set(LIST)
    seeds_of = {}
    for a in args:
        if ':' in a:
            k, s = a.split(':', 1)
            seeds_of.setdefault(k, []).extend(int(x) for x in s.split(':'))
    out_dir = Path(__file__).resolve().parent / 'review-b4' / 'raw'
    out_dir.mkdir(parents=True, exist_ok=True)

    t0 = time.time()
    pipe = StableDiffusionPipeline.from_pretrained(
        MODEL_ID, torch_dtype=torch.float16, safety_checker=None,
    ).to('cuda')
    pipe.set_progress_bar_config(disable=True)
    print(f'[load] {time.time() - t0:.1f}s', flush=True)

    for oid in [k for k in LIST if k in wanted]:
        prompt = LIST[oid]
        for seed in seeds_of.get(oid, list(SEEDS)):
            gen = torch.Generator('cuda').manual_seed(seed)
            img = pipe(
                prompt, negative_prompt=NEG, num_inference_steps=STEPS,
                guidance_scale=CFG, width=512, height=512, generator=gen,
            ).images[0]
            out = out_dir / f'{oid}_{seed}.png'
            img.convert('RGBA').save(out)
            print(f'[ok] {out.name}', flush=True)


if __name__ == '__main__':
    main()