Ir al contenido principal

Tutorial de GLM Image: crea un generador de decks infográficos

Aprende a usar GLM-Image de Z.ai para generar un deck completo de diapositivas infográficas a partir de un único prompt.
Actualizado 17 sept 2026  · 12 min leer

Explorar con IA

ChatGPTClaudePerplexity

Crear infografías y decks de diapositivas es uno de los retos más complejos en la generación de imágenes: no solo generas visuales, sino también información estructurada como titulares, viñetas, gráficos y un pie con numeración, manteniendo la coherencia de texto y composición. GLM-Image es el modelo de generación de imágenes de Z.ai capaz de producir imágenes de alta resolución, con mucho texto y sensibles al layout cuando se le dan restricciones de diseño explícitas.

En este tutorial, verás cómo construir un generador de decks infográficos a partir de un prompt usando una app sencilla en Streamlit. Puedes introducir prácticamente cualquier tema. Gemini 2.5 Flash realiza investigación con apoyo web para extraer datos y tendencias recientes, el LLM convierte esa investigación en un plan de diapositivas JSON estricto, y GLM-Image renderiza cada diapositiva como una infografía de alta resolución.

Al final tendrás un pipeline completo que convierte un solo prompt en un deck descargable, sin necesidad de GPU, ya que todo el trabajo pesado ocurre vía APIs. Echa un vistazo al vídeo acelerado que muestra el flujo:

¿Qué es GLM Image?

GLM-Image es el modelo de texto a imagen de Z.ai (disponible con el ID de modelo glm-image) creado específicamente para manejar generación densa en conocimiento y sensible al layout, como pósteres, visuales tipo PPT y diagramas científicos.

Figura: GLM-Image: autoregresivo para generación de imágenes de alta fidelidad y conocimiento denso

Bajo el capó, utiliza un diseño híbrido de decodificador autoregresivo y de difusión: la columna autoregresiva se centra en comprender instrucciones y el diseño global, mientras que el decodificador por difusión refina los detalles de alta frecuencia (incluidos trazos de texto más nítidos). Este enfoque híbrido es clave para que GLM-Image sea más fiable en visuales con mucho texto que muchos generadores puramente de difusión.

En este tutorial, usamos GLM-Image como motor de renderizado para cada diapositiva. En lugar de darle un prompt vago, le pasamos un prompt estricto y sensible al layout que fija:

  • Un único color de fondo sólido (sin degradados),
  • Restricciones de paleta (fondo / acento / texto),
  • Jerarquía tipográfica clara (titular vs cuerpo vs nota a pie),
  • Reglas de composición explícitas por tipo de diapositiva (dato, comparación, cronología, proceso, lista),
  • Instrucciones opcionales para dibujar gráficos (barras / tarta / líneas),
  • Notas de pie y numeración de diapositivas.

Nota: GLM-Image devuelve una URL de imagen, admite relaciones de aspecto comunes de presentaciones como 16:9 y funciona mejor con tamaños recomendados como 1728×960.

Tutorial GLM Image: crea un generador de infografías en tiempo real

En esta sección, construiremos un pipeline de prompt a deck infográfico impulsado por Gemini 2.5 Flash y GLM-Image, dentro de una aplicación Streamlit. A alto nivel, la app realiza tres tareas clave:

  • Investiga un tema con datos web recientes, de modo que el deck refleje cifras y tendencias actuales, no la memoria del modelo
  • Convierte la investigación en un plan de diapositivas estructurado que incluye un esquema JSON estricto con layouts como dato, cronología, comparación, proceso y resumen
  • Renderiza cada diapositiva como una infografía completa con GLM-Image, incluyendo jerarquía tipográfica, colores de tema, restricciones de layout, gráficos opcionales y numeración.

Entre bambalinas, el flujo se divide en dos fases:

  • Fase 1: La función research_topic(topic) usa Gemini 2.5 Flash con búsqueda web activada para producir un resumen de investigación rico en datos y ajustado al tema.
  • Fase 2: La función structure_slides() transforma esa investigación en un deck JSON, que luego se convierte en prompts detallados para GLM-Image y se renderiza diapositiva a diapositiva.

Paso 1: consigue las claves de API

