कोर्स
इन्फोग्राफिक्स और स्लाइड डेक सबसे कठिन इमेज जनरेशन कार्यों में आते हैं, क्योंकि आप केवल विज़ुअल्स नहीं बना रहे होते—आपको हेडलाइन, बुलेट पॉइंट्स, चार्ट्स और फुटर जैसी संरचित जानकारी भी बनानी होती है, और साथ ही टेक्स्ट व लेआउट में स्थिरता बनाए रखनी होती है। GLM-Image Z.ai का इमेज जनरेशन मॉडल है जो स्पष्ट डिज़ाइन बाधाओं के साथ प्रॉम्प्ट करने पर हाई-रेज़ोल्यूशन, टेक्स्ट-गहन, लेआउट-संवेदी इमेजेज़ बना सकता है।
इस ट्यूटोरियल में, मैं आपको एक सरल Streamlit ऐप के जरिए प्रॉम्प्ट-टू-इन्फोग्राफिक डेक जेनरेटर बनाने की प्रक्रिया बताऊंगा। आप लगभग किसी भी विषय को दर्ज कर सकते हैं। Gemini 2.5 Flash वेब-आधारित रिसर्च करके ताज़ा आँकड़े और रुझान लाता है, LLM उस रिसर्च को एक सख्त स्लाइड JSON योजना में बदलता है, और GLM-Image हर स्लाइड को हाई-रेज़ोल्यूशन इन्फोग्राफिक के रूप में रेंडर करता है।
अंत तक, आपके पास एक संपूर्ण पाइपलाइन होगी जो एक प्रॉम्प्ट को डाउनलोड करने योग्य डेक में बदल देती है—वह भी बिना किसी GPU के, क्योंकि पूरा भारी काम API के माध्यम से होता है। वर्कफ़्लो दिखाती स्पीड-अप वीडियो देखें:
GLM Image क्या है?
GLM-Image Z.ai का टेक्स्ट-टू-इमेज मॉडल है (जो glm-image मॉडल ID के जरिए सर्व होता है), जिसे विशेष रूप से पोस्टर्स, PPT-शैली के विज़ुअल्स और साइंस डायग्राम जैसी ज्ञान-सघन, लेआउट-संवेदी जनरेशन को संभालने के लिए बनाया गया है।

