Ana içeriğe atla

GLM Image Eğitimi: Bir İnfografik Destesi Oluşturucu İnşa Etme

Z.ai’nin GLM-Image modelini kullanarak tek bir istemden eksiksiz bir infografik slayt destesi oluşturmayı öğrenin.
Güncel 3 Eki 2026  · 12 dk. oku

Yapay Zeka ile Keşfedin

ChatGPTClaudePerplexity

İnfografikler ve slayt desteleri, yalnızca görseller üretmediğiniz, aynı zamanda başlıklar, madde işaretleri, grafikler ve alt bilgi gibi yapılandırılmış bilgiler oluşturup metin ve yerleşimi tutarlı tuttuğunuz için en zor görsel üretim görevlerindendir. GLM-Image, Z.ai’nin, açık tasarım kısıtlarıyla yönlendirildiğinde yüksek çözünürlüklü, metin yoğun, yerleşim farkındalığı olan görseller üretebilen görüntü üretim modelidir.

Bu eğitimde, basit bir Streamlit uygulamasıyla istemden infografik destesi üreten bir sistemin nasıl kurulacağını adım adım göstereceğim. Neredeyse her konuyu girebilirsiniz. Gemini 2.5 Flash, web destekli araştırma yaparak güncel istatistik ve trendleri çıkarır, LLM bu araştırmayı katı bir slayt JSON planına dönüştürür ve GLM-Image her slaytı yüksek çözünürlüklü bir infografik olarak işler.

Sonunda, tüm ağır işlemlerin API’ler üzerinden gerçekleşmesi sayesinde GPU gerekmeden tek bir istemi indirilebilir bir desteye dönüştüren eksiksiz bir hattınız olacak. İş akışını gösteren hızlandırılmış videoya göz atın:

GLM Image nedir?

GLM-Image, özellikle posterler, PPT tarzı görseller ve bilimsel diyagramlar gibi bilgi yoğun, yerleşime duyarlı üretimi ele almak için ( glm-image model kimliği üzerinden sunulan) Z.ai’nin metinden görüntüye modelidir.

Şekil: GLM-Image: Yoğun Bilgi ve Yüksek Doğruluklu Görüntü Üretimi için Otoregresif

Arka planda, talimat anlama ve genel yerleşime odaklanan otoregresif bir omurga ile yüksek frekanslı ayrıntıları (daha keskin metin çizgileri dahil) rafine eden difüzyon kod çözücüsünü birleştiren hibrit bir otoregresif ve difüzyon kod çözücü tasarımı kullanır. Bu hibrit kurulum, GLM-Image’ın, pek çok saf difüzyon üreticisine kıyasla metin yoğun görsellerde daha güvenilir olmasının önemli bir nedenidir.

Bu eğitimde GLM-Image’ı her slayt için oluşturma motoru olarak kullanıyoruz. Onu muğlak istemlerle beslemek yerine, şu unsurları kesin olarak belirleyen, yerleşimin farkında bir istem veriyoruz:

  • Tek, düz bir arka plan rengi (gradyan yok),
  • Palet kısıtları (arka plan / vurgu / metin),
  • Açık tipografi hiyerarşisi (başlık vs. gövde vs. alt not),
  • Slayt türüne göre açık yerleşim kuralları (istatistik, karşılaştırma, zaman çizelgesi, süreç, liste),
  • İsteğe bağlı grafik çizim talimatları (çubuk / pasta / çizgi),
  • Alt notlar ve slayt numaralandırması.

Not: GLM-Image bir görsel URL’si döndürür, 16:9 gibi yaygın sunum en-boy oranlarını destekler ve 1728×960 gibi önerilen boyutlarda en iyi sonucu verir.

GLM Image Eğitimi: Gerçek Zamanlı Bir İnfografik Oluşturucu Kurun

Bu bölümde, Streamlit uygulaması içine sarılı Gemini 2.5 Flash ve GLM-Image ile çalışan bir istemden infografik slayt destesi hattı oluşturacağız. Üst düzeyde uygulama üç temel görev yapar:

  • Konu hakkında taze web verileriyle araştırma yapar; böylece deste, eski model belleğine değil, güncel sayı ve trendlere dayanır
  • Araştırmayı, istatistik, zaman çizelgesi, karşılaştırma, süreç ve özet gibi slayt yerleşimlerini tanımlayan katı bir JSON şemasını içeren yapılandırılmış bir slayt planına dönüştürür
  • Her slaytı GLM-Image kullanarak eksiksiz bir infografik görseli olarak oluşturur; tipografi hiyerarşisi, tema renkleri, yerleşim kısıtları, isteğe bağlı grafikler ve slayt numaralandırması dahil.

Perde arkasında iş akışı iki kilit aşama tarafından yönlendirilir:

  • Aşama 1: research_topic(topic) işlevi, web araması etkin Gemini 2.5 Flash’ı kullanarak konuya özel, veri açısından zengin bir araştırma özeti üretir.
  • Aşama 2: structure_slides() işlevi bu araştırmayı bir JSON desteye çevirir; ardından bu plan ayrıntılı GLM-Image istemlerine dönüştürülerek slayt slayt işlenir.

Adım 1: API anahtarlarını edinin

İnfografik oluşturucuyu kurmadan önce, temel hattı çalıştıran iki harici hizmete erişmemiz gerekiyor:

  • OpenRouter, web destekli araştırma ve slayt yapılandırma için kullandığımız Gemini 2.5 Flash’a erişim sağlar.
  • Z.ai, GLM-Image modelini API ve HuggingFace üzerinden barındırır ve son infografik slaytları üretir.

Bu ayrım bilinçlidir. Metin modeli veri toplama ve planlamayı üstlenirken, GLM-Image tamamen görsel üretime odaklanır.

Adım 1.1: OpenRouter API Anahtarı

OpenRouter, tek bir anahtarla birden fazla modele (Gemini, OpenAI, Claude ve diğerleri) erişim sağlayan birleşik bir geçit olarak çalışır.

  • OpenRouter’da bir hesap oluşturun ve bir API anahtarı üretin.
  • Küçük bir kredi ekleyin (yaklaşık 5$). Bu, bu demoyu birkaç kez çalıştırmak ve gelecekteki denemeler için yeterlidir.

Bu eğitimde, yalnızca model eğitim bilgisine güvenmek yerine uygulamanın güncel veri ve istatistikleri çekmesini sağlayan web aramasını desteklediği için Gemini 2.5 Flash kullanıyoruz. İsterseniz desteklenen başka bir modeli de kullanabilirsiniz.

Adım 1.2: Z.ai API Anahtarı

Sonraki adımda, GLM Image modeline erişmek için Z.ai üzerinde bir hesap oluşturun.

Z.ai API Key

  • API Management’a gidin ve Billing’i seçin
  • Add Payment Method’a tıklayın ve kart bilgilerinizi girin
  • Görüntü üretim uç noktası için bir API anahtarı oluşturun

Bu anahtar, her slaydın işlenmesi için glm-image modelini çağırmakta kullanılacaktır.

Adım 1.3: Anahtarları ortam değişkeni olarak saklayın

Her iki anahtar da oluşturulduktan sonra, aşağıdaki komutları kullanarak ortam değişkenleri olarak saklayın:

export OPENROUTER_API_KEY="Your_OpenRouter_API_Key"
export ZAI_API_KEY="Your_ZAI_API_Key"

Bu ortam değişkenleri, kimlik bilgilerini kod tabanınızın dışında tutar ve Streamlit Cloud, Docker veya diğer ortamlara dağıtımı çok daha kolay hale getirir. 

Adım 2: Bağımlılıkları yükleyin

Şimdi gerekli bağımlılıkları yükleyip Streamlit uygulamasını oluşturmaya başlayacağız.

pip install streamlit requests Pillow

Paketler yüklendikten sonra, uygulama boyunca kullanacağımız standart kütüphaneleri ve iki API uç noktasını tanımlayan birkaç sabiti içe aktarıyoruz.