Antes de construir el generador de infografías, necesitamos acceso a dos servicios externos que impulsan el pipeline:

  • OpenRouter da acceso a Gemini 2.5 Flash, que usamos para la investigación con soporte web y la estructuración de diapositivas.
  • Z.ai aloja el modelo GLM-Image vía API y HuggingFace, que genera las diapositivas finales.

Esta separación es intencional. El modelo de texto se encarga de recopilar datos y planificar, mientras que GLM-Image se centra por completo en la generación visual.

Paso 1.1: clave de API de OpenRouter

OpenRouter actúa como una puerta de enlace unificada que te da acceso a múltiples modelos (Gemini, OpenAI, Claude y otros) con una sola clave.

  • Crea una cuenta en OpenRouter y genera una clave de API.
  • Añade un pequeño crédito (unos 5 $). Es suficiente para varias ejecuciones de esta demo y futuros experimentos.

En este tutorial usamos Gemini 2.5 Flash porque admite búsqueda web, lo que permite a la app recuperar datos y estadísticas actualizadas en lugar de depender solo del conocimiento del entrenamiento. Puedes sustituirlo por otro modelo compatible si lo prefieres.

Paso 1.2: clave de API de Z.ai

A continuación, crea una cuenta en Z.ai para acceder al modelo GLM Image.

Z.ai API Key

  • Ve a API Management y selecciona Billing
  • Haz clic en Add Payment Method e introduce los datos de tu tarjeta
  • Genera una clave de API para el endpoint de generación de imágenes

Esta clave se usará para llamar al modelo glm-image en el renderizado de cada diapositiva.

Paso 1.3: guarda las claves como variables de entorno

Una vez creadas ambas claves, guárdalas como variables de entorno con los siguientes comandos:

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

Estas variables mantienen las credenciales fuera del código y facilitan el despliegue en Streamlit Cloud, Docker u otros entornos.

Paso 2: instala dependencias

Ahora instalaremos las dependencias necesarias y empezaremos a construir la aplicación en Streamlit.

pip install streamlit requests Pillow

Una vez instalados los paquetes, importamos las librerías estándar que usaremos en la app, junto con algunas constantes que definen los dos 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"

Estas librerías forman la capa de ejecución de la aplicación. Streamlit impulsa la interfaz web interactiva, Requests gestiona las llamadas a las APIs de OpenRouter (Gemini 2.5 Flash) y Z.ai (GLM-Image), y Pillow nos permite cargar y mostrar las imágenes generadas. Otras librerías estándar gestionan el parseo de JSON, variables de entorno, tiempos y la exportación a archivos ZIP.

También definimos constantes para los endpoints de OpenRouter y GLM-Image, de modo que la app sepa adónde enviar las peticiones de investigación, estructuración y generación de imágenes. Este enfoque mantiene el entorno ligero, con todo el cómputo intensivo delegándose a APIs.

Paso 3: define resoluciones y temas de presentación

En este paso, definimos un conjunto de presets de resolución y una biblioteca de temas que controla el color de fondo, el color de acento, el estilo tipográfico y la redacción del estilo de infografía que inyectaremos en cada prompt de 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",
]

El diccionario RESOLUTIONS mapea una etiqueta con la cadena exacta WxH que espera el endpoint de generación de imágenes de Z.ai. La ventaja es la previsibilidad: puedes cambiar de formato (diapositivas panorámicas vs cuadrados para redes sociales vs vertical) sin tocar la lógica del prompt. Las relaciones de aspecto:

  • 16:9 es ideal para decks clásicos y presentaciones infográficas.
  • 1:1 funciona bien para carruseles en LinkedIn/Twitter.
  • 2:3 es útil para pósteres infográficos mobile-first.

También definimos THEMES, donde cada tema es un sistema de diseño compacto con tres partes:

  • desc: breve descripción que se muestra en la barra lateral
  • colors: nuestra paleta estricta (fondo, acento, texto)
  • style: bloque de estilo que orienta a GLM-Image hacia una composición, tipografía y decoración consistentes

En la práctica, este objeto de tema actúa como un “prefijo de prompt” reutilizable para cada diapositiva, de modo que el deck se lea como un único producto y no como 7 imágenes inconexas.

Paso 4: fase 1 - investiga con Gemini 2.5 Flash

