Curso
Infográficos e decks de slides estão entre as tarefas mais desafiadoras de geração de imagens, porque você não está apenas criando visuais — está gerando informação estruturada como títulos, bullets, gráficos e rodapés, mantendo texto e layout consistentes. O GLM-Image é o modelo de geração de imagens da Z.ai capaz de produzir imagens de alta resolução, com muito texto e sensíveis ao layout quando recebe prompts com restrições de design explícitas.
Neste tutorial, vou te mostrar como criar um gerador de deck infográfico a partir de um prompt usando um app simples em Streamlit. Você pode inserir praticamente qualquer tema. O Gemini 2.5 Flash faz uma pesquisa atualizada na web para trazer estatísticas e tendências recentes, o LLM converte essa pesquisa em um plano rígido de slides em JSON, e o GLM-Image renderiza cada slide como um infográfico em alta resolução.
No fim, você terá um pipeline completo que transforma um único prompt em um deck para download — sem precisar de GPU local, já que todo o processamento pesado acontece via APIs. Confira o vídeo acelerado mostrando o fluxo:
O que é o GLM Image?
GLM-Image é o modelo de texto para imagem da Z.ai (disponível pelo ID de modelo glm-image) criado especificamente para lidar com geração densa em conhecimento e sensível a layout, como pôsteres, visuais no estilo PPT e diagramas científicos.

Figura: GLM-Image: autoregressivo para geração de imagens com conhecimento denso e alta fidelidade
Por baixo do capô, ele usa um design híbrido de decodificador autoregressivo e difusão, onde um backbone autoregressivo foca no entendimento das instruções e no layout global, enquanto o decodificador de difusão refina detalhes de alta frequência (incluindo traços de texto mais nítidos). Esse arranjo híbrido é um dos principais motivos pelos quais o GLM-Image tende a ser mais confiável para visuais com muito texto do que muitos geradores baseados apenas em difusão.
Neste tutorial, usamos o GLM-Image como motor de renderização de cada slide. Em vez de um prompt genérico, fornecemos um prompt rígido e consciente de layout que fixa:
- Uma única cor de fundo sólida (sem degradês),
- Restrições de paleta (fundo / destaque / texto),
- Hierarquia tipográfica clara (título vs corpo vs rodapé),
- Regras de layout explícitas por tipo de slide (estatística, comparação, linha do tempo, processo, lista),
- Instruções opcionais para gráficos (barra / pizza / linha),
- Notas de rodapé e numeração de slides.
Observação: o GLM-Image retorna um URL de imagem, suporta proporções comuns de apresentação como 16:9 e funciona melhor com tamanhos recomendados como 1728×960.
Tutorial GLM Image: crie um gerador de infográficos em tempo real
Nesta seção, vamos construir um pipeline de prompt para deck infográfico usando Gemini 2.5 Flash e GLM-Image, dentro de um app Streamlit. Em linhas gerais, o app realiza três tarefas centrais:
- Pesquisa um tema com dados recentes da web, para que o deck reflita números e tendências atuais — não apenas a memória do modelo
- Converte a pesquisa em um plano estruturado de slides, com um esquema JSON rígido que define layouts como estatística, linha do tempo, comparação, processo e resumo
- Renderiza cada slide como um infográfico completo usando o GLM-Image, incluindo hierarquia tipográfica, cores do tema, restrições de layout, gráficos opcionais e numeração.
Nos bastidores, o fluxo é guiado por duas fases principais:
- Fase 1: a função
research_topic(topic)usa o Gemini 2.5 Flash com busca na web habilitada para produzir um resumo de pesquisa rico em dados, focado exatamente no tema. - Fase 2: a função
structure_slides()transforma essa pesquisa em um deck JSON, que depois vira prompts detalhados para o GLM-Image e é renderizado slide a slide.
Passo 1: obtenha as chaves de API
Antes de criar o gerador de infográficos, precisamos de acesso a dois serviços externos que alimentam o pipeline principal:
- OpenRouter fornece acesso ao Gemini 2.5 Flash, usado para pesquisa na web e estruturação de slides.
- Z.ai hospeda o modelo GLM-Image via API e HuggingFace, que gera os slides infográficos finais.
Essa separação é intencional. O modelo de texto cuida da coleta de dados e do planejamento, enquanto o GLM-Image foca totalmente na geração visual.
Passo 1.1: chave de API do OpenRouter
O OpenRouter funciona como um gateway unificado que te dá acesso a vários modelos (Gemini, OpenAI, Claude e outros) usando uma única chave.
- Crie uma conta no OpenRouter e gere uma chave de API.
- Adicione um pequeno crédito (cerca de US$ 5). Isso é suficiente para várias execuções deste demo e futuros testes.
Neste tutorial, usamos o Gemini 2.5 Flash porque ele suporta busca na web, permitindo que o app recupere dados e estatísticas atualizados em vez de depender apenas do conhecimento de treinamento do modelo. Se preferir, você pode substituir por outro modelo compatível.
Passo 1.2: chave de API da Z.ai
Em seguida, crie uma conta na Z.ai para acessar o modelo GLM Image.