import streamlit as st
import json
import time
import requests
import os
import io
import re
import zipfile
from datetime import date
from PIL import Image
from typing import Optional, List, Dict
OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
OPENROUTER_MODEL = "google/gemini-2.5-flash"
GLM_IMAGE_API_URL = "https://api.z.ai/api/paas/v4/images/generations"
GLM_IMAGE_MODEL = "glm-image"

Bu kütüphaneler, uygulamanın çalışma zamanını oluşturur. Streamlit etkileşimli web arayüzünü çalıştırır, Requests OpenRouter’a (Gemini 2.5 Flash) ve Z.ai’ye (GLM-Image) yapılan API çağrılarını yönetir, Pillow ise üretilen görselleri yükleyip görüntülememizi sağlar. Ek standart kütüphaneler JSON ayrıştırma, ortam değişkenleri, zamanlama ve sonuçları ZIP dosyaları olarak dışa aktarmayı yönetir. 

Ayrıca OpenRouter ve GLM-Image uç noktaları için sabitleri tanımlarız; böylece uygulama, araştırma, yapılandırma ve görsel üretim isteklerini nereye göndereceğini bilir. Bu kurulum, tüm ağır hesaplamalar API’ler üzerinden yapıldığı için ortamı hafif tutar.

Adım 3: Çözünürlükleri ve sunum temalarını tanımlayın

Bu adımda, çözünürlük hazır ayarlarının küçük bir kümesini ve her GLM-Image istemine enjekte edeceğimiz arka plan rengi, vurgu rengi, tipografi havası ve infografik stil anlatımını kontrol eden bir tema kütüphanesini tanımlıyoruz.

RESOLUTIONS = {
    "16:9 Landscape (1728×960)": "1728x960",
    "3:2 Landscape (1568×1056)": "1568x1056",
    "1:1 Square (1280×1280)": "1280x1280",
    "2:3 Portrait (1056×1568)": "1056x1568",
}
THEMES = {
    "Corporate Navy": {
        "desc": "Professional dark blue palette, clean sans-serif, data-driven",
        "colors": {"bg": "#0F172A", "accent": "#38BDF8", "text": "#F1F5F9"},
        "style": (
            "professional corporate infographic, dark navy blue (#0F172A) background, "
            "cyan (#38BDF8) accent color, clean white (#F1F5F9) text, "
            "modern sans-serif typography, clean grid layout, subtle geometric decorations, "
            "professional data visualization elements, business presentation quality"
        ),
    },
    "Neon Noir": {
        "desc": "Dark mode with vivid neon pink accents, tech-forward aesthetic",
        "colors": {"bg": "#1A1A2E", "accent": "#E94560", "text": "#EAEAEA"},
        "style": (
            "dark mode tech infographic, deep dark (#1A1A2E) background, "
            "vivid neon pink (#E94560) accent color, light gray (#EAEAEA) text, "
            "bold modern typography with sharp edges, neon glow effects, "
            "futuristic layout with geometric shapes, startup pitch deck aesthetic"
        ),
    },
    "Warm Paper": {
        "desc": "Cream textured backgrounds, burnt orange accents, editorial feel",
        "colors": {"bg": "#F8F5F0", "accent": "#D4641A", "text": "#1C1917"},
        "style": (
            "warm editorial infographic on cream (#F8F5F0) textured paper background, "
            "burnt orange (#D4641A) accent color, dark brown (#1C1917) text, "
            "elegant serif typography mixed with clean sans-serif, "
            "magazine editorial layout, hand-drawn illustration elements, "
            "warm earth tones, scholarly professional design"
        ),
    },
    "Cyber Teal": {
        "desc": "Black backgrounds, electric teal highlights, terminal-inspired",
        "colors": {"bg": "#0C0A09", "accent": "#22D3EE", "text": "#FAFAF9"},
        "style": (
            "cyberpunk infographic, black (#0C0A09) background, "
            "electric teal (#22D3EE) accent color, white (#FAFAF9) text, "
            "monospace and tech typography, terminal-inspired layout elements, "
            "circuit board patterns, data stream visuals, hacker aesthetic, "
            "sharp angular design elements"
        ),
    },
    "Indigo Dream": {
        "desc": "Deep purple base with soft violet accents, premium feel",
        "colors": {"bg": "#1E1B4B", "accent": "#A78BFA", "text": "#EDE9FE"},
        "style": (
            "premium luxury infographic, deep indigo (#1E1B4B) background, "
            "soft violet (#A78BFA) accent color, lavender (#EDE9FE) text, "
            "elegant modern typography, gradient overlays, "
            "smooth rounded shapes, glassmorphism effects, "
            "high-end presentation quality with subtle glow effects"
        ),
    },
    "Solar Gold": {
        "desc": "Warm cream with golden accents, confident and bold",
        "colors": {"bg": "#FEFCE8", "accent": "#A16207", "text": "#1C1917"},
        "style": (
            "premium golden infographic, warm cream (#FEFCE8) background, "
            "rich gold (#A16207) accent color, dark (#1C1917) text, "
            "bold confident typography, art deco inspired geometric patterns, "
            "luxury brand aesthetic, clean information hierarchy, "
            "warm highlights and gold foil style embellishments"
        ),
    },
    "Forest Green": {
        "desc": "Dark green palette, nature-inspired, calming data storytelling",
        "colors": {"bg": "#052E16", "accent": "#4ADE80", "text": "#F0FDF4"},
        "style": (
            "nature-inspired infographic, deep forest green (#052E16) background, "
            "bright green (#4ADE80) accent color, light mint (#F0FDF4) text, "
            "organic flowing typography, leaf and nature motif decorations, "
            "sustainability report aesthetic, calming visual design, "
            "clean environmental data presentation"
        ),
    },
    "Crimson Bold": {
        "desc": "High contrast red on dark, attention-grabbing poster style",
        "colors": {"bg": "#1C1917", "accent": "#EF4444", "text": "#FAFAF9"},
        "style": (
            "bold poster-style infographic, dark charcoal (#1C1917) background, "
            "vivid crimson red (#EF4444) accent color, white (#FAFAF9) text, "
            "oversized impactful numbers, bold condensed typography, "
            "high contrast design, attention-grabbing layout, "
            "social media optimized visual hierarchy"
        ),
    },
}
EXAMPLE_TOPICS = [
    "AI Industry Market Trends January 2026",
    "Global Electric Vehicle Sales 2025",
    "SpaceX Starship Progress 2025-2026",
    "State of Remote Work 2026",
    "Bitcoin & Crypto Market February 2026",
    "Climate Change Key Metrics 2025",
]

RESOLUTIONS sözlüğü, bir etiketi Z.ai görüntü üretim uç noktasının beklediği tam GxY dizesine eşler. Buradaki temel fayda öngörülebilirliktir; yani istem mantığını değiştirmeden formatları (geniş slaytlar vs kare sosyal görseller vs dikey) değiştirebilirsiniz. En-boy oranları:

  • 16:9, klasik slayt desteleri ve sunum tarzı infografikler için en iyisidir.
  • 1:1, LinkedIn/Twitter karuseli benzeri görseller için iyi çalışır.
  • 2:3, mobil öncelikli infografik posterler için kullanışlıdır.

Ayrıca her temanın üç bileşenden oluştuğu THEMES tanımlıyoruz:

  • desc: Kenar çubuğunda gösterilen kısa bir arayüz açıklaması
  • colors: Katı paletimizi içerir (arka plan, vurgu, metin)
  • style: GLM-Image’ı tutarlı yerleşim, tipografi ve süslemeye yönlendiren isteme uygun stil bloğu

Pratikte bu tema nesnesi, her slayt için yeniden kullanılabilir bir “istem öneki” haline gelir; böylece deste, birbiriyle alakasız 7 görsel yerine tek bir ürün gibi okunur.

Adım 4: Aşama 1 - Gemini 2.5 Flash ile araştırma