Antes de generar diapositivas, necesitamos datos actualizados sobre el tema. En lugar de depender del conocimiento estático de un modelo, este paso usa Gemini 2.5 Flash vía OpenRouter con búsqueda web activada para recopilar estadísticas recientes, cronologías e insights clave.

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)

El código anterior define dos funciones clave que conforman la capa de investigación del pipeline.

  • La función call_openrouter() actúa como interfaz para enviar peticiones de chat a OpenRouter usando el modelo Gemini 2.5 Flash. Construye el payload con el nombre del modelo, los mensajes, la temperatura y límites de tokens, y gestiona la autenticación con la clave de API.
  • Cuando use_web_search=True, la función activa el plugin web e inyecta un prompt de búsqueda para que el modelo priorice la información más reciente. Tras enviar la solicitud, valida la respuesta y extrae el texto generado, evitando que el pipeline continúe con salidas vacías o fallidas.
  • La función research_topic() construye un prompt de investigación específico para generar infografías. Pide explícitamente información cuantitativa como estadísticas, tasas de crecimiento, tamaños de mercado, cronologías, comparaciones y actores clave, para poder visualizarlos más adelante como gráficos, tablas o diagramas. Luego llama a call_openrouter() con baja temperatura para mantener una salida factual y estructurada.

Puedes experimentar con parámetros como la temperatura, el número de resultados web o la estrictidad de las instrucciones para ajustar el equilibrio entre cobertura, actualidad y precisión de los datos recopilados.

En el siguiente paso, convertiremos esta investigación en un esquema de diapositivas (JSON) para definir el layout, las métricas clave y los elementos visuales de cada diapositiva antes de enviarlos al modelo GLM-Image.

Paso 5: fase 2 - convierte la investigación en JSON estructurado de diapositivas

Con la investigación lista, el siguiente paso es transformarla en un plano estructurado que GLM-Image pueda renderizar con consistencia. En lugar de pasar párrafos, generamos un plan JSON estricto en el que cada diapositiva especifica su layout, el texto clave y datos de gráfico opcionales.

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 ""

En la fase 2, definimos dos funciones principales: una para estructurar las diapositivas y otra para construir instrucciones de gráficos o diagramas. Estas funciones incluyen:

  • Función structure_slides(): usa Gemini (sin búsqueda web) como motor de estructuración. Crea un prompt de sistema que obliga al modelo a devolver solo un array JSON válido con exactamente el número de diapositivas solicitado. Cada deck sigue un patrón narrativo fijo: una portada, varias diapositivas de contenido y un resumen final. Las diapositivas de contenido pueden usar distintos layouts (dato, comparación, cronología, proceso o lista), y el prompt aplica restricciones importantes como:
  • Cada viñeta debe incluir una métrica concreta (número, porcentaje o importe)
  • Mezclar layouts para evitar repetición
  • Elementos visuales específicos del tema
  • chart_data opcional cuando el contenido se presta a un gráfico
  • Luego llama a call_openrouter() con baja temperatura para una salida estable, elimina envoltorios markdown accidentales y parsea la respuesta a un diccionario de Python para su uso posterior.
  • Función _build_chart_instruction(): convierte el chart_data opcional del JSON en instrucciones de dibujo para GLM-Image. Según el tipo de gráfico (barras, tarta, líneas o tabla), genera indicaciones claras sobre ubicación, etiquetado, tamaño y uso de color basadas en el color de acento del tema.

Al final de este paso, contamos con una especificación limpia a nivel de diapositiva que define tanto el flujo narrativo como la estructura visual del deck.

Paso 6: convierte el JSON de diapositivas en prompts para GLM-Image

Ahora traducimos cada objeto de diapositiva a un prompt muy bien acotado que indica al modelo de imagen qué dibujar, dónde colocarlo y cómo estilizarlo.

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}"
            )

La función build_glm_prompt() convierte un objeto JSON de diapositiva en un prompt para GLM-Image. Lo hace en tres capas:

  • Control de tema y fondo: toma el style y la paleta del tema, y aplica una ancla de fondo estricta. Esto fija un único color de fondo sólido y evita degradados o variaciones.
  • Reglas de composición: un bloque compartido impone jerarquía tipográfica, colores de texto consistentes y calidad de renderizado. También añade un pequeño contador N / total para que cada imagen se sienta parte de un deck.
  • Estructura específica del layout: la función adapta el prompt según el tipo de diapositiva:
    • Portada: el titular es el elemento dominante, establece el tema de un vistazo, y la etiqueta y el subtítulo aportan contexto y una métrica clave.
    • Resumen: consolida los insights más importantes en puntos concisos y de alto impacto.
    • Diapositivas de contenido: admiten varios layouts. stat destaca una cifra con viñetas de apoyo; comparison usa dos columnas limpias; timeline y process visualizan eventos cronológicos; list presenta viñetas ricas en datos, opcionalmente con gráfico.