चित्र: GLM-Image: घनी-ज्ञान और उच्च-निष्ठा इमेज जनरेशन के लिए ऑटो-रेग्रेसिव
अंदरूनी तौर पर, यह एक हाइब्रिड ऑटोरेग्रेसिव और डिफ्यूज़न डिकोडर डिज़ाइन का उपयोग करता है, जहाँ ऑटोरेग्रेसिव बैकबोन निर्देशों को समझने और ग्लोबल लेआउट पर फोकस करता है, जबकि डिफ्यूज़न डिकोडर हाई-फ्रीक्वेंसी डिटेल्स (तेज़ टेक्स्ट स्ट्रोक्स सहित) को परिष्कृत करता है। यही हाइब्रिड सेटअप एक बड़ा कारण है कि GLM-Image टेक्स्ट-भारी विज़ुअल्स के लिए कई शुद्ध डिफ्यूज़न जेनरेटरों की तुलना में अधिक भरोसेमंद रहता है।
इस ट्यूटोरियल में, हम GLM-Image को प्रत्येक स्लाइड के लिए रेंडरिंग इंजन के रूप में उपयोग करते हैं। इसे अस्पष्ट तरीके से प्रॉम्प्ट करने के बजाय, हम इसे एक सख्त, लेआउट-संवेदी प्रॉम्प्ट देते हैं जो इन बातों को फिक्स करता है:
- एक सिंगल सॉलिड बैकग्राउंड रंग (कोई ग्रेडिएंट नहीं),
- पैलेट प्रतिबंध (बैकग्राउंड / एक्सेंट / टेक्स्ट),
- स्पष्ट टाइपोग्राफी पदानुक्रम (हेडलाइन बनाम बॉडी बनाम फुटनोट),
- हर स्लाइड प्रकार के लिए स्पष्ट लेआउट नियम (stat, comparison, timeline, process, list),
- वैकल्पिक चार्ट ड्रॉइंग निर्देश (bar / pie / line),
- फुटर नोट्स और स्लाइड नंबरिंग।
नोट: GLM-Image एक इमेज URL लौटाता है, 16:9 जैसे सामान्य प्रस्तुति आस्पेक्ट रेशियो को सपोर्ट करता है, और 1728×960 जैसे सुझाए गए आकारों पर सबसे अच्छा काम करता है।
GLM Image ट्यूटोरियल: रियल-टाइम इन्फोग्राफिक जेनरेटर बनाएं
इस सेक्शन में, हम Gemini 2.5 Flash और GLM-Image द्वारा संचालित, Streamlit एप्लिकेशन के भीतर लिपटी एक प्रॉम्प्ट-टू-इन्फोग्राफिक स्लाइड डेक पाइपलाइन बनाएंगे। ऊँचे स्तर पर, ऐप तीन मुख्य कार्य करता है:
- वेब के ताज़ा डाटा से किसी विषय पर रिसर्च करना, ताकि डेक मौजूदा संख्याएँ और रुझान दिखाए, न कि पुराने मॉडल मेमोरी पर निर्भर रहे
- रिसर्च को एक संरचित स्लाइड योजना में बदलना जिसमें एक सख्त JSON स्कीमा शामिल हो जो stat, timeline, comparison, process, और summary जैसे स्लाइड लेआउट को परिभाषित करे
- हर स्लाइड को GLM-Image का उपयोग करके एक पूर्ण इन्फोग्राफिक इमेज के रूप में रेंडर करना, जिसमें टाइपोग्राफी पदानुक्रम, थीम रंग, लेआउट बाधाएँ, वैकल्पिक चार्ट और स्लाइड नंबरिंग शामिल हैं।
पर्दे के पीछे, वर्कफ़्लो दो प्रमुख चरणों से संचालित होता है:
- फेज 1:
research_topic(topic)फ़ंक्शन Gemini 2.5 Flash को वेब सर्च सक्षम करके उपयोग करता है ताकि बिल्कुल उसी विषय के अनुरूप डाटा-समृद्ध रिसर्च सारांश तैयार किया जा सके। - फेज 2:
structure_slides()फ़ंक्शन उस रिसर्च को JSON डेक में बदल देता है, जिसे फिर विस्तृत GLM-Image प्रॉम्प्ट में ट्रांसफ़ॉर्म करके स्लाइड-दर-स्लाइड रेंडर किया जाता है।
स्टेप 1: API कुंजियाँ प्राप्त करें
इन्फोग्राफिक जेनरेटर बनाने से पहले, हमें दो बाहरी सेवाओं की एक्सेस चाहिए जो कोर पाइपलाइन को पावर देती हैं:
- OpenRouter Gemini 2.5 Flash तक पहुँच देता है, जिसे हम वेब-आधारित रिसर्च और स्लाइड स्ट्रक्चरिंग के लिए उपयोग करेंगे।
- Z.ai API और HuggingFace के माध्यम से GLM-Image मॉडल होस्ट करता है, जो अंतिम इन्फोग्राफिक स्लाइड्स जनरेट करता है।
यह अलगाव जानबूझकर है। टेक्स्ट मॉडल डाटा इकट्ठा करने और योजना बनाने का काम करता है, जबकि GLM-Image पूरी तरह विज़ुअल जनरेशन पर केंद्रित रहता है।
स्टेप 1.1: OpenRouter API कुंजी
OpenRouter एक एकीकृत गेटवे की तरह काम करता है जो आपको एक ही कुंजी से कई मॉडलों (Gemini, OpenAI, Claude, और अन्य) तक पहुँच देता है।
- OpenRouter पर एक खाता बनाएँ और API कुंजी जनरेट करें।
- थोड़ा क्रेडिट जोड़ें (लगभग $5)। यह इस डेमो के कई रन और भविष्य के प्रयोगों के लिए पर्याप्त है।
इस ट्यूटोरियल में, हम Gemini 2.5 Flash का उपयोग करते हैं क्योंकि यह वेब सर्च सपोर्ट करता है, जिससे ऐप केवल मॉडल के प्रशिक्षण ज्ञान पर निर्भर रहने के बजाय अप-टू-डेट डाटा और आँकड़े प्राप्त कर सकता है। यदि आप चाहें, तो किसी अन्य समर्थित मॉडल को भी बदलकर उपयोग कर सकते हैं।
स्टेप 1.2: Z.ai API कुंजी
इसके बाद, Z.ai पर एक खाता बनाएँ ताकि GLM Image मॉडल तक पहुँच मिल सके।

- API Management पर जाएँ और Billing चुनें
- Add Payment Method पर क्लिक करें और अपने कार्ड विवरण दर्ज करें
- इमेज जनरेशन एंडपॉइंट के लिए API कुंजी जनरेट करें
यह कुंजी प्रत्येक स्लाइड को रेंडर करने के लिए glm-image मॉडल को कॉल करने में उपयोग होगी।
स्टेप 1.3: कुंजियों को environment variables के रूप में स्टोर करें
एक बार दोनों कुंजियाँ बन जाने पर, उन्हें निम्न कमांड्स का उपयोग करके environment variables के रूप में स्टोर करें:
export OPENROUTER_API_KEY="Your_OpenRouter_API_Key"
export ZAI_API_KEY="Your_ZAI_API_Key"
ये environment variables आपकी क्रेडेंशियल्स को कोडबेस से बाहर रखते हैं और Streamlit Cloud, Docker या अन्य परिवेशों में परिनियोजन को बहुत आसान बनाते हैं।
स्टेप 2: निर्भरताएँ इंस्टॉल करें
अब, हम आवश्यक निर्भरताएँ इंस्टॉल करेंगे और Streamlit एप्लिकेशन बनाना शुरू करेंगे।
pip install streamlit requests Pillow
पैकेज इंस्टॉल हो जाने के बाद, हम ऐप में उपयोग होने वाली मानक लाइब्रेरीज़ इम्पोर्ट करेंगे, साथ ही कुछ कॉन्स्टेंट्स जो दोनों API एंडपॉइंट्स को परिभाषित करते हैं।
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"
ये लाइब्रेरीज़ एप्लिकेशन की रनटाइम परत बनाती हैं। Streamlit इंटरैक्टिव वेब इंटरफ़ेस को पावर देता है, Requests OpenRouter (Gemini 2.5 Flash) और Z.ai (GLM-Image) के लिए API कॉल्स संभालता है, और Pillow हमें जनरेट की गई इमेजेज़ लोड और डिस्प्ले करने देता है। अतिरिक्त मानक लाइब्रेरीज़ JSON पार्सिंग, environment variables, टाइमिंग, और परिणामों को ZIP फ़ाइल के रूप में एक्सपोर्ट करने का काम संभालती हैं।
हम OpenRouter और GLM-Image एंडपॉइंट्स के लिए कॉन्स्टेंट्स भी परिभाषित करते हैं ताकि ऐप को पता रहे कि रिसर्च, स्ट्रक्चरिंग और इमेज जनरेशन रिक्वेस्ट कहाँ भेजने हैं। यह सेटअप परिवेश को हल्का रखता है, जबकि सारा भारी computation API के ज़रिए होता है।
स्टेप 3: रेज़ोल्यूशन और प्रस्तुति थीम्स परिभाषित करें
इस स्टेप में, हम कुछ रेज़ोल्यूशन प्रीसेट्स और एक थीम लाइब्रेरी परिभाषित करेंगे जो बैकग्राउंड रंग, एक्सेंट रंग, टाइपोग्राफी का मूड, और वह इन्फोग्राफिक शैली-शब्दावली नियंत्रित करती है जिसे हम हर GLM-Image प्रॉम्प्ट में इंजेक्ट करेंगे।
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 डिक्शनरी लेबल को ठीक उसी WxH स्ट्रिंग से मैप करती है जिसकी Z.ai इमेज जनरेशन एंडपॉइंट को अपेक्षा होती है। इसका प्रमुख लाभ पूर्वानुमेयता है—यानी आप फॉर्मैट्स (चौड़ी स्लाइड्स बनाम स्क्वायर सोशल एसेट्स बनाम पोर्ट्रेट) बदले बिना किसी प्रॉम्प्ट लॉजिक को बदले स्विच कर सकते हैं। आस्पेक्ट रेशियो:
- 16:9 क्लासिक स्लाइड डेक और प्रस्तुति-शैली इन्फोग्राफिक्स के लिए श्रेष्ठ है।
- 1:1 LinkedIn/Twitter कैरोसेल जैसे विज़ुअल्स के लिए उपयुक्त है।
- 2:3 मोबाइल-प्रथम इन्फोग्राफिक पोस्टर्स के लिए उपयोगी है।
हम THEMES भी परिभाषित करते हैं जहाँ हर थीम तीन भागों वाला एक कॉम्पैक्ट डिज़ाइन सिस्टम है:
desc: साइडबार में दिखाने हेतु एक छोटा UI विवरणcolors: हमारी सख्त पैलेट (बैकग्राउंड, एक्सेंट, टेक्स्ट)style: प्रॉम्प्ट करने योग्य स्टाइल ब्लॉक जो GLM-Image को एकसमान लेआउट, टाइपोग्राफी और सजावट की ओर धकेलता है
व्यावहारिक रूप से, यह थीम ऑब्जेक्ट हर स्लाइड के लिए एक पुन: प्रयोज्य “प्रॉम्प्ट प्रीफ़िक्स” बन जाता है, ताकि डेक 7 असंबंधित इमेजेज़ की बजाय एक ही प्रोडक्ट जैसा पढ़े।
स्टेप 4: फेज 1 - Gemini 2.5 Flash से रिसर्च
स्लाइड्स जनरेट करने से पहले, हमें विषय के बारे में अप-टू-डेट डाटा चाहिए। किसी मॉडल के स्थिर ज्ञान पर निर्भर रहने के बजाय, यह स्टेप OpenRouter के माध्यम से वेब सर्च सक्षम Gemini 2.5 Flash का उपयोग करता है ताकि ताज़ा आँकड़े, टाइमलाइंस और प्रमुख इनसाइट्स एकत्रित किए जा सकें।
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)
ऊपर दिए गए कोड में दो प्रमुख हेल्पर फ़ंक्शंस परिभाषित हैं जो पाइपलाइन की रिसर्च परत बनाते हैं।
call_openrouter()फ़ंक्शन Gemini 2.5 Flash मॉडल का उपयोग करते हुए OpenRouter को चैट रिक्वेस्ट भेजने का इंटरफ़ेस है। यह मॉडल नाम, संदेश, तापमान और टोकन लिमिट के साथ रिक्वेस्ट पेलोड बनाता है, और API कुंजी के माध्यम से प्रमाणीकरण संभालता है।- जब
use_web_search=Trueहोता है, तो फ़ंक्शन वेब प्लगइन सक्षम करता है और एक सर्च प्रॉम्प्ट इंजेक्ट करता है ताकि मॉडल नवीनतम जानकारी को प्राथमिकता दे। रिक्वेस्ट भेजने के बाद, यह रिस्पॉन्स को वैलिडेट करता है और जेनरेटेड टेक्स्ट निकालता है, सुनिश्चित करते हुए कि पाइपलाइन खाली या असफल आउटपुट के साथ आगे न बढ़े। research_topic()फ़ंक्शन इन्फोग्राफिक जनरेशन के लिए डिज़ाइन किया गया एक टास्क-विशिष्ट रिसर्च प्रॉम्प्ट बनाता है। प्रॉम्प्ट स्पष्ट रूप से सांख्यिकी, ग्रोथ रेट्स, मार्केट साइज, टाइमलाइंस, तुलना और प्रमुख इकाइयों जैसी मात्रात्मक जानकारी मांगता है, ताकि बाद में उन्हें चार्ट्स, टेबल्स या डायग्राम्स के रूप में विज़ुअलाइज़ किया जा सके। फ़ंक्शन फिर आउटपुट को तथ्यात्मक और संरचित रखने के लिए कम तापमान के साथcall_openrouter()को कॉल करता है।
आप तापमान, वेब परिणामों की संख्या, या रिसर्च निर्देशों की कठोरता जैसे पैरामीटर्स के साथ प्रयोग कर सकते हैं ताकि एकत्र किए गए डाटा की व्यापकता, ताज़गी और सटीकता के बीच संतुलन नियंत्रित कर सकें।
अगले स्टेप में, हम इस कच्चे रिसर्च को एक संरचित स्लाइड स्कीमा (JSON) में बदलेंगे ताकि हर स्लाइड का लेआउट, प्रमुख मीट्रिक्स और विज़ुअल तत्व GLM-Image मॉडल को भेजने से पहले परिभाषित हो जाएँ।
स्टेप 5: फेज 2 - रिसर्च को संरचित स्लाइड JSON में बदलना
रिसर्च तैयार हो जाने के बाद, अगला स्टेप इसे एक संरचित ब्लूप्रिंट में बदलना है जिसे GLM-Image लगातार रेंडर कर सके। पैरा पास करने की जगह, हम एक सख्त JSON योजना जनरेट करते हैं जहाँ हर स्लाइड अपना लेआउट, मुख्य टेक्स्ट और वैकल्पिक चार्ट डाटा निर्दिष्ट करती है।
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 ""
फेज 2 में, हम दो मुख्य फ़ंक्शंस परिभाषित करते हैं: एक हमारे स्लाइड्स को स्ट्रक्चर करने के लिए और दूसरा चार्ट्स या डायग्राम निर्देश बनाने के लिए। इन फ़ंक्शंस में शामिल हैं:
structure_slides()फ़ंक्शन: यह फ़ंक्शन Gemini (वेब सर्च के बिना) को स्ट्रक्चरिंग इंजन की तरह उपयोग करता है। यह एक सिस्टम प्रॉम्प्ट बनाता है जो मॉडल को केवल वैध JSON ऐरे लौटाने के लिए बाध्य करता है, जिसमें ठीक उतनी स्लाइड्स हों जितनी माँगी गई हैं। हर डेक एक फिक्स्ड नैरेटिव पैटर्न का पालन करता है: एक टाइटल स्लाइड, कई कंटेंट स्लाइड्स, और अंतिम समरी। कंटेंट स्लाइड्स विभिन्न लेआउट अपना सकती हैं—जैसे stat, comparison, timeline, process, या list—और प्रॉम्प्ट कुछ महत्त्वपूर्ण नियम लागू करता है, जैसे:- हर बुलेट में एक ठोस मीट्रिक (संख्या, प्रतिशत, या डॉलर राशि) शामिल हो
- दोहराव से बचने के लिए लेआउट्स का मिश्रण हो
- विज़ुअल एलिमेंट्स विषय-विशिष्ट हों
- जब कंटेंट स्वाभाविक रूप से चार्ट में फिट बैठता हो, तब वैकल्पिक
chart_dataशामिल हो - इसके बाद फ़ंक्शन लो टेम्परेचर के साथ
call_openrouter()को कॉल करता है ताकि आउटपुट स्थिर रहे, किसी भी अनजाने मार्कडाउन रैपर को हटाता है, और रिस्पॉन्स को डाउनस्ट्रीम उपयोग के लिए Python डिक्शनरी में पार्स करता है। _build_chart_instruction()फ़ंक्शन: यह हेल्पर JSON के वैकल्पिकchart_dataको GLM-Image के लिए ड्रॉइंग निर्देशों में बदल देता है। चार्ट प्रकार (bar, pie, line, या table) के आधार पर, यह थीम के एक्सेंट रंग के अनुसार प्लेसमेंट, लेबलिंग, साइजिंग और रंग उपयोग पर स्पष्ट गाइडेंस जनरेट करता है।
इस स्टेप के अंत तक, हमारे पास एक साफ-सुथरा स्लाइड-स्तरीय स्पेसिफिकेशन होता है जो डेक की नैरेटिव फ्लो और विज़ुअल संरचना दोनों को परिभाषित करता है।
स्टेप 6: स्लाइड JSON को GLM-Image प्रॉम्प्ट्स में बदलें
इस स्टेप में, हम हर स्लाइड ऑब्जेक्ट को एक अत्यंत नियंत्रित प्रॉम्प्ट में अनुवादित करते हैं जो इमेज मॉडल को बताता है कि क्या बनाना है, कहाँ रखना है, और कैसा स्टाइल रखना है।
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() फ़ंक्शन एक स्लाइड JSON ऑब्जेक्ट को एक GLM-Image प्रॉम्प्ट स्ट्रिंग में बदल देता है। यह तीन लेयर्स में काम करता है:
- थीम और बैकग्राउंड नियंत्रण: फ़ंक्शन थीम का
styleऔर कलर पैलेट खींचता है, फिर एक सख्त बैकग्राउंड एंकर लागू करता है। इससे स्लाइड एक ही सॉलिड बैकग्राउंड रंग पर लॉक हो जाती है और ग्रेडिएंट या भिन्नता रोकी जाती है। - लेआउट नियम: एक साझा निर्देश ब्लॉक टेक्स्ट पदानुक्रम, एकसमान टेक्स्ट रंग और रेंडरिंग गुणवत्ता लागू करता है। यह एक छोटा स्लाइड काउंटर
N / totalभी जोड़ता है, ताकि हर इमेज एक स्टैंडअलोन ग्राफिक के बजाय डेक के पेज जैसी लगे। - लेआउट-विशिष्ट संरचना: फ़ंक्शन स्लाइड के प्रकार के आधार पर प्रॉम्प्ट को एडॉप्ट करता है:
- Title: हेडलाइन को प्रमुख विज़ुअल एलिमेंट बनाया जाता है, जिससे विषय तुरंत स्थापित हो, जबकि टैग और सबहेडलाइन संदर्भ और एक प्रमुख मीट्रिक प्रदान करते हैं।
- Summary: यह स्लाइड सबसे महत्त्वपूर्ण इनसाइट्स को संक्षिप्त और प्रभावशाली बिंदुओं में समेटती है।
- कंटेंट स्लाइड्स: ये स्लाइड्स विविध लेआउट्स सपोर्ट करती हैं।
statलेआउट एक बड़े प्रमुख नंबर को सपोर्टिंग बुलेट्स के साथ हाईलाइट करता है,comparisonएक साफ दो-कॉलम स्ट्रक्चर उपयोग करता है,timelineऔरprocessकालानुक्रमिक घटनाओं को विज़ुअलाइज़ करते हैं, जबकिlistडाटा-समृद्ध बुलेट पॉइंट्स प्रस्तुत करता है जिन्हें वैकल्पिक रूप से चार्ट के साथ जोड़ा जा सकता है।
यदि chart_data मौजूद है, तो _build_chart_instruction() फ़ंक्शन इसे स्पष्ट ड्रॉइंग निर्देशों में बदल देता है। इससे विश्वसनीयता बढ़ती है क्योंकि मॉडल को ठीक-ठीक बताया जाता है कि क्या और कहाँ बनाना है।
स्टेप 7: GLM-Image का उपयोग करके इमेजेज़ जनरेट करें
अब अंतिम स्टेप यह है कि ऊपर दिए गए प्रॉम्प्ट्स को वास्तव में इन्फोग्राफिक इमेजेज़ में रेंडर किया जाए।
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
ऊपर दिया गया फ़ंक्शन GLM-Image के लिए संपूर्ण इमेज जनरेशन वर्कफ़्लो को इस तरह संभालता है:
generate_glm_image()फ़ंक्शन हर स्लाइड के लिए एंट्री पॉइंट है। यह सबसे पहले आवश्यक फ़ील्ड्स—जैसेglm-imageमॉडल पहचानकर्ता, स्लाइड प्रॉम्प्ट (जिसमें पहले से लेआउट, टेक्स्ट, थीम और चार्ट निर्देश मौजूद हैं) और आउटपुट रेज़ोल्यूशन (जैसे 1728x960)—के साथ रिक्वेस्ट पेलोड बनाता है।- इसके बाद फ़ंक्शन इस पेलोड को Z.ai इमेज जनरेशन एंडपॉइंट पर
requests.post()के साथ एक विस्तारित टाइमआउट के साथ भेजता है, क्योंकि इमेज जनरेशन आमतौर पर टेक्स्ट रिस्पॉन्स की तुलना में अधिक समय लेती है। - रिक्वेस्ट सफल होने पर, फ़ंक्शन API रिस्पॉन्स से होस्टेड इमेज URL निकालता है,
requests.get()से इमेज बाइट्स डाउनलोड करता है, रिस्पॉन्स को वैलिडेट करता है और कंटेंट को RGBPIL.Imageऑब्जेक्ट में कन्वर्ट करता है।
आप विभिन्न रेज़ोल्यूशन मानों के साथ प्रयोग कर सकते हैं ताकि क्वालिटी–लेटेंसी ट्रेड-ऑफ समझ सकें और यह भी कि आउटपुट साइज जनरेशन समय और विज़ुअल स्पष्टता को कैसे प्रभावित करता है।
स्टेप 8: Streamlit UI बनाएं
अंत में, हम सभी फ़ंक्शंस को Streamlit वर्कफ़्लो में एकीकृत करेंगे ताकि स्लाइड्स क्रमवार जनरेट हों, प्रोग्रेस ट्रैक हो, और अंतिम डेक को डाउनलोड के लिए पैकेज किया जा सके।
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()
उपरोक्त कोड संपूर्ण Streamlit एप्लिकेशन को परिभाषित करता है जो पाइपलाइन के सभी चरणों को एक वर्कफ़्लो में जोड़ता है।
main()फ़ंक्शन एंट्री पॉइंट है जो पेज लेआउट कॉन्फ़िगर करता है, कस्टम स्टाइलिंग लागू करता है और बैनर रेंडर करता है। साइडबार फिर कॉन्फ़िगरेशन पैनल की तरह काम करता है, जिससे उपयोगकर्ता विज़ुअल थीम चुन सकें, उसके रंगों का प्रीव्यू देख सकें, आउटपुट रेज़ोल्यूशन चुन सकें और स्लाइड्स की संख्या नियंत्रित कर सकें। ये सेटिंग्स सीधे प्रॉम्प्ट कंस्ट्रक्शन और इमेज जनरेशन को प्रभावित करती हैं।- मुख्य इंटरफ़ेस विषय इनपुट लेता है और तेज़ प्रयोग के लिए त्वरित उदाहरण बटन प्रदान करता है। Generate Infographic Deck बटन यह मान्य करता है कि विषय और आवश्यक API कुंजियाँ मौजूद हैं, फिर निष्पादन शुरू करता है।
- ट्रिगर होने पर, वर्कफ़्लो दो फेज़ में चलता है। पहले, सिस्टम वेब रिसर्च करता है और भाषा मॉडल का उपयोग करके परिणामों को संरचित स्लाइड JSON में बदलता है—प्रोग्रेस इंडिकेटर्स और एक्सपैंडेबल सेक्शंस के साथ ताकि रिसर्च सारांश और संरचित आउटपुट देखा जा सके। इसके बाद, ऐप GLM-Image का उपयोग करके क्रमवार इमेजेज़ जनरेट करता है, प्रोग्रेस बार, प्रति-स्लाइड टाइमिंग और टोस्ट नोटिफिकेशंस दिखाता है, और परिणामों को
st.session_stateमें स्टोर करता है ताकि रीरन के बीच भी बने रहें। - जनरेशन के बाद, इंटरफ़ेस डेक व्यूअर में बदल जाता है जहाँ उपयोगकर्ता स्लाइड्स नेविगेट कर सकते हैं, संरचित कंटेंट डिटेल्स देख सकते हैं, सटीक जनरेशन प्रॉम्प्ट देख सकते हैं और थंबनेल्स का प्रीव्यू कर सकते हैं।
- अंत में, ऐप सभी आउटपुट—स्लाइड इमेजेज़, स्लाइड JSON और प्रॉम्प्ट्स—को डाउनलोड करने योग्य ZIP में पैकेज करता है, साथ ही अलग JSON एक्सपोर्ट विकल्प देता है।
खुद आज़माने के लिए, कोड को app.py के रूप में सेव करें और लॉन्च करें:
Streamlit run app.py

निष्कर्ष
इस ट्यूटोरियल में, हमने एक रियल-टाइम इन्फोग्राफिक डेक जेनरेटर बनाया जिसमें उपयोगकर्ता विषय दर्ज करता है, Gemini 2.5 Flash वेब-समर्थित रिसर्च करता है और कंटेंट को एक स्लाइड योजना में स्ट्रक्चर करता है, और GLM-Image हर स्लाइड को हाई-रेज़ोल्यूशन इन्फोग्राफिक इमेज के रूप में रेंडर करता है। इसे कामयाब बनाने की कुंजी एक ऐसी पाइपलाइन बनाना है जो असंरचित जानकारी को एक बाध्य रेंडर स्पेसिफिकेशन में बदल दे।
फिर भी सुधार की गुंजाइश है। GLM-Image मॉडल कभी-कभी शब्दों की वर्तनी गलत कर देता है, जो अधिकांश इमेज जनरेशन मॉडलों के लिए एक सामान्य विफलता मोड है जब वे इमेजेज़ के अंदर लंबा, घना टेक्स्ट बनाते हैं।
यहीं से, आप सिटेशन हैंडलिंग, रिपिटिशन चेक्स और प्रति-स्लाइड री-जनरेशन जोड़कर इस डेमो को प्रोडक्शन-ग्रेड सिस्टम में बदल सकते हैं। लेकिन अपने मौजूदा रूप में भी, यह ऐप दिखाता है कि इमेज जनरेशन मॉडल्स को डाटा स्टोरीटेलिंग के लिए विज़ुअल रेंडरिंग इंजन के रूप में कैसे उपयोग किया जा सकता है।
Google के नए इमेज जनरेशन मॉडल Nano Banana 2 पर हमारे गाइड को भी ज़रूर देखें!
GLM Image FAQs
क्या इस GLM Image प्रोजेक्ट को चलाने के लिए GPU चाहिए?
नहीं। यह एप्लिकेशन पूरी तरह APIs (टेक्स्ट के लिए OpenRouter और विज़ुअल्स के लिए GLM-Image) के माध्यम से चलता है, इसलिए लोकल GPU की आवश्यकता नहीं है। एक मानक मशीन और स्थिर इंटरनेट कनेक्शन पर्याप्त है।
GLM Image का उपयोग करके एक डेक जनरेट करने में कितनी लागत आती है?
लागत स्लाइड्स की संख्या और API उपयोग पर निर्भर करती है। सामान्य रन (4–8 स्लाइड्स) के लिए OpenRouter और Z.ai क्रेडिट में कुछ डॉलर पर्याप्त होते हैं। प्रत्येक खाते में लगभग $5 कई प्रयोगों के लिए काफ़ी है।
क्या मैं Gemini 2.5 Flash की जगह कोई अन्य टेक्स्ट मॉडल उपयोग कर सकता/सकती हूँ?
क्योंकि प्रोजेक्ट OpenRouter का उपयोग करता है, आप OpenAI, Claude, या अन्य Gemini वेरिएंट जैसे समर्थित मॉडलों पर स्विच कर सकते हैं। बस अपने पसंदीदा प्रदाता और लागत/प्रदर्शन आवश्यकताओं के अनुसार मॉडल नाम अपडेट करें।
क्या मैं स्लाइड्स की विज़ुअल स्टाइल को कस्टमाइज़ कर सकता/सकती हूँ?
हाँ। ऐप में थीम चयन (बैकग्राउंड, एक्सेंट और टेक्स्ट रंग), रेज़ोल्यूशन सेटिंग्स और स्लाइड काउंट नियंत्रण शामिल हैं। आप थीम डिक्शनरी का विस्तार कर सकते हैं या कस्टम डिज़ाइन स्टाइल बनाने के लिए प्रॉम्प्ट टेम्पलेट्स संशोधित कर सकते हैं।
क्या मैं OpenRouter की बजाय सीधे Gemini उपयोग कर सकता/सकती हूँ?
हाँ। यहाँ सुविधा के लिए OpenRouter का उपयोग किया गया है, लेकिन आप Google AI Studio या Vertex AI के माध्यम से सीधे Gemini 2.5 Flash को कॉल कर सकते हैं। बस OpenRouter एंडपॉइंट को आधिकारिक Gemini API से बदलें और वही मैसेज स्ट्रक्चर रखें।
मैं एमएल (Gen AI) में Google Developers Expert, Kaggle 3x Expert, और Women Techmakers एंबेसडर हूँ, तथा तकनीक क्षेत्र में 3+ वर्षों का अनुभव रखती हूँ। मैंने 2020 में एक हेल्थ-टेक स्टार्टअप की सह-स्थापना की और वर्तमान में Georgia Tech से कंप्यूटर विज्ञान में मास्टर कर रही हूँ, जिसमें मेरा विशेषज्ञता क्षेत्र मशीन लर्निंग है।