Herhangi bir slayt üretmeden önce, konu hakkında güncel verilere ihtiyacımız var. Bir modelin statik bilgisine güvenmek yerine, bu adım OpenRouter üzerinden web araması etkin Gemini 2.5 Flash’ı kullanarak taze istatistikler, zaman çizelgeleri ve kilit içgörüler toplar.

def call_openrouter(
    messages: List[Dict],
    api_key: str,
    use_web_search: bool = False,
    temperature: float = 0.7,
    max_tokens: int = 4096,
) -> str:
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "HTTP-Referer": "https://glm-image-infographic-demo.streamlit.app",
        "X-Title": "GLM-Image Infographic Generator",
    }

    payload = {
        "model": OPENROUTER_MODEL,
        "messages": messages,
        "temperature": temperature,
        "max_tokens": max_tokens,
    }
    if use_web_search:
        today = date.today().strftime("%B %d, %Y")
        payload["plugins"] = [{
            "id": "web",
            "max_results": 10,
            "search_prompt": (
                f"Use these recent web search results (searched on {today}) to answer "
                f"with the most up-to-date information available:"
            ),
        }]

    resp = requests.post(OPENROUTER_API_URL, headers=headers, json=payload, timeout=120)
    resp.raise_for_status()
    data = resp.json()
    content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
    if not content:
        raise ValueError(f"Empty response from OpenRouter: {json.dumps(data, indent=2)[:500]}")
    return content
def research_topic(topic: str, api_key: str) -> str:
    today = date.today().strftime("%B %d, %Y")  # e.g. "February 13, 2026"
    current_year = date.today().year

    messages = [
        {
            "role": "system",
            "content": (
                f"You are a focused research analyst. Today's date is {today}. "
                f"CRITICAL: You must research ONLY the specific topic provided. Do NOT include "
                f"information about unrelated topics, industries, or subjects. If web search returns "
                f"irrelevant results, ignore them completely. "
                f"Your training data may be outdated. Rely primarily on the web search results "
                f"provided to you for current data. Prioritize sources from {current_year} and "
                f"late {current_year - 1}. If a data point comes from an older source, note the year. "
                f"Compile comprehensive, data-rich research on the given topic. Include specific "
                f"numbers, percentages, dollar amounts, growth rates, timelines, and key players. "
                f"Provide data suitable for multiple visualization formats: charts, comparisons, "
                f"timelines, and processes. Be thorough but concise. Stay strictly on topic."
            ),
        },
        {
            "role": "user",
            "content": (
                f"Research ONLY this specific topic and provide a data-rich summary:\n\n"
                f"**{topic}**\n\n"
                f"CRITICAL INSTRUCTIONS:\n"
                f"1. Research EXCLUSIVELY about '{topic}' - nothing else\n"
                f"2. Ignore any web search results not directly related to '{topic}'\n"
                f"3. Do NOT include data about semiconductors, smartphones, grooming products, or other "
                f"unrelated industries unless they are directly part of '{topic}'\n"
                f"4. Search for the most recent data available as of {today}\n"
                f"5. Prefer sources published in {current_year}\n\n"
                f"For '{topic}' specifically, include:\n"
                f"- Key statistics with specific numbers, percentages, or dollar amounts\n"
                f"- Comparison data directly relevant to '{topic}'\n"
                f"- Timeline or chronological events related to '{topic}'\n"
                f"- Process or methodology descriptions relevant to '{topic}'\n"
                f"- Market sizes, growth rates, and future outlook for '{topic}'\n"
                f"- Major players/entities involved in '{topic}'\n\n"
                f"Structure the data so it can be visualized as bar charts, pie charts, "
                f"timelines, comparison tables, and process diagrams - all focused on '{topic}'."
            ),
        },
    ]
    return call_openrouter(messages, api_key, use_web_search=True, temperature=0.5)

Yukarıdaki kod, hattın araştırma katmanını oluşturan iki temel yardımcı işlevi tanımlar.

  • call_openrouter() işlevi, Gemini 2.5 Flash modelini kullanarak OpenRouter’a sohbet istekleri göndermek için bir arayüz görevi görür. İstek yükünü model adı, mesajlar, sıcaklık ve token sınırlarıyla oluşturur ve kimlik doğrulamayı API anahtarıyla yönetir. 
  • use_web_search=True olduğunda işlev, web eklentisini etkinleştirir ve modelin en güncel bilgileri önceliklendirmesi için bir arama istemi ekler. İsteği gönderdikten sonra yanıtı doğrular ve oluşturulan metni çıkarır; böylece çıktı boş veya hatalıysa hattın devam etmesini engeller.
  • research_topic() işlevi, infografik üretimi için tasarlanmış konuya özel bir araştırma istemi kurar. İstatistikler, büyüme oranları, pazar büyüklükleri, zaman çizelgeleri, karşılaştırmalar ve kilit aktörler gibi nicel bilgileri açıkça talep eder; böylece bunlar daha sonra grafikler, tablolar veya diyagramlar olarak görselleştirilebilir. İşlev ardından çıktının yaratıcı değil, olgusal ve yapılandırılmış kalması için call_openrouter()’ı daha düşük sıcaklıkla çağırır.

Toplanan verilerin kapsamı, güncelliği ve doğruluğu arasındaki dengeyi kontrol etmek için sıcaklık, web sonuçlarının sayısı veya araştırma talimatlarının katılığı gibi parametrelerle deney yapabilirsiniz.

Bir sonraki adımda, bu ham araştırmayı, GLM-Image modeline göndermeden önce her slayt için yerleşimi, temel metrikleri ve görsel öğeleri tanımlayan yapılandırılmış bir slayt şemasına (JSON) dönüştüreceğiz.

Adım 5: Aşama 2 - Araştırmayı yapılandırılmış slayt JSON’una dönüştürün

Araştırma hazır olduğunda, bir sonraki adım bunu GLM-Image’ın tutarlı şekilde işleyebileceği yapılandırılmış bir taslağa dönüştürmektir. Paragraflar geçirmek yerine, her slaytın yerleşimini, ana metnini ve isteğe bağlı grafik verilerini belirten katı bir JSON planı üretiriz.

def structure_slides(topic: str, research: str, num_slides: int, api_key: str) -> List[Dict]:
    system_prompt = f"""You create structured infographic slide decks. Return ONLY a valid JSON array with exactly {num_slides} slide objects. No markdown backticks. No explanation. Just the JSON.
SLIDE FORMATS:
Slide 1 — Title:
{{"type":"title","slide_number":1,"headline":"Bold Title (max 8 words)","subheadline":"One-liner with a key stat","tag":"CATEGORY LABEL","visual_elements":"2-3 topic-relevant decorative motifs (e.g. globe icon, circuit pattern for tech topics)"}}
Slides 2 to {num_slides-1} — Content (pick ONE layout per slide):
Layout "stat" — Use when there is one standout number to feature:
{{"type":"content","slide_number":N,"layout":"stat","headline":"Section Title (max 6 words)","key_stat":"$4.2T or +340%","key_stat_label":"What the stat means","bullets":["3-4 bullets with specific numbers"],"visual_elements":"2-3 topic-relevant icons","chart_data":{{"type":"bar|pie|line","title":"Chart title","data":[{{"label":"Category","value":"123"}}]}},"bottom_note":"Source"}}
Layout "comparison" — Use for side-by-side comparisons:
{{"type":"content","slide_number":N,"layout":"comparison","headline":"Section Title","items":[{{"label":"Item A","points":["fact 1 with number","fact 2 with number"]}},{{"label":"Item B","points":["fact 1","fact 2"]}}],"visual_elements":"topic-relevant icons","bottom_note":"Source"}}
Layout "timeline" — Use for chronological progressions:
{{"type":"content","slide_number":N,"layout":"timeline","headline":"Section Title","milestones":[{{"date":"2020","event":"Event description with metric"}},{{"date":"2023","event":"Another event"}}],"visual_elements":"topic-relevant icons","bottom_note":"Source"}}
Layout "process" — Use for step-by-step flows:
{{"type":"content","slide_number":N,"layout":"process","headline":"Section Title","steps":[{{"step":"1","title":"Step Name","detail":"Description with metric"}},{{"step":"2","title":"Next Step","detail":"Description"}}],"visual_elements":"topic-relevant icons","bottom_note":"Source"}}
Layout "list" — Use for data-rich bullets without a dominant stat:
{{"type":"content","slide_number":N,"layout":"list","headline":"Section Title","bullets":["4-5 bullets each with a specific number/metric"],"visual_elements":"topic-relevant icons","chart_data":{{"type":"bar|pie|line","title":"Chart title","data":[{{"label":"Category","value":"123"}}]}},"bottom_note":"Source"}}
Slide {num_slides} — Summary:
{{"type":"summary","slide_number":{num_slides},"headline":"Key Takeaways","takeaways":["4-5 punchy takeaway lines with data"],"outlook":"One sentence future outlook","visual_elements":"topic-relevant closing motif"}}
RULES:
- Every bullet MUST contain a specific number, percentage, or dollar amount
- Use a MIX of layouts across the deck. Do NOT use "stat" for every slide. Choose the layout that best fits the content. At most half of content slides should use "stat" layout.
- key_stat is ONLY used with "stat" layout. Other layouts do NOT have key_stat.
- visual_elements MUST be topic-specific (e.g. for EV topic: "electric car silhouette, battery icon, charging station"; for AI topic: "neural network diagram, robot icon, chip illustration"; for climate: "globe, thermometer, polar bear"). NEVER use generic shapes.
- chart_data is OPTIONAL. Include it when the slide's data naturally fits a chart. Fields:
  * type: "bar", "pie", or "line"
  * title: short chart title
  * data: array of {{"label":"...","value":"..."}} with real numbers from the research
  * axis_label: (optional) label for the value axis
  Not every slide needs chart_data. Use it for stat and list layouts when data is quantitative.
- Each slide covers a distinct aspect — no repetition
- Ensure logical narrative flow across slides"""
    messages = [
        {"role": "system", "content": system_prompt},
        {
            "role": "user",
            "content": (
                f"Create a {num_slides}-slide infographic deck about:\n\n"
                f"**{topic}**\n\n"
                f"RESEARCH DATA:\n{research}\n\n"
                f"Return ONLY the JSON array."
            ),
        },
    ]
    raw = call_openrouter(messages, api_key, use_web_search=False, temperature=0.2)
    raw = re.sub(r"^```(?:json)?\s*", "", raw.strip())
    raw = re.sub(r"\s*```$", "", raw.strip())
    return json.loads(raw)
def _build_chart_instruction(chart_data: Dict, accent_hex: str) -> str:
    if not chart_data or not chart_data.get("type"):
        return ""
    chart_type = chart_data["type"]
    title = chart_data.get("title", "Chart")
    data = chart_data.get("data", [])
    axis_label = chart_data.get("axis_label", "")
    if not data:
        return ""
    if chart_type == "bar":
        bars = ", ".join([f"'{d['label']}': {d['value']}" for d in data])
        axis_part = f" Y-axis label: '{axis_label}'." if axis_label else ""
        return (
            f"Draw a vertical bar chart titled '{title}' in the lower-right area of the slide, "
            f"taking up about 40% of the slide width. "
            f"Bars: [{bars}]. Label each bar clearly below it with its category name. "
            f"Display the value on top of each bar.{axis_part} "
            f"Use {accent_hex} color for the bars with slight shade variation between them. "
        )
    elif chart_type == "pie":
        segments = ", ".join([f"'{d['label']}': {d['value']}%" for d in data])
        return (
            f"Draw a pie chart titled '{title}' in the lower-right area of the slide, "
            f"taking up about 35% of the slide width. "
            f"Segments: [{segments}]. Label each segment with its name and percentage value. "
            f"Use {accent_hex} as the primary segment color with contrasting shades for other segments. "
        )
    elif chart_type == "line":
        points = ", ".join([f"({d['label']}, {d['value']})" for d in data])
        axis_part = f" Y-axis: '{axis_label}'." if axis_label else ""
        return (
            f"Draw a line chart titled '{title}' in the lower-right area of the slide, "
            f"taking up about 40% of the slide width. "
            f"Data points: [{points}]. Mark each data point with a dot and label its value. "
            f"Connect points with a smooth line in {accent_hex} color.{axis_part} "
        )
    elif chart_type == "table":
        rows = " | ".join([f"{d['label']}: {d['value']}" for d in data])
        return (
            f"Draw a data table titled '{title}' in the lower portion of the slide. "
            f"Data: [{rows}]. Use alternating row shading with {accent_hex} colored header row. "
            f"Keep text small but legible. "
        )
    return ""

Aşama 2’de iki ana işlev tanımlarız: biri slaytları yapılandırmak, diğeri ise grafik veya diyagram talimatları oluşturmaktır. Bu işlevler şunları içerir:

  • structure_slides() işlevi: Bu işlev, yapılandırma motoru olarak (web araması olmadan) Gemini’yi kullanır. Modeli, tam olarak istenen sayıda slayt içeren geçerli bir JSON dizi döndürmeye zorlayan bir sistem istemi oluşturur. Her deste sabit bir anlatı örüntüsünü izler: bir başlık slaydı, birden çok içerik slaydı ve son bir özet. İçerik slaytları istatistik, karşılaştırma, zaman çizelgesi, süreç veya liste gibi farklı yerleşimler kullanabilir ve istem, şu gibi önemli kısıtları uygular:
  • Her madde işareti somut bir metrik içermelidir (sayı, yüzde veya dolar değeri)
  • Tekrarı önlemek için yerleşimler karıştırılmalıdır
  • Görsel unsurlar konuya özgü olmalıdır
  • İçerik doğal olarak bir grafiğe uygunsa isteğe bağlı chart_data eklenir
  • İşlev daha sonra istikrarlı çıktı için call_openrouter()’ı düşük sıcaklıkla çağırır, olası yanlışlıkla eklenmiş markdown sarmalayıcılarını temizler ve yanıtı aşağı akışta kullanılmak üzere bir Python sözlüğüne ayrıştırır.
  • _build_chart_instruction() işlevi: Bu yardımcı, JSON’daki isteğe bağlı chart_datayı GLM-Image için çizim talimatlarına dönüştürür. Grafik türüne (çubuk, pasta, çizgi veya tablo) bağlı olarak, temanın vurgu rengine göre yerleşim, etiketleme, boyutlandırma ve renk kullanımı hakkında net yönergeler üretir. 

Bu adımın sonunda, destenin hem anlatı akışını hem de görsel yapısını tanımlayan temiz bir slayt düzeyi spesifikasyonuna sahibiz.

Adım 6: Slayt JSON’unu GLM-Image istemlerine dönüştürün

Bu adımda, her slayt nesnesini görüntü modeline neyi, nereye ve nasıl çizmesi gerektiğini tam olarak söyleyen, yüksek kısıtlı bir isteme çeviriyoruz.