- Navegue até API Management e selecione Billing
- Clique em Add Payment Method e insira os dados do seu cartão
- Gere uma chave de API para o endpoint de geração de imagens
Essa chave será usada para chamar o modelo glm-image na renderização de cada slide.
Passo 1.3: armazene as chaves como variáveis de ambiente
Depois de criar as duas chaves, armazene-as como variáveis de ambiente usando os comandos abaixo:
export OPENROUTER_API_KEY="Your_OpenRouter_API_Key"
export ZAI_API_KEY="Your_ZAI_API_Key"
Essas variáveis de ambiente mantêm as credenciais fora do seu código e facilitam o deploy no Streamlit Cloud, Docker ou outros ambientes.
Passo 2: instale as dependências
Agora vamos instalar as dependências necessárias e começar a construir o aplicativo em Streamlit.
pip install streamlit requests Pillow
Com os pacotes instalados, importamos as bibliotecas padrão usadas no app, além de algumas constantes que definem os dois endpoints de 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"
Essas bibliotecas formam a camada de runtime do app. O Streamlit alimenta a interface web interativa, o Requests faz as chamadas de API para o OpenRouter (Gemini 2.5 Flash) e a Z.ai (GLM-Image), e o Pillow permite carregar e exibir as imagens geradas. Bibliotecas padrão adicionais fazem o parsing de JSON, gerenciam variáveis de ambiente, timing e exportação de resultados em arquivos ZIP.
Também definimos constantes para os endpoints do OpenRouter e do GLM-Image, para que o app saiba para onde enviar as requisições de pesquisa, estruturação e geração de imagens. Essa configuração mantém o ambiente leve, com todo o processamento pesado feito via APIs.
Passo 3: defina resoluções e temas de apresentação
Aqui, definimos um pequeno conjunto de presets de resolução e uma biblioteca de temas que controla a cor de fundo, a cor de destaque, o clima tipográfico e a descrição de estilo de infográfico que vamos injetar em todo prompt do 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",
]
O dicionário RESOLUTIONS mapeia um rótulo para a string exata WxH esperada pelo endpoint de geração de imagens da Z.ai. O grande benefício aqui é a previsibilidade — você pode alternar formatos (slides wide vs ativos quadrados para redes sociais vs retrato) sem mudar a lógica do prompt. As proporções:
- 16:9 é ideal para decks clássicos e infográficos no estilo apresentação.
- 1:1 funciona bem para carrosséis no LinkedIn/Twitter.
- 2:3 é útil para pôsteres infográficos pensados para mobile.
Também definimos THEMES, onde cada tema é um sistema de design compacto com três partes:
desc: uma breve descrição exibida na barra lateralcolors: nossa paleta rígida (fundo, destaque, texto)style: o bloco de estilo para prompt que orienta o GLM-Image rumo a um layout, tipografia e decoração consistentes
Na prática, esse objeto de tema vira um “prefixo de prompt” reutilizável para cada slide, para que o deck pareça um único produto — e não 7 imagens desconexas.
Passo 4: fase 1 — pesquise com o Gemini 2.5 Flash
Antes de gerar qualquer slide, precisamos de dados atualizados sobre o tema. Em vez de confiar no conhecimento estático do modelo, esta etapa usa o Gemini 2.5 Flash via OpenRouter, com busca na web habilitada, para coletar estatísticas, linhas do tempo e insights recentes.
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)
O código acima define duas funções auxiliares que formam a camada de pesquisa do pipeline.
- A função
call_openrouter()atua como interface para enviar requisições de chat ao OpenRouter usando o modelo Gemini 2.5 Flash. Ela monta o payload com nome do modelo, mensagens, temperatura e limite de tokens, e cuida da autenticação pela chave de API. - Quando
use_web_search=True, a função ativa o plugin de web e injeta um prompt de busca para o modelo priorizar as informações mais recentes. Depois de enviar a requisição, valida a resposta e extrai o texto gerado, garantindo que o pipeline não siga com saídas vazias ou falhas. - A função
research_topic()constrói um prompt de pesquisa específico para geração de infográficos. O prompt pede explicitamente informações quantitativas como estatísticas, taxas de crescimento, tamanhos de mercado, linhas do tempo, comparações e entidades-chave, para que depois possam ser visualizadas como gráficos, tabelas ou diagramas. Em seguida, chamacall_openrouter()com temperatura mais baixa para manter a saída factual e estruturada.
Você pode testar parâmetros como temperatura, número de resultados da web ou o grau de rigidez das instruções de pesquisa para controlar o equilíbrio entre abrangência, atualidade e precisão dos dados coletados.
No próximo passo, vamos transformar essa pesquisa bruta em um esquema estruturado de slides (JSON) para definir o layout, as métricas-chave e os elementos visuais de cada slide antes de enviá-los ao modelo GLM-Image.
Passo 5: fase 2 — converta a pesquisa em JSON estruturado de slides
Com a pesquisa pronta, o próximo passo é transformá-la em um blueprint estruturado que o GLM-Image possa renderizar de forma consistente. Em vez de passar parágrafos, geramos um plano em JSON rígido em que cada slide especifica seu layout, principais textos e dados opcionais de gráficos.
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 ""
Na fase 2, definimos duas funções principais: uma para estruturar os slides e outra para criar instruções de gráficos/diagramas. Elas incluem:
- Função
structure_slides(): usa o Gemini (sem busca na web) como motor de estruturação. Ela cria um prompt de sistema que força o modelo a retornar apenas um array JSON válido com exatamente o número solicitado de slides. Cada deck segue um padrão narrativo fixo: um slide de título, vários slides de conteúdo e um resumo final. Os slides de conteúdo podem usar layouts diferentes — estatística, comparação, linha do tempo, processo ou lista — e o prompt impõe restrições importantes, como: - Todo bullet deve incluir uma métrica concreta (número, porcentagem ou valor em dinheiro)
- Os layouts devem variar para evitar repetição
- Os elementos visuais devem ser específicos do tema
- Um
chart_dataopcional é incluído quando o conteúdo naturalmente se adapta a um gráfico - A função então chama
call_openrouter()com baixa temperatura para uma saída estável, remove eventuais wrappers em markdown e faz o parse para dicionário Python para uso a jusante. - Função
_build_chart_instruction(): converte ochart_dataopcional do JSON em instruções de desenho para o GLM-Image. Dependendo do tipo de gráfico (barra, pizza, linha ou tabela), gera orientações claras de posicionamento, rotulagem, dimensionamento e uso de cores com base na cor de destaque do tema.
Ao final desta etapa, temos uma especificação limpa por slide que define tanto o fluxo narrativo quanto a estrutura visual do deck.
Passo 6: converta o JSON de slides em prompts para o GLM-Image
Agora traduzimos cada objeto de slide em um prompt altamente restrito que indica exatamente o que desenhar, onde posicionar e como estilizar.
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}"
)
A função build_glm_prompt() converte um objeto JSON de slide em um prompt do GLM-Image. Ela faz isso em três camadas:
- Controle de tema e fundo: a função puxa o
stylee a paleta de cores do tema e aplica uma âncora de fundo rígida. Isso fixa o slide em uma única cor sólida de fundo e evita degradês ou variações. - Regras de layout: um bloco de instruções compartilhado impõe hierarquia de texto, cores consistentes e qualidade de renderização. Ele também adiciona um pequeno contador de slide
N / totalpara que cada imagem pareça uma página de um deck — e não um gráfico isolado. - Estrutura específica do layout: a função adapta o prompt com base no tipo de slide:
- Título: o título é o elemento visual dominante, estabelecendo o tema de cara, enquanto a tag e o subtítulo trazem contexto e um indicador-chave.
- Resumo: consolida os principais insights em pontos concisos e de impacto.
- Slides de conteúdo: suportam vários layouts. O
statdestaca um número-chave com bullets de apoio;comparisonusa duas colunas;timelineeprocessvisualizam eventos cronológicos; jálistapresenta bullets ricos em dados que podem, opcionalmente, vir com um gráfico.
Se chart_data estiver presente, a função _build_chart_instruction() o converte em instruções explícitas de desenho. Isso melhora a confiabilidade, pois o modelo recebe exatamente o que desenhar e onde.
Passo 7: gere imagens usando o GLM-Image
Agora chegou a hora de renderizar os prompts acima em imagens infográficas.
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
Veja como a função acima trata todo o fluxo de geração de imagem no GLM-Image:
- A função
generate_glm_image()é o ponto de entrada para cada slide. Primeiro, ela monta o payload com os campos necessários, como o identificador do modeloglm-image, o prompt do slide (que já contém layout, texto, tema e instruções de gráfico) e a resolução de saída desejada, como 1728x960. - Em seguida, envia esse payload para o endpoint de geração de imagens da Z.ai usando
requests.post()com timeout estendido, já que gerar imagens normalmente leva mais tempo que respostas de texto. - Quando a requisição é bem-sucedida, a função extrai o URL da imagem hospedada na resposta da API, faz o download dos bytes com
requests.get(), valida a resposta e converte o conteúdo em um objetoPIL.ImageRGB.
Você pode experimentar diferentes resoluções para explorar o trade-off entre qualidade e latência e entender como o tamanho de saída impacta o tempo de geração e a clareza visual.
Passo 8: construa a interface no Streamlit
Por fim, vamos integrar todas as funções no fluxo do Streamlit para gerar slides sequencialmente, acompanhar o progresso e empacotar o deck final para download.
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()
O código acima define o aplicativo completo em Streamlit que conecta todas as etapas do pipeline em um único fluxo.
- A função
main()é o ponto de entrada que configura o layout da página, aplica estilos customizados e renderiza o banner. A barra lateral atua como painel de configuração, permitindo escolher o tema visual, pré-visualizar as cores, selecionar a resolução de saída e controlar o número de slides. Essas escolhas influenciam diretamente a construção dos prompts e a geração de imagens. - A interface principal coleta o tema a ser pesquisado e oferece botões de exemplos rápidos para acelerar a experimentação. O botão Generate Infographic Deck valida se o tema e as chaves de API necessárias estão presentes antes de iniciar a execução.
- Ao disparar, o fluxo roda em duas fases. Primeiro, o sistema faz a pesquisa na web e converte os resultados em JSON estruturado de slides usando o modelo de linguagem, com indicadores de progresso e seções expansíveis para inspecionar o resumo da pesquisa e a saída estruturada. Depois, o app gera as imagens sequencialmente com o GLM-Image, exibindo barra de progresso, tempo por slide e notificações, enquanto armazena imagens, prompts e metadados em
st.session_statepara persistirem entre execuções. - Após a geração, a interface vira um visualizador do deck, onde você pode navegar pelos slides, ver detalhes estruturados do conteúdo, inspecionar o prompt exato usado e pré-visualizar miniaturas.
- Por fim, o app empacota todas as saídas — imagens dos slides, JSON dos slides e prompts — em um ZIP para download, além de exportações em JSON separadas.
Para testar você mesmo, salve o código como app.py e execute:
Streamlit run app.py

Conclusão
Neste tutorial, construímos um gerador de deck infográfico em tempo real: você insere um tema, o Gemini 2.5 Flash faz a pesquisa com apoio da web e estrutura o conteúdo em um plano de slides, e o GLM-Image renderiza cada slide como um infográfico em alta resolução. O segredo é montar um pipeline que converte informação não estruturada em uma especificação de renderização com restrições.
Ainda há espaço para melhorar. O modelo GLM-Image ocasionalmente erra a ortografia de palavras — uma falha comum na maioria dos modelos de geração de imagens quando precisam produzir textos longos e densos dentro das imagens.
Daqui em diante, você pode transformar este demo em um sistema de produção adicionando tratamento de citações, checagens de repetição e regeneração por slide. Mesmo assim, o app já é um ótimo exemplo de como modelos de geração de imagens podem atuar como motores de renderização visual para storytelling com dados.
Não deixe de conferir também nosso guia sobre o novo modelo de geração de imagens do Google, Nano Banana 2!
GLM Image: perguntas frequentes
Preciso de uma GPU para rodar este projeto com GLM Image?
Não. O aplicativo roda totalmente via APIs (OpenRouter para texto e GLM-Image para visuais), então não é necessário GPU local. Uma máquina comum com conexão estável à internet é suficiente.
Quanto custa gerar um deck usando o GLM Image?
Os custos dependem do número de slides e do uso das APIs. Para execuções típicas (4–8 slides), alguns dólares em créditos no OpenRouter e na Z.ai costumam ser suficientes. Cerca de US$ 5 em cada conta dá para vários experimentos.
Posso usar outro modelo de texto em vez do Gemini 2.5 Flash?
Sim. Como o projeto usa o OpenRouter, você pode trocar para outros modelos compatíveis, como OpenAI, Claude ou outras variantes do Gemini. Basta atualizar o nome do modelo conforme o provedor e suas necessidades de custo/desempenho.
Posso personalizar o estilo visual dos slides?
Sim. O app inclui seleção de tema (cores de fundo, destaque e texto), ajustes de resolução e controle do número de slides. Você também pode ampliar o dicionário de temas ou modificar os templates de prompt para criar estilos de design personalizados.
Posso usar o Gemini diretamente em vez do OpenRouter?
Sim. O OpenRouter é usado aqui por conveniência, mas você pode chamar o Gemini 2.5 Flash diretamente pelo Google AI Studio ou Vertex AI. É só substituir o endpoint do OpenRouter pela API oficial do Gemini e manter a mesma estrutura de mensagens.
Sou Especialista Google Developers em ML (Gen AI), tricampeã no Kaggle e Embaixadora Women Techmakers, com mais de três anos de experiência na área de tecnologia. Cofundei uma startup de saúde em 2020 e atualmente faço um mestrado em ciência da computação na Georgia Tech, com foco em aprendizado de máquina.