Si hay chart_data, la función _build_chart_instruction() lo convierte en instrucciones explícitas de dibujo. Esto mejora la fiabilidad porque el modelo sabe exactamente qué dibujar y dónde.

Paso 7: genera imágenes con GLM-Image 

Por fin, toca convertir los prompts anteriores en imágenes 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

Así gestiona la función anterior el flujo completo de generación de imágenes con GLM-Image:

  • La función generate_glm_image() es el punto de entrada por diapositiva. Construye el payload con el identificador del modelo glm-image, el prompt de la diapositiva (que ya contiene layout, texto, tema e instrucciones de gráficos) y la resolución objetivo, como 1728x960.
  • Luego envía el payload al endpoint de Z.ai con requests.post() y un timeout amplio, ya que la generación de imágenes tarda más que la de texto.
  • Cuando la solicitud tiene éxito, extrae la URL de la imagen del response del API, descarga los bytes con requests.get(), valida la respuesta y convierte el contenido en un objeto PIL.Image en RGB.

Puedes probar con distintas resoluciones para explorar el equilibrio calidad–latencia y ver cómo el tamaño de salida afecta al tiempo de generación y la nitidez.

Paso 8: construye la interfaz en Streamlit

Por último, integraremos todas las funciones en el flujo de Streamlit para generar diapositivas secuencialmente, seguir el progreso y empaquetar el deck final para su descarga.

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()

El código anterior define la aplicación completa en Streamlit que conecta todas las etapas del pipeline en un único flujo.

  • La función main() es el punto de entrada que configura la página, aplica estilos personalizados y muestra el banner. La barra lateral actúa como panel de configuración: eliges tema visual, previsualizas colores, seleccionas la resolución de salida y controlas el número de diapositivas. Estos ajustes influyen directamente en la construcción de prompts y la generación de imágenes.
  • La interfaz principal recoge el tema a investigar y ofrece botones de ejemplo para experimentar más rápido. El botón Generate Infographic Deck valida que hay tema y claves de API antes de ejecutar.
  • Al iniciarse, el flujo corre en dos fases. Primero, el sistema realiza investigación web y convierte los resultados en JSON estructurado con el modelo de lenguaje, con indicadores de progreso y secciones desplegables para revisar el resumen y la salida estructurada. Después, la app genera imágenes secuencialmente con GLM-Image, mostrando una barra de progreso, tiempos por diapositiva y notificaciones, mientras guarda imágenes, prompts y metadatos en st.session_state para persistir los resultados entre ejecuciones.
  • Tras la generación, la interfaz pasa a un visor del deck donde puedes navegar por las diapositivas, ver los detalles estructurados, inspeccionar el prompt exacto y previsualizar miniaturas.
  • Por último, la app empaqueta todo (imágenes, JSON de diapositivas y prompts) en un ZIP descargable, además de exportaciones JSON por separado.

Para probarlo, guarda el código como app.py y lanza:

Streamlit run app.py

image5.png

Conclusión

En este tutorial hemos creado un generador de decks infográficos en tiempo real: introduces un tema, Gemini 2.5 Flash realiza investigación con apoyo web y estructura el contenido en un plan de diapositivas, y GLM-Image renderiza cada una como una infografía de alta resolución. La clave es construir un pipeline que convierte información no estructurada en una especificación de renderizado bien acotada.

Aun así, hay margen de mejora. El modelo GLM-Image a veces comete faltas ortográficas, un fallo común en modelos de generación de imágenes cuando producen texto largo y denso dentro de imágenes.

Desde aquí, puedes convertir esta demo en un sistema de producción añadiendo gestión de citas, controles de repetición y regeneración por diapositiva. Incluso en su estado actual, la app es un buen ejemplo de cómo usar modelos de imagen como motores de renderizado visual para contar historias con datos.

No te pierdas también nuestra guía sobre el nuevo modelo de generación de imágenes de Google, Nano Banana 2.

Preguntas frecuentes sobre GLM Image

¿Necesito una GPU para ejecutar este proyecto de GLM Image?

No. La aplicación funciona íntegramente mediante APIs (OpenRouter para texto y GLM-Image para visuales), así que no necesitas una GPU local. Basta con un equipo estándar con conexión estable a internet.

¿Cuánto cuesta generar un deck con GLM Image?

Los costes dependen del número de diapositivas y del uso de las APIs. Para ejecuciones típicas (4–8 diapositivas), unos pocos dólares en créditos de OpenRouter y Z.ai suelen ser suficientes. Alrededor de 5 $ en cada cuenta basta para varios experimentos.

¿Puedo usar un modelo de texto diferente en lugar de Gemini 2.5 Flash?

Sí. Como el proyecto usa OpenRouter, puedes cambiar a otros modelos compatibles como OpenAI, Claude u otras variantes de Gemini. Solo tienes que actualizar el nombre del modelo según tu proveedor preferido y tus necesidades de coste/rendimiento.

¿Puedo personalizar el estilo visual de las diapositivas?

Sí. La app incluye selección de tema (colores de fondo, acento y texto), ajustes de resolución y control del número de diapositivas. También puedes ampliar el diccionario de temas o modificar las plantillas de prompt para crear estilos de diseño a medida.

¿Puedo usar Gemini directamente en lugar de OpenRouter?

Sí. Aquí usamos OpenRouter por conveniencia, pero puedes llamar a Gemini 2.5 Flash directamente con Google AI Studio o Vertex AI. Simplemente sustituye el endpoint de OpenRouter por la API oficial de Gemini y mantiene la misma estructura de mensajes.


Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Soy experta Google Developers en ML (Gen AI), triple experta en Kaggle y embajadora de Women Techmakers, con más de tres años de experiencia en el sector tecnológico. Cofundé una startup de salud en 2020 y actualmente curso un máster en informática en Georgia Tech, con especialización en aprendizaje automático.

Temas
Inteligencia Artificial
Grandes modelos lingüísticos

Los mejores cursos de DataCamp

Curso

Diseño de sistemas agénticos con LangChain

3 h
14.5K
Familiarízate con los componentes básicos de los agentes LangChain y crea agentes de chat personalizados.
Ver detallesRight Arrow
Iniciar Curso
Ver másRight Arrow
Relacionado
An AI juggles tasks

blog

Cinco proyectos que puedes crear con modelos de IA generativa (con ejemplos)

Aprende a utilizar modelos de IA generativa para crear un editor de imágenes, un chatbot similar a ChatGPT con pocos recursos y una aplicación clasificadora de aprobación de préstamos y a automatizar interacciones PDF y un asistente de voz con GPT.
Abid Ali Awan's photo

Abid Ali Awan

10 min

blog

¿Qué es un modelo generativo?

Los modelos generativos utilizan el machine learning para descubrir patrones en los datos y generar datos nuevos. Conoce su importancia y sus aplicaciones en la IA.
Abid Ali Awan's photo

Abid Ali Awan

11 min

blog

Explicación de los modelos de lenguaje visual (VLM)

Los modelos de lenguaje visual (VLM) son modelos de IA que pueden comprender y procesar datos visuales y textuales, lo que permite realizar tareas como subtitular imágenes, responder a preguntas visuales y generar texto a imagen.
Bhavishya Pandit's photo

Bhavishya Pandit

8 min

Tutorial

Tutorial FLAN-T5: Guía y puesta a punto

Una guía completa para afinar un modelo FLAN-T5 para una tarea de respuesta a preguntas utilizando la biblioteca de transformadores, y ejecutando la inferencia optmizada en un escenario del mundo real.
Zoumana Keita 's photo

Zoumana Keita

15 min

Tutorial

GLM en R: Modelo lineal generalizado

Conozca los modelos lineales generalizados (MLG) y en qué se diferencian de los modelos lineales.
DataCamp Team's photo

DataCamp Team

2 min

Tutorial

Árboles de decisión en aprendizaje automático con R

Una guía completa para construir, visualizar e interpretar modelos de árboles de decisión con R.
Arunn Thevapalan's photo

Arunn Thevapalan

15 min

Ver MásVer Más