def build_glm_prompt(slide: Dict, theme: Dict, total: int, topic: str) -> str:
    style = theme["style"]
    colors = theme["colors"]
    bg_hex = colors["bg"]
    accent_hex = colors["accent"]
    text_hex = colors["text"]
    bg_anchor = (
        f"BACKGROUND: Use a solid, uniform {bg_hex} background across the entire image. "
        f"Do not add gradients, varying tones, or alternate background colors. "
        f"The background must be exactly {bg_hex}. "
    )
    common_footer = (
        f"Typography hierarchy: Headlines in bold large font, subheadings in semibold medium font, "
        f"body text in regular smaller font, footnotes in small font. "
        f"All text must be {text_hex} color unless it is an accent element using {accent_hex}. "
        f"In the bottom-right corner, display '{slide.get('slide_number', 1)} / {total}' in small text using {text_hex} at 50% opacity. "
        f"The overall design should be a single cohesive infographic slide image, "
        f"high resolution, crisp text rendering, professional quality. "
        f"All text in the image must be clearly legible and accurately spelled."
    )
    visual_elements = slide.get("visual_elements", "")
    visual_elements_instruction = (
        f"Include these topic-relevant visual elements as subtle icons or illustrations: {visual_elements}. "
        if visual_elements else ""
    )
    if slide["type"] == "title":
        return (
            f"{bg_anchor}"
            f"Create a professional infographic title slide. {style}. "
            f"Display these text elements in the image:\n"
            f"- Small uppercase tag at the top-center: '{slide.get('tag', 'REPORT')}'\n"
            f"- Large bold headline centered in the middle of the slide: '{slide['headline']}'\n"
            f"- Smaller subtitle centered below the headline: '{slide['subheadline']}'\n\n"
            f"The headline should be the dominant visual element. "
            f"{visual_elements_instruction}"
            f"{common_footer}"
        )
    elif slide["type"] == "summary":
        items = "\n".join(
            [f"  {i+1}. '{t}'" for i, t in enumerate(slide.get("takeaways", []))]
        )
        return (
            f"{bg_anchor}"
            f"Create a professional infographic summary slide. {style}. "
            f"Display these text elements in the image:\n"
            f"- Header at top-left: '{slide['headline']}'\n"
            f"- Numbered takeaway list in the center area:\n{items}\n"
            f"- Italic footer outlook at bottom-left: '{slide.get('outlook', '')}'\n\n"
            f"Use numbered circles in {accent_hex} color or checkmarks for each takeaway. "
            f"Make the layout clean and conclusive. "
            f"{visual_elements_instruction}"
            f"{common_footer}"
        )
    else: 
        layout = slide.get("layout", "stat")
        chart_instruction = _build_chart_instruction(slide.get("chart_data", {}), accent_hex)
        if layout == "stat":
            bullets_text = "\n".join([f"  • '{b}'" for b in slide.get("bullets", [])])
            has_chart = bool(chart_instruction)
            bullet_position = "in the left half of the slide" if has_chart else "below the statistic"
            return (
                f"{bg_anchor}"
                f"Create a professional infographic content slide. {style}. "
                f"Display these text elements in the image:\n"
                f"- Section header at top-left: '{slide['headline']}'\n"
                f"- One large featured statistic displayed very prominently in the center: '{slide.get('key_stat', '')}'\n"
                f"- Label below the stat in smaller text: '{slide.get('key_stat_label', '')}'\n"
                f"- Bullet points with data {bullet_position}:\n{bullets_text}\n"
                f"- Footer at bottom: '{slide.get('bottom_note', '')}'\n\n"
                f"The key statistic should be the most visually dominant element — "
                f"displayed in a very large, bold font in {accent_hex} color inside a highlight box. "
                f"Bullets should use small {accent_hex} colored icons or dot indicators. "
                f"{chart_instruction}"
                f"{visual_elements_instruction}"
                f"{common_footer}"
            )
        elif layout == "comparison":
            items = slide.get("items", [])
            items_text = ""
            for idx, item in enumerate(items):
                label = item.get("label", f"Item {idx+1}")
                points = item.get("points", [])
                points_text = "\n".join([f"    - '{p}'" for p in points])
                col_pos = "left column" if idx == 0 else "right column"
                items_text += f"  [{col_pos}] '{label}':\n{points_text}\n"
            return (
                f"{bg_anchor}"
                f"Create a professional infographic comparison slide. {style}. "
                f"Display these text elements in the image:\n"
                f"- Section header at top-left: '{slide['headline']}'\n"
                f"- Side-by-side comparison layout with a vertical divider line in {accent_hex}:\n{items_text}"
                f"- Footer at bottom: '{slide.get('bottom_note', '')}'\n\n"
                f"Use a clean two-column layout. Each column header should be bold and in {accent_hex}. "
                f"Bullet points under each column should have small icons. "
                f"Add a 'VS' or divider element between the columns. "
                f"{visual_elements_instruction}"
                f"{common_footer}"
            )
        elif layout == "timeline":
            milestones = slide.get("milestones", [])
            milestones_text = "\n".join(
                [f"  [{m.get('date', '')}] → '{m.get('event', '')}'" for m in milestones]
            )
            return (
                f"{bg_anchor}"
                f"Create a professional infographic timeline slide. {style}. "
                f"Display these text elements in the image:\n"
                f"- Section header at top-left: '{slide['headline']}'\n"
                f"- Horizontal timeline with these milestones connected by a line:\n{milestones_text}\n"
                f"- Footer at bottom: '{slide.get('bottom_note', '')}'\n\n"
                f"Draw a horizontal timeline line in {accent_hex} running across the center of the slide. "
                f"Place milestone dates above the line and event descriptions below, connected by vertical dots. "
                f"Each milestone dot should be a circle in {accent_hex}. "
                f"{visual_elements_instruction}"
                f"{common_footer}"
            )
        elif layout == "process":
            steps = slide.get("steps", [])
            steps_text = "\n".join(
                [f"  Step {s.get('step', idx+1)}: '{s.get('title', '')}' — {s.get('detail', '')}" for idx, s in enumerate(steps)]
            )
            return (
                f"{bg_anchor}"
                f"Create a professional infographic process flow slide. {style}. "
                f"Display these text elements in the image:\n"
                f"- Section header at top-left: '{slide['headline']}'\n"
                f"- Process flow with numbered steps connected by arrows:\n{steps_text}\n"
                f"- Footer at bottom: '{slide.get('bottom_note', '')}'\n\n"
                f"Arrange steps horizontally or in a flow diagram with arrows between them. "
                f"Each step should have a numbered circle in {accent_hex} with the step title in bold below it "
                f"and the detail text in smaller font underneath. "
                f"Connect steps with directional arrows in {accent_hex}. "
                f"{visual_elements_instruction}"
                f"{common_footer}"
            )
        else:  
            bullets_text = "\n".join([f"  • '{b}'" for b in slide.get("bullets", [])])
            has_chart = bool(chart_instruction)
            bullet_position = "in the left half of the slide" if has_chart else "in the center area"
            return (
                f"{bg_anchor}"
                f"Create a professional infographic content slide. {style}. "
                f"Display these text elements in the image:\n"
                f"- Section header at top-left: '{slide['headline']}'\n"
                f"- Data-rich bullet points {bullet_position}:\n{bullets_text}\n"
                f"- Footer at bottom: '{slide.get('bottom_note', '')}'\n\n"
                f"Bullets should use small {accent_hex} colored icons or numbered indicators. "
                f"Focus on clean information hierarchy and data-rich visual presentation. "
                f"{chart_instruction}"
                f"{visual_elements_instruction}"
                f"{common_footer}"
            )

build_glm_prompt() işlevi, tek bir slayt JSON nesnesini tek bir GLM-Image istem dizesine dönüştürür. Bunu üç katmanda yapar:

  • Tema ve arka plan kontrolü: İşlev, temanın style ve renk paletini çeker, ardından katı bir arka plan çıpası uygular. Bu, slaydı tek bir düz arka plan rengine kilitler ve gradyan veya varyasyonu engeller.
  • Yerleşim kuralları: Paylaşılan bir talimat bloğu, metin hiyerarşisini, tutarlı metin renklerini ve işleme kalitesini uygular. Ayrıca her görüntünün bir destenin sayfası gibi hissettirmesi için küçük bir slayt sayacı N / total ekler.
  • Yerleşime özgü yapı: İşlev, slayt türüne göre istemi uyarlar:
    • Başlık: Başlık, konuyu anında kuran baskın görsel unsur olacak şekilde tasarlanır; etiket ve alt başlık bağlam ve bir ana metrik sağlar.
    • Özet: Bu slayt, en önemli içgörüleri özlü ve etkili maddelerde toplar.
    • İçerik slaytları: Bu slaytlar çeşitli yerleşimleri destekler. stat düzeni destekleyici maddelerle tek büyük ana sayıyı öne çıkarır, comparison temiz iki sütunlu bir yapı kullanır, timeline ve process kronolojik olayları görselleştirir, list ise isteğe bağlı bir grafikle eşleştirilebilen veri zengini madde işaretleri sunar.

chart_data varsa, _build_chart_instruction() işlevi bunu açık çizim talimatlarına çevirir. Bu, modele tam olarak neyin, nereye çizileceği söylendiği için güvenilirliği artırır.

Adım 7: GLM-Image kullanarak görseller üretin 

Şimdi, son adımda yukarıdaki istemleri gerçekten infografik görsellere dönüştürüyoruz.

def generate_glm_image(prompt: str, api_key: str, size: str = "1728x960") -> Optional[Image.Image]:
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    payload = {
        "model": GLM_IMAGE_MODEL,
        "prompt": prompt,
        "size": size,
    }
    try:
        resp = requests.post(GLM_IMAGE_API_URL, headers=headers, json=payload, timeout=180)
        resp.raise_for_status()
        data = resp.json()
        url = data.get("data", [{}])[0].get("url")
        if not url:
            st.error(f"GLM-Image returned no URL. Response: {json.dumps(data)[:300]}")
            return None
        img_resp = requests.get(url, timeout=60)
        img_resp.raise_for_status()
        return Image.open(io.BytesIO(img_resp.content)).convert("RGB")
    except requests.exceptions.Timeout:
        st.error("GLM-Image request timed out (180s). The model may be under heavy load.")
        return None
    except requests.exceptions.HTTPError as e:
        st.error(f"GLM-Image HTTP error: {e} — {e.response.text[:300] if e.response else ''}")
        return None
    except Exception as e:
        st.error(f"GLM-Image error: {e}")
        return None

Yukarıdaki işlev, GLM-Image için tam görsel üretim iş akışını şu şekilde ele alır:

  • generate_glm_image() işlevi, her slayt için giriş noktasıdır. Önce glm-image model tanımlayıcısı, slayt istemi (yerleşim, metin, tema ve grafik talimatlarını zaten içerir) ve 1728x960 gibi hedef çıktı çözünürlüğü gibi gerekli alanlarla istek yükünü oluşturur. 
  • İşlev daha sonra bu yükü, görüntü üretimi tipik olarak metin yanıtlarından daha uzun sürdüğü için uzatılmış zaman aşımıyla requests.post() kullanarak Z.ai görüntü üretim uç noktasına gönderir.
  • İstek başarılı olduğunda, işlev API yanıtından barındırılan görsel URL’sini çıkarır ve görüntü baytlarını requests.get() ile indirir, yanıtı doğrular ve içeriği RGB bir PIL.Image nesnesine dönüştürür. 

Kalite–gecikme değişimini keşfetmek ve çıktı boyutunun üretim süresi ve görsel netlik üzerindeki etkisini anlamak için farklı çözünürlük değerleriyle deneyebilirsiniz.

Adım 8: Streamlit arayüzünü kurun

Son olarak, tüm işlevleri Streamlit iş akışına entegre ederek slaytları sıralı şekilde üretelim, ilerlemeyi izleyelim ve son desteyi indirme için paketleyelim.

def render_theme_preview(name: str, theme: Dict):
    c = theme["colors"]
    return (
        f'<div style="display:inline-flex;align-items:center;gap:6px;">'
        f'<span style="display:inline-block;width:14px;height:14px;border-radius:3px;background:{c["bg"]};border:1px solid #555;"></span>'
        f'<span style="display:inline-block;width:14px;height:14px;border-radius:3px;background:{c["accent"]};"></span>'
        f'<span style="display:inline-block;width:14px;height:14px;border-radius:3px;background:{c["text"]};border:1px solid #555;"></span>'
        f'<span style="">{name}</span>'
        f'</div>'
    )
def main():
    st.set_page_config(
        page_title="GLM-Image Infographic Generator",
        page_icon="📊",
        layout="wide",
        initial_sidebar_state="expanded",
    )
    st.markdown("""
    <style>
    @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap');
    @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap');
    .stApp {}
    .hero-banner {
        background: linear-gradient(135deg, #0F172A 0%, #1E293B 50%, #0F172A 100%);
        
        border-radius: 16px;
        
        border: 1px solid #334155;
        position: relative;
        overflow: hidden;
    }
    .hero-banner::before {
        content: '';
        position: absolute;
        top: -50%;
        right: -20%;
        width: 400px;
        height: 400px;
        background: radial-gradient(circle, rgba(56,189,248,0.08) 0%, transparent 70%);
        border-radius: 50%;
    }
    .hero-banner h1 {
        
        
        font-weight: 800;
        
        letter-spacing: -0.5px;
    }
    .hero-banner .subtitle {
        
        font-size: 0.95rem;
        
    }
    .pipeline-tag {
        display: inline-block;
        
        background: rgba(56,189,248,0.1);
        
        
        border-radius: 20px;
        
        font-weight: 600;
        
        letter-spacing: 0.3px;
        border: 1px solid rgba(56,189,248,0.2);
    }
    .phase-header {
        
        
        letter-spacing: 1px;
        
        text-transform: uppercase;
        
    }
    .cost-card {
        background: #1E293B;
        border: 1px solid #334155;
        border-radius: 10px;
        
        
    }
    .cost-card .total {
        
        font-weight: 800;
        
    }
    .slide-nav-btn {
        background: #1E293B;
        border: 1px solid #334155;
        
        
        border-radius: 8px;
        cursor: pointer;
        font-weight: 600;
    }
    #MainMenu {visibility: hidden;}
    footer {visibility: hidden;}
    </style>
    """, unsafe_allow_html=True)
    st.markdown("""
    <div class="hero-banner">
        <h1>GLM-Image Infographic Generator</h1>
        <p class="subtitle">Enter any topic → AI researches the web → GLM-Image generates a full infographic slide deck</p>
        <div class="pipeline-tag">GEMINI 2.5 FLASH and GLM-IMAGE </div>
    </div>
    """, unsafe_allow_html=True)
    with st.sidebar:
        openrouter_key = os.environ.get("OPENROUTER_API_KEY", "")
        zai_key = os.environ.get("ZAI_API_KEY", "")
        st.markdown("#### Visual Theme")
        theme_name = st.selectbox(
            "Select Theme",
            list(THEMES.keys()),
            index=0,
        )
        theme = THEMES[theme_name]
        st.caption(theme["desc"])
        c = theme["colors"]
        st.markdown(
            f'<div style="display:flex;gap:8px;">'
            f'<div style="flex:1;height:32px;border-radius:6px;background:{c["bg"]};border:1px solid #555;" title="Background"></div>'
            f'<div style="flex:1;height:32px;border-radius:6px;background:{c["accent"]};" title="Accent"></div>'
            f'<div style="flex:1;height:32px;border-radius:6px;background:{c["text"]};border:1px solid #555;" title="Text"></div>'
            f'</div>',
            unsafe_allow_html=True,
        )
        st.markdown("#### Slide Settings")
        resolution = st.selectbox("Resolution", list(RESOLUTIONS.keys()), index=0)
        num_slides = st.slider("Number of Slides", 4, 10, 5)
        st.divider()
    if "selected_example" in st.session_state:
        default_topic = st.session_state.pop("selected_example")
    else:
        default_topic = ""
    topic = st.text_input(
        "Enter a topic to research and visualize",
        value=default_topic,
        placeholder="e.g. AI Industry Market Trends January 2026",
    )
    st.markdown("**Quick examples:**")
    cols = st.columns(3)
    for i, ex in enumerate(EXAMPLE_TOPICS):
        with cols[i % 3]:
            if st.button(ex, key=f"ex_{i}", use_container_width=True):
                st.session_state["selected_example"] = ex
                st.rerun()
    st.markdown("---")
    generate = st.button(
        "Generate Infographic Deck",
        type="primary",
        use_container_width=True,
        disabled=(not topic or not openrouter_key or not zai_key),
    )
    if generate:
        if not openrouter_key:
            st.error("Please enter your **OpenRouter API Key** in the sidebar.")
            st.stop()
        if not zai_key:
            st.error("Please enter your **Z.ai API Key** in the sidebar.")
            st.stop()
        size = RESOLUTIONS[resolution]
        st.markdown('<div class="phase-header">Phase 1 — Research & Structure</div>', unsafe_allow_html=True)
        with st.status("Researching topic with Gemini 2.5 Flash + Web Search...", expanded=True) as status:
            st.markdown(f"**Topic:** {topic}")
            st.markdown(f"**Model:** {OPENROUTER_MODEL}")
            t0 = time.time()
            try:
                research = research_topic(topic, openrouter_key)
                t_research = time.time() - t0
                st.success(f"Research complete in {t_research:.1f}s")
                status.update(label="Research complete", state="complete")
            except Exception as e:
                st.error(f"Research failed: {e}")
                status.update(label="Research failed", state="error")
                st.stop()
        with st.status("Structuring slide content...", expanded=False) as status:
            try:
                slides_data = structure_slides(topic, research, num_slides, openrouter_key)
                st.success(f"Structured {len(slides_data)} slides")
                status.update(label=f"{len(slides_data)} slides structured", state="complete")
            except Exception as e:
                st.error(f"Structuring failed: {e}")
                status.update(label="Structuring failed", state="error")
                st.stop()
        with st.expander("Research Summary", expanded=False):
            st.markdown(research)
        with st.expander("Slide JSON", expanded=False):
            st.json(slides_data)
        st.markdown("---")
        st.markdown('<div class="phase-header">Phase 2 — GLM-Image Generation</div>', unsafe_allow_html=True)
        progress = st.progress(0, text="Starting image generation...")
        images: List[Optional[Image.Image]] = []
        prompts: List[str] = []
        timings: List[float] = []
        for i, slide in enumerate(slides_data):
            headline = slide.get("headline", f"Slide {i+1}")
            progress.progress(
                i / len(slides_data),
                text=f"Generating slide {i+1}/{len(slides_data)}: {headline}",
            )
            prompt = build_glm_prompt(slide, theme, len(slides_data), topic)
            prompts.append(prompt)
            t0 = time.time()
            img = generate_glm_image(prompt, zai_key, size)
            elapsed = time.time() - t0
            timings.append(elapsed)
            images.append(img)
            if img:
                st.toast(f"Slide {i+1} generated ({elapsed:.1f}s)")
            else:
                st.toast(f"Slide {i+1} failed", icon="")
        progress.progress(1.0, text="All slides generated!")
        time.sleep(0.3)
        progress.empty()
        st.session_state["images"] = images
        st.session_state["slides_data"] = slides_data
        st.session_state["prompts"] = prompts
        st.session_state["timings"] = timings
        st.session_state["research"] = research
        st.session_state["gen_topic"] = topic
        st.session_state["gen_theme"] = theme_name
    if "images" in st.session_state and st.session_state["images"]:
        images = st.session_state["images"]
        slides_data = st.session_state["slides_data"]
        prompts = st.session_state.get("prompts", [])
        timings = st.session_state.get("timings", [])
        st.markdown("---")
        gen_topic = st.session_state.get("gen_topic", "")
        gen_theme = st.session_state.get("gen_theme", "")
        success_count = sum(1 for img in images if img is not None)
        st.markdown(
            f"### Infographic Deck: *{gen_topic}*\n"
            f"{success_count}/{len(images)} slides · {gen_theme} theme · "
            f"{list(RESOLUTIONS.keys())[0]} resolution"
        )
        slide_idx = st.select_slider(
            "Navigate Slides",
            options=list(range(1, len(images) + 1)),
            value=1,
            format_func=lambda x: f"Slide {x}: {slides_data[x-1].get('headline', '') if x-1 < len(slides_data) else ''}",
        )
        idx = slide_idx - 1
        col_img, col_meta = st.columns([3, 1])
        with col_img:
            if images[idx] is not None:
                st.image(images[idx], use_container_width=True)
            else:
                st.warning(
                    f"Slide {slide_idx} failed to generate. "
                    "Check your Z.ai API key and quota."
                )
        with col_meta:
            slide = slides_data[idx] if idx < len(slides_data) else {}
            st.markdown(f"**Slide {slide_idx} / {len(images)}**")
            slide_type = slide.get('type', 'N/A')
            layout = slide.get('layout', '')
            type_label = f"{slide_type}" + (f" · {layout}" if layout else "")
            st.markdown(f"**Type:** {type_label}")
            if slide.get("key_stat"):
                tc = THEMES.get(gen_theme, THEMES["Corporate Navy"])["colors"]
                st.markdown(
                    f'<div style="background:{tc["bg"]};border:2px solid {tc["accent"]}; '
                    f'border-radius:12px;text-align:center;">'
                    f'<div style="font-weight:800;color:{tc["accent"]};">'
                    f'{slide["key_stat"]}</div>'
                    f'<div style="color:{tc["text"]};opacity:0.7;">'
                    f'{slide.get("key_stat_label","")}</div></div>',
                    unsafe_allow_html=True,
                )
            if slide.get("bullets"):
                st.markdown("**Data Points:**")
                for b in slide["bullets"]:
                    st.markdown(f"• {b}")
            if slide.get("items"):
                st.markdown("**Comparison:**")
                for item in slide["items"]:
                    st.markdown(f"**{item.get('label', '')}**")
                    for p in item.get("points", []):
                        st.markdown(f"  • {p}")
            if slide.get("milestones"):
                st.markdown("**Timeline:**")
                for m in slide["milestones"]:
                    st.markdown(f"• **{m.get('date', '')}** — {m.get('event', '')}")
            if slide.get("steps"):
                st.markdown("**Process:**")
                for s in slide["steps"]:
                    st.markdown(f"{s.get('step', '')}. **{s.get('title', '')}** — {s.get('detail', '')}")
            if slide.get("takeaways"):
                st.markdown("**Takeaways:**")
                for i, t in enumerate(slide["takeaways"], 1):
                    st.markdown(f"{i}. {t}")
            if idx < len(timings):
                st.caption(f"Generated in {timings[idx]:.1f}s")
        if idx < len(prompts):
            with st.expander(f"GLM-Image Prompt — Slide {slide_idx}"):
                st.code(prompts[idx], language="text")
        st.markdown("---")
        st.markdown("**All Slides:**")
        thumb_cols = st.columns(min(len(images), 8))
        for i, img in enumerate(images):
            with thumb_cols[i % len(thumb_cols)]:
                if img is not None:
                    st.image(img, caption=f"Slide {i+1}", use_container_width=True)
                else:
                    st.caption(f"Slide {i+1}")
        st.markdown("---")
        dl_col1, dl_col2, dl_col3, _ = st.columns([1, 1, 1, 1])
        with dl_col1:
            zip_buf = io.BytesIO()
            with zipfile.ZipFile(zip_buf, "w", zipfile.ZIP_DEFLATED) as zf:
                for i, img in enumerate(images):
                    if img is not None:
                        buf = io.BytesIO()
                        img.save(buf, format="PNG", quality=95)
                        zf.writestr(f"slide_{i+1:02d}.png", buf.getvalue())
                zf.writestr("slide_data.json", json.dumps(slides_data, indent=2, ensure_ascii=False))
                zf.writestr("prompts.json", json.dumps(prompts, indent=2, ensure_ascii=False))
            safe_name = re.sub(r"[^a-zA-Z0-9]+", "_", gen_topic)[:40]
            st.download_button(
                "Download ZIP (all slides)",
                data=zip_buf.getvalue(),
                file_name=f"infographic_{safe_name}.zip",
                mime="application/zip",
                use_container_width=True,
            )
        with dl_col2:
            st.download_button(
                "Download Slide JSON",
                data=json.dumps(slides_data, indent=2, ensure_ascii=False),
                file_name=f"slides_{safe_name}.json",
                mime="application/json",
                use_container_width=True,
            )
    elif "images" not in st.session_state:
        st.markdown(
            '<div style="text-align:center;">'
            '<div style="opacity:0.3;">📊</div>'
            '<p style="max-width:500px;">'
            "Enter any topic above, and the system will search the web for the latest data, "
            "structure it into slides, and generate a complete infographic deck using GLM-Image."
            "</p></div>",
            unsafe_allow_html=True,
        )
if __name__ == "__main__":
    main()

Yukarıdaki kod, hattın tüm aşamalarını tek bir iş akışında birleştiren eksiksiz Streamlit uygulamasını tanımlar.

  • main() işlevi, sayfa düzenini yapılandıran, özel stilleri uygulayan ve banner’ı gösteren giriş noktasıdır. Kenar çubuğu daha sonra yapılandırma paneli olarak görev yapar; kullanıcıların görsel tema seçmesine, renklerini ön izlemesine, çıktı çözünürlüğünü seçmesine ve slayt sayısını kontrol etmesine olanak tanır. Bu ayarlar, istem oluşturmayı ve görsel üretimini doğrudan etkiler.
  • Ana arayüz konu girişini toplar ve hızlı denemeler için hızlı örnek düğmeleri sunar. Generate Infographic Deck düğmesi, yürütmeyi başlatmadan önce konunun ve gerekli API anahtarlarının mevcut olduğunu doğrular.
  • Tetiklendikten sonra iş akışı iki aşamada çalışır. İlk olarak sistem, web araştırması yapar ve sonuçları, araştırma özetini ve yapılandırılmış çıktıyı incelemek için ilerleme göstergeleri ve genişletilebilir bölümlerle dil modeli kullanarak yapılandırılmış slayt JSON’una dönüştürür. Sonrasında uygulama, GLM-Image kullanarak görselleri sıralı şekilde üretir; bir ilerleme çubuğu, slayt başına süre ve tost bildirimleri gösterir ve görseller, istemler ve üst verileri st.session_state’te saklayarak sonuçların yeniden çalıştırmalar arasında kalıcı olmasını sağlar.
  • Üretimden sonra arayüz, kullanıcıların slaytlar arasında gezinebileceği, yapılandırılmış içerik ayrıntılarını görebileceği, tam üretim istemini inceleyebileceği ve küçük resimleri ön izleyebileceği bir deste görüntüleyiciye geçer. 
  • Son olarak, uygulama slayt görselleri, slayt JSON’u ve istemler dahil tüm çıktıları indirilebilir bir ZIP’e paketler; ayrıca ayrı JSON dışa aktarma seçenekleri sunar.

Kendiniz denemek için, kodu app.py olarak kaydedin ve şunla başlatın:

Streamlit run app.py

image5.png

Sonuç

Bu eğitimde, kullanıcının bir konu girdiği, Gemini 2.5 Flash’ın web destekli araştırma yapıp içeriği bir slayt planına yapılandırdığı ve GLM-Image’ın her slaytı yüksek çözünürlüklü bir infografik görseli olarak işlediği gerçek zamanlı bir infografik deste oluşturucu geliştirdik. Bunu çalışır kılmanın anahtarı, yapılandırılmamış bilgiyi kısıtlı bir işleme (render) spesifikasyonuna dönüştüren bir hat kurmaktır. 

Yine de geliştirilecek alan var. GLM-Image modeli, görseller içinde uzun ve yoğun metin üretirken çoğu görüntü üretim modelinde görülen yaygın bir hata modu olarak zaman zaman yazım hataları yapabiliyor.

Buradan, atıf yönetimi, tekrar kontrolü ve slayt başına yeniden üretim ekleyerek bu demoyu üretim düzeyinde bir sisteme dönüştürebilirsiniz. Ancak mevcut haliyle bile, uygulama görüntü üretim modellerinin veri hikâye anlatımı için görsel işleme motorları olarak nasıl kullanılabileceğine güçlü bir örnektir.

Ayrıca Google’ın yeni görüntü üretim modeli olan Nano Banana 2 rehberimize de mutlaka göz atın!

GLM Image SSS

Bu GLM Image projesini çalıştırmak için bir GPU’ya ihtiyacım var mı?

Hayır. Uygulama tamamen API’ler (metin için OpenRouter ve görseller için GLM-Image) üzerinden çalışır; bu nedenle yerel GPU gerekmez. Stabil internet bağlantısına sahip standart bir makine yeterlidir.

GLM Image kullanarak bir deste üretmenin maliyeti ne kadar?

Maliyetler, slayt sayısına ve API kullanımına bağlıdır. Tipik çalıştırmalarda (4–8 slayt) OpenRouter ve Z.ai kredilerinde birkaç dolar genellikle yeterlidir. Her bir hesapta yaklaşık 5$ birden çok deneme için yeterlidir.

Gemini 2.5 Flash yerine farklı bir metin modeli kullanabilir miyim?

Proje OpenRouter kullandığı için, OpenAI, Claude veya diğer Gemini varyantları gibi desteklenen diğer modellere geçebilirsiniz. Tercih ettiğiniz sağlayıcı ve maliyet/performans ihtiyaçlarınıza göre model adını güncellemeniz yeterlidir.

Slaytların görsel stilini özelleştirebilir miyim?

Evet. Uygulama tema seçimi (arka plan, vurgu ve metin renkleri), çözünürlük ayarları ve slayt sayısı kontrollerini içerir. Ayrıca tema sözlüğünü genişletebilir veya özel tasarım stilleri oluşturmak için istem şablonlarını değiştirebilirsiniz.

OpenRouter yerine Gemini’yi doğrudan kullanabilir miyim?

Evet. Burada kolaylık için OpenRouter kullanılıyor; ancak Gemini 2.5 Flash’ı Google AI Studio veya Vertex AI ile doğrudan çağırabilirsiniz. OpenRouter uç noktasını resmi Gemini API’siyle değiştirip aynı mesaj yapısını korumanız yeterlidir.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

ML (Üretken Yapay Zekâ) alanında Google Developers Uzmanıyım, Kaggle 3x Expert unvanına sahibim ve 3+ yıllık teknoloji deneyimiyle Women Techmakers Elçisiyim. 2020'de bir sağlık teknolojileri girişiminin kurucu ortağı oldum ve Georgia Tech'te makine öğrenmesi alanında uzmanlaşarak bilgisayar bilimleri yüksek lisansı yapıyorum.

Konular
Yapay Zeka
Büyük Dil Modelleri

En İyi DataCamp Kursları

Kurs

LangChain ile Aracı Sistemler Tasarlama

3 sa
14.8K
LangChain ajanlarının temel bileşenlerini öğrenin ve özel sohbet ajanları oluşturun.
Ayrıntıları GörüntüleRight Arrow
Kursa Başla
Devamını GörRight Arrow
İlgili

blog

Hızlı Sevkiyat İçin Pratik Vibe Kodlama Teknoloji Yığını

Ön uç, arka uç, veritabanları, kimlik doğrulama, depolama, e-posta, test, dağıtım ve izleme için en iyi araçları keşfedin.
Abid Ali Awan's photo

Abid Ali Awan

14 dk.

blog

2026’da En Popüler 40 Yazılım Mühendisi Mülakat Sorusu

Algoritmalar, sistem tasarımı ve davranışsal senaryoları kapsayan bu temel sorularla teknik mülakat sürecine hakim olun. Uzman cevapları, kod örnekleri ve kanıtlanmış hazırlık stratejileri edinin.
Dario Radečić's photo

Dario Radečić

15 dk.

Eğitim

.gitignore Nasıl Kullanılır: Örneklerle Pratik Bir Giriş

Git deponuzu temiz tutmak için .gitignore’u nasıl kullanacağınızı öğrenin. Bu eğitim; temelleri, yaygın kullanım durumlarını ve başlamanıza yardımcı olacak pratik örnekleri kapsar!
Kurtis Pykes 's photo

Kurtis Pykes

8 dk.

Eğitim

Python'da Listeyi String'e Nasıl Dönüştürürsünüz

Bu hızlı eğitimde, Python'da bir listeyi string'e nasıl dönüştüreceğinizi öğrenin.
Adel Nehme's photo

Adel Nehme

Devamını GörDevamını Gör