Otomatik Görsel Üretim Pipeline’ı: Stable Diffusion API Kullanımı

Görsel üretimi otomatikleştirmek istediğinizde, ilk başta “bir API çağırırsın, görsel gelir” gibi basit görünüyor. Sonra gerçekle yüzleşiyorsunuz: batch işler yarım kalıyor, VRAM yetersizliğinden process çöküyor, üretilen görsellerin kalitesi tutarsız, pipeline’ı izleyecek hiçbir mekanizma yok. Bu yazıda sıfırdan bir production-grade Stable Diffusion API pipeline’ı kurmayı ele alacağız. Sadece “çalışıyor” seviyesinde değil, gerçekten güvenilir bir şekilde.

Mimariye Karar Vermek

Production ortamında SD kullanmak için üç farklı yaklaşım var:

  • AUTOMATIC1111 WebUI API modu: En yaygın, en geniş extension desteği, ama tek process ve yönetimi zahmetli
  • ComfyUI API: Node-based pipeline, karmaşık workflow’lar için mükemmel, JSON tabanlı workflow tanımları
  • Standalone inference: Diffusers kütüphanesi ile doğrudan, en fazla kontrol ama en fazla kod

Biz AUTOMATIC1111’in API modunu temel alacağız çünkü mevcut çoğu ekibin zaten aşina olduğu interface bu. Sonrasında ComfyUI entegrasyonuna da değineceğiz.

Fiziksel kurulum için de bazı kararlar vermek gerekiyor. NVIDIA GPU’su olan bir sunucu mu kullanacaksınız, yoksa birden fazla worker mı? Tek GPU’lu basit bir setup mı, yoksa load balancer arkasında birden fazla instance mı? Bu yazıda tek GPU’lu bir sunucu üzerinde kurulum yapacağız ama multi-instance için de notlar ekleyeceğiz.

AUTOMATIC1111’i API Modunda Ayağa Kaldırmak

Önce gereksinimleri kontrol edelim:

# CUDA versiyonunu kontrol et
nvidia-smi
nvcc --version

# Python versiyonu (3.10 önerilir)
python3 --version

# Disk alanı - modeller için en az 20GB boş alan şart
df -h /opt

Kurulum için temiz bir virtual environment ile başlamak hayat kurtarır:

# Kurulum dizini
sudo mkdir -p /opt/stable-diffusion
sudo chown $USER:$USER /opt/stable-diffusion
cd /opt/stable-diffusion

# SD WebUI klonla
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
cd stable-diffusion-webui

# Python virtual environment
python3 -m venv venv
source venv/bin/activate

# Gerekli temel paketler
pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

Şimdi kritik kısma geliyoruz: WebUI’yi sadece API modu olarak çalıştıracak bir launch scripti. Bu scriptı hazırlamak ileride pek çok sorunun önüne geçiyor:

cat > /opt/stable-diffusion/launch_api.sh << 'EOF'
#!/bin/bash

# Ortam değişkenleri
export CUDA_VISIBLE_DEVICES=0
export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512

# Log dizini
LOG_DIR="/var/log/stable-diffusion"
mkdir -p $LOG_DIR

cd /opt/stable-diffusion/stable-diffusion-webui

source venv/bin/activate

# API modu: nowebui, api, dinleme adresi
exec python launch.py 
    --nowebui 
    --api 
    --api-log 
    --listen 
    --port 7860 
    --no-half-vae 
    --xformers 
    --medvram 
    --skip-torch-cuda-test 
    2>&1 | tee -a $LOG_DIR/webui.log
EOF

chmod +x /opt/stable-diffusion/launch_api.sh

Önemli parametrelerin ne işe yaradığını bilmek gerekiyor:

  • –nowebui: Gradio arayüzünü açmaz, sadece API çalışır, kaynak tüketimi azalır
  • –api: REST API endpoint’lerini aktif eder
  • –api-log: Her API isteğini loglar, debug için çok değerli
  • –medvram: 6-8GB VRAM’li kartlar için bellek optimizasyonu
  • –xformers: Dikkat mekanizması optimizasyonu, hem hız hem bellek için kritik
  • –no-half-vae: VAE’yi float32 ile çalıştırır, NaN artifact sorunlarını önler

Systemd service olarak tanımlamak production için şart:

cat > /etc/systemd/system/stable-diffusion-api.service << 'EOF'
[Unit]
Description=Stable Diffusion API Server
After=network.target
Wants=network.target

[Service]
Type=simple
User=sduser
Group=sduser
WorkingDirectory=/opt/stable-diffusion/stable-diffusion-webui
ExecStart=/opt/stable-diffusion/launch_api.sh
Restart=on-failure
RestartSec=10
StandardOutput=append:/var/log/stable-diffusion/service.log
StandardError=append:/var/log/stable-diffusion/error.log

# GPU erişimi için gerekli
Environment="PATH=/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
Environment="LD_LIBRARY_PATH=/usr/local/cuda/lib64"

# OOM durumunda restart
OOMScoreAdjust=500

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable stable-diffusion-api
systemctl start stable-diffusion-api

API ile İlk Bağlantı Testleri

Servis ayağa kalktıktan sonra API’nin gerçekten çalışıp çalışmadığını doğrulamak için birkaç kontrol:

# Servis durumu
curl -s http://localhost:7860/sdapi/v1/progress | python3 -m json.tool

# Yüklü modeller listesi
curl -s http://localhost:7860/sdapi/v1/sd-models | python3 -m json.tool | grep "model_name"

# Sistem bilgisi
curl -s http://localhost:7860/sdapi/v1/memory | python3 -m json.tool

Şimdi asıl işe koyulalım ve gerçek bir görsel üretelim. Önce basit bir test:

curl -X POST http://localhost:7860/sdapi/v1/txt2img 
  -H "Content-Type: application/json" 
  -d '{
    "prompt": "a photorealistic mountain landscape, golden hour, 8k, detailed",
    "negative_prompt": "blurry, low quality, watermark, text",
    "steps": 25,
    "cfg_scale": 7,
    "width": 512,
    "height": 512,
    "sampler_name": "DPM++ 2M Karras",
    "seed": -1,
    "save_images": true
  }' 
  | python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
with open('/tmp/test_output.png', 'wb') as f:
    f.write(base64.b64decode(data['images'][0]))
print('Görsel kaydedildi: /tmp/test_output.png')
print('Seed:', json.loads(data['info'])['seed'])
"

Production Pipeline: Python Client Kütüphanesi

Curl ile test tamam, ama gerçek pipeline için düzgün bir Python client yazmak gerekiyor. Aşağıdaki sınıf production’da kullandığım versiyonun sadeleştirilmiş hali:

#!/usr/bin/env python3
"""
Stable Diffusion API Production Client
Retry mekanizması, rate limiting ve health check dahil
"""

import time
import base64
import logging
import requests
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger('sd_client')


@dataclass
class GenerationConfig:
    prompt: str
    negative_prompt: str = "blurry, low quality, deformed, watermark"
    steps: int = 25
    cfg_scale: float = 7.0
    width: int = 512
    height: int = 512
    sampler_name: str = "DPM++ 2M Karras"
    seed: int = -1
    batch_size: int = 1
    restore_faces: bool = False
    hr_scale: float = 1.0  # >1 ise hires.fix aktif
    hr_upscaler: str = "Latent"
    denoising_strength: float = 0.7


class SDApiClient:
    def __init__(
        self,
        base_url: str = "http://localhost:7860",
        timeout: int = 300,
        max_retries: int = 3,
        retry_delay: float = 5.0
    ):
        self.base_url = base_url.rstrip('/')
        self.timeout = timeout
        self.max_retries = max_retries
        self.retry_delay = retry_delay
        self.session = requests.Session()

    def health_check(self) -> bool:
        try:
            resp = self.session.get(
                f"{self.base_url}/sdapi/v1/progress",
                timeout=10
            )
            return resp.status_code == 200
        except Exception as e:
            logger.error(f"Health check hatası: {e}")
            return False

    def wait_for_ready(self, max_wait: int = 120) -> bool:
        logger.info("API hazır olana kadar bekleniyor...")
        for i in range(max_wait // 5):
            if self.health_check():
                logger.info("API hazır")
                return True
            time.sleep(5)
        logger.error(f"{max_wait}s sonra API hala hazır değil")
        return False

    def generate(
        self,
        config: GenerationConfig,
        output_dir: Optional[Path] = None
    ) -> list[Path]:
        payload = {
            "prompt": config.prompt,
            "negative_prompt": config.negative_prompt,
            "steps": config.steps,
            "cfg_scale": config.cfg_scale,
            "width": config.width,
            "height": config.height,
            "sampler_name": config.sampler_name,
            "seed": config.seed,
            "batch_size": config.batch_size,
            "restore_faces": config.restore_faces,
        }

        if config.hr_scale > 1.0:
            payload.update({
                "enable_hr": True,
                "hr_scale": config.hr_scale,
                "hr_upscaler": config.hr_upscaler,
                "denoising_strength": config.denoising_strength,
            })

        for attempt in range(1, self.max_retries + 1):
            try:
                logger.info(
                    f"Görsel üretimi başlatıldı "
                    f"(deneme {attempt}/{self.max_retries})"
                )
                resp = self.session.post(
                    f"{self.base_url}/sdapi/v1/txt2img",
                    json=payload,
                    timeout=self.timeout
                )
                resp.raise_for_status()
                data = resp.json()

                import json as _json
                info = _json.loads(data.get('info', '{}'))
                seed = info.get('seed', 'unknown')
                logger.info(f"Üretim tamamlandı. Seed: {seed}")

                if output_dir:
                    output_dir = Path(output_dir)
                    output_dir.mkdir(parents=True, exist_ok=True)

                saved_paths = []
                for idx, img_b64 in enumerate(data['images']):
                    ts = int(time.time())
                    filename = f"sd_{ts}_{seed}_{idx}.png"
                    if output_dir:
                        path = output_dir / filename
                        with open(path, 'wb') as f:
                            f.write(base64.b64decode(img_b64))
                        saved_paths.append(path)
                        logger.info(f"Kaydedildi: {path}")

                return saved_paths

            except requests.exceptions.Timeout:
                logger.warning(
                    f"Timeout (deneme {attempt}). "
                    f"{self.retry_delay}s sonra tekrar denenecek."
                )
            except requests.exceptions.HTTPError as e:
                logger.error(f"HTTP hatası: {e}")
                if e.response.status_code in (400, 422):
                    raise  # Retry yapmanın anlamı yok
            except Exception as e:
                logger.error(f"Beklenmedik hata: {e}")

            if attempt < self.max_retries:
                time.sleep(self.retry_delay * attempt)

        raise RuntimeError(
            f"{self.max_retries} denemeden sonra görsel üretilemedi"
        )

Batch İşlem Pipeline’ı

Gerçek dünya senaryosu: bir e-ticaret sitesi için 500 ürün görseli üretmek, her ürünün farklı prompt’u var, bazıları hires.fix gerektiriyor, hataları loglamak gerekiyor.

#!/usr/bin/env python3
"""
Batch görsel üretim pipeline'ı
CSV'den prompt okur, görselleri üretir, rapor oluşturur
"""

import csv
import json
import time
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime

# Yukarıda tanımladığımız client ve config sınıflarını import ediyoruz

OUTPUT_BASE = Path("/data/generated-images")
REPORT_FILE = OUTPUT_BASE / "batch_report.jsonl"
FAILED_FILE = OUTPUT_BASE / "failed_prompts.csv"

def process_single(row: dict, client: SDApiClient) -> dict:
    """Tek bir satırı işle, sonucu döndür"""
    start = time.time()
    product_id = row['product_id']
    
    try:
        config = GenerationConfig(
            prompt=row['prompt'],
            negative_prompt=row.get(
                'negative_prompt',
                "blurry, low quality, watermark"
            ),
            width=int(row.get('width', 512)),
            height=int(row.get('height', 512)),
            steps=int(row.get('steps', 25)),
            hr_scale=float(row.get('hr_scale', 1.0)),
        )

        output_dir = OUTPUT_BASE / product_id
        paths = client.generate(config, output_dir=output_dir)

        duration = time.time() - start
        return {
            "product_id": product_id,
            "status": "success",
            "files": [str(p) for p in paths],
            "duration_seconds": round(duration, 2),
            "timestamp": datetime.utcnow().isoformat()
        }

    except Exception as e:
        duration = time.time() - start
        return {
            "product_id": product_id,
            "status": "failed",
            "error": str(e),
            "duration_seconds": round(duration, 2),
            "timestamp": datetime.utcnow().isoformat()
        }


def run_batch(csv_path: str, workers: int = 1):
    """
    workers=1 önerilir çünkü tek GPU paylaşımı
    birden fazla worker'la VRAM patlamasına yol açar
    """
    client = SDApiClient(max_retries=3)

    if not client.wait_for_ready(max_wait=180):
        raise RuntimeError("API başlatılamadı")

    with open(csv_path, 'r', encoding='utf-8') as f:
        rows = list(csv.DictReader(f))

    print(f"Toplam {len(rows)} görsel üretilecek")

    OUTPUT_BASE.mkdir(parents=True, exist_ok=True)
    success_count = 0
    failed_rows = []

    with open(REPORT_FILE, 'a', encoding='utf-8') as report_f:
        for i, row in enumerate(rows, 1):
            print(f"[{i}/{len(rows)}] İşleniyor: {row['product_id']}")
            result = process_single(row, client)
            report_f.write(json.dumps(result, ensure_ascii=False) + 'n')
            report_f.flush()

            if result['status'] == 'success':
                success_count += 1
            else:
                failed_rows.append(row)
                print(f"  HATA: {result['error']}")

    # Başarısız olanları tekrar denemek için kaydet
    if failed_rows:
        with open(FAILED_FILE, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=failed_rows[0].keys())
            writer.writeheader()
            writer.writerows(failed_rows)

    print(f"nTamamlandı: {success_count}/{len(rows)} başarılı")
    print(f"Rapor: {REPORT_FILE}")
    if failed_rows:
        print(f"Başarısız liste: {FAILED_FILE}")


if __name__ == "__main__":
    import sys
    run_batch(sys.argv[1])

İzleme ve VRAM Yönetimi

Production’da en büyük sorun GPU belleği. Stable Diffusion uzun süre çalışırken VRAM fragmentasyonu yaşanabilir ve performans düşer. Periyodik model unload/reload yapmak sorunu azaltıyor:

#!/bin/bash
# /opt/scripts/sd_health_monitor.sh
# Crontab: */5 * * * * /opt/scripts/sd_health_monitor.sh

LOG="/var/log/stable-diffusion/monitor.log"
API="http://localhost:7860"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> $LOG
}

# API erişilebilirlik kontrolü
if ! curl -sf "$API/sdapi/v1/progress" > /dev/null; then
    log "KRITIK: API yanıt vermiyor, servis yeniden başlatılıyor"
    systemctl restart stable-diffusion-api
    sleep 30
    exit 1
fi

# VRAM kullanımını kontrol et
VRAM_USED=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits | head -1)
VRAM_TOTAL=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | head -1)
VRAM_PCT=$((VRAM_USED * 100 / VRAM_TOTAL))

log "VRAM kullanımı: ${VRAM_USED}MB / ${VRAM_TOTAL}MB (%${VRAM_PCT})"

# %95 üzerinde VRAM kullanımında model unload
if [ $VRAM_PCT -gt 95 ]; then
    log "UYARI: VRAM kritik seviyede, model cache temizleniyor"
    curl -sf -X POST "$API/sdapi/v1/unload-checkpoint" > /dev/null
    sleep 5
    curl -sf -X POST "$API/sdapi/v1/reload-checkpoint" > /dev/null
    log "Model yeniden yüklendi"
fi

# GPU sıcaklık kontrolü
GPU_TEMP=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader | head -1)
if [ "$GPU_TEMP" -gt 85 ]; then
    log "UYARI: GPU sıcaklığı yüksek: ${GPU_TEMP}C"
fi

Model Yönetimi ve ControlNet Entegrasyonu

Birden fazla model kullanılacaksa model değiştirme API üzerinden yapılabilir:

# Mevcut aktif modeli öğren
curl -s http://localhost:7860/sdapi/v1/options 
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('sd_model_checkpoint'))"

# Model değiştir (ağır işlem, ~30s sürebilir)
curl -X POST http://localhost:7860/sdapi/v1/options 
  -H "Content-Type: application/json" 
  -d '{
    "sd_model_checkpoint": "realisticVisionV60B1_v60B1VAE.safetensors"
  }'

# ControlNet ile görsel üretme (ControlNet extension yüklüyse)
curl -X POST http://localhost:7860/sdapi/v1/txt2img 
  -H "Content-Type: application/json" 
  -d '{
    "prompt": "a woman standing, professional photo",
    "steps": 30,
    "width": 512,
    "height": 768,
    "alwayson_scripts": {
      "controlnet": {
        "args": [{
          "enabled": true,
          "module": "openpose",
          "model": "control_v11p_sd15_openpose",
          "weight": 0.8,
          "image": "<BASE64_POSE_IMAGE>",
          "resize_mode": 1,
          "control_mode": 0
        }]
      }
    }
  }' | python3 -c "
import sys,json,base64
d=json.load(sys.stdin)
open('controlnet_output.png','wb').write(base64.b64decode(d['images'][0]))
print('Tamamlandı')
"

Nginx ile Reverse Proxy ve Temel Güvenlik

API’yi dışarıya açmanız gerekiyorsa Nginx arkasına almak ve en azından basit bir API key kontrolü eklemek şart. Stable Diffusion API’si kendi başına hiçbir authentication mekanizması sunmuyor:

# /etc/nginx/sites-available/stable-diffusion
server {
    listen 443 ssl;
    server_name sd-api.sirketiniz.com;

    ssl_certificate /etc/letsencrypt/live/sd-api.sirketiniz.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sd-api.sirketiniz.com/privkey.pem;

    # Basit API key kontrolü
    # Gerçek bir auth için ayrı bir proxy servisi yazmanız önerilir
    location /sdapi/ {
        # Sadece belirli IP'lere izin ver (VPN IP aralığı)
        allow 10.0.0.0/8;
        allow 192.168.0.0/16;
        deny all;

        proxy_pass http://127.0.0.1:7860;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # Görsel üretimi uzun sürebilir
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;

        # Büyük base64 response'lar için
        proxy_buffer_size 128k;
        proxy_buffers 8 256k;
        proxy_busy_buffers_size 512k;

        client_max_body_size 50m;
    }
}

Sık Karşılaşılan Sorunlar

CUDA out of memory hatası: En sık karşılaşılan sorun. --medvram veya --lowvram ile başlatın. Hala oluyorsa batch_size’ı 1’e düşürün ve hires.fix’i devre dışı bırakın. Uzun çalışmalar sonrası yaşanıyorsa periyodik model reload scriptinizi çalıştırın.

“Model failed to load” hatası: Model dosyası corrupt olabilir. sha256sum ile hash kontrolü yapın ve modeli tekrar indirin. .safetensors formatı .ckpt‘ye kıyasla çok daha güvenilir.

API çok yavaş, timeout alıyorum: İlk istek her zaman yavaştır çünkü model GPU’ya yüklenir. Timeout değerlerinizi en az 300 saniyeye çekin. Steps sayısını düşürün, DPM++ 2M Karras sampler en iyi hız/kalite dengesini sunar.

Görseller tutarsız kalitede çıkıyor: Seed’i sabitleyerek test edin. Sorun devam ediyorsa --no-half-vae flag’ini ekleyin. CFG scale değeri çok yüksekse (>12) renk dağılımları bozulabilir.

Process çöküyor ama loglarda hata yok: OOM killer devreye girmiş olabilir. dmesg | grep -i "oom|killed" ile kontrol edin. Sistemd servis tanımındaki OOMScoreAdjust değerini düşürün (negatif değer = öldürülme önceliği düşük).

Sonuç

Stable Diffusion API’sini production’a taşımak, modeli çalıştırmaktan çok daha fazlasını gerektiriyor. VRAM yönetimi, retry mekanizmaları, izleme, güvenlik katmanı ve batch işlem koordinasyonu hepsinin bir arada düşünülmesi gerekiyor.

Burada anlattığım yapıyı küçük bir e-ticaret müşterisinin görsel üretim hattında yaklaşık altı aydır kullanıyoruz. Günde ortalama 800-1200 görsel üretiliyor, çalışma süresi %99’un üzerinde. En büyük sorun başta VRAM fragmentasyonu idi, periyodik model reload scriptiyle bunu büyük ölçüde çözdük.

ComfyUI’ye geçmeyi düşünüyorsanız, özellikle karmaşık workflow’larınız varsa (img2img zincirleme, LoRA karıştırma, ControlNet stack’leri) zaman ayırın. Öğrenme eğrisi var ama API üzerinden JSON workflow gönderme mekanizması çok daha deterministik ve yönetilebilir. Bunu da ayrı bir yazının konusu yapalım.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir