Tutorial
Cómo usar nano-banana a través de la API de HiAPI: curl, Python y solicitudes ejecutables
El flujo completo, desde la primera solicitud hasta la descarga del resultado
Así es una solicitud a nano-banana
Antes de escribir código, conviene entender el ciclo de vida de una llamada a nano-banana en HiAPI. Cada solicitud sigue el mismo patrón: envías un cuerpo JSON que describe lo que necesitas, HiAPI acepta la tarea y luego o bien recibes directamente el resultado ya completado, o bien recibes un identificador de tarea con el que consultar el estado de finalización.
Algunas características definen su forma:
- Un endpoint, una API key. Todo el tráfico de nano-banana pasa por la interfaz unificada de HiAPI. Te autenticas con tu clave de HiAPI y nunca accedes directamente al proveedor upstream.
- Entra JSON, sale JSON. El cuerpo de la solicitud es JSON. La respuesta también es JSON: o bien contiene el resultado en línea, o bien un id de tarea más un campo de estado.
- El texto es obligatorio, la imagen es opcional. Como mínimo debes enviar un prompt. Puedes añadir imágenes de referencia para guiar el sujeto, el estilo o el personaje.
- Apto para asincronía. La generación de imágenes puede tardar más que un timeout HTTP típico, así que la API admite tareas asíncronas que puedes consultar hasta que terminen.
Estructura aproximada del cuerpo de la solicitud:
{
"model": "nano-banana",
"prompt": "a description of the image you want",
"image_input": ["https://.../reference.jpg"],
"consistency": "character",
"async": true
}
Respuesta en modo asíncrono:
{
"id": "job_abc123",
"status": "queued",
"created_at": "..."
}
Cuando la tarea termina y la consultas, status pasa a succeeded (o failed), y la carga útil del resultado incluye la URL de salida. Ten presente este modelo de dos fases, porque todo lo que sigue en este tutorial se apoya en él.
Una solicitud curl ejecutable
Esta es la solicitud mínima capaz de generar una imagen. Se asume que HIAPI_API_KEY ya está exportada en tu shell:
curl -X POST https://api.hiapi.ai/v1/images/generations \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "nano-banana",
"prompt": "a cozy reading nook by a rain-streaked window, warm lamp light, watercolor style",
"async": true
}'
Explicación campo por campo:
model— se establece ennano-banana. HiAPI lo usa para enrutar la solicitud.prompt— el único campo creativo estrictamente obligatorio. Describe con precisión el sujeto, la composición y el estilo; los prompts vagos son la causa más común de resultados decepcionantes.async— cuando estrue, la API devuelve de inmediato un identificador de tarea. Si prefieres bloquear hasta que termine, ponlo enfalse, pero prepárate para tiempos de respuesta más largos.image_input— array opcional de URL de imágenes de referencia. Omítelo cuando generes solo a partir de texto.consistency— control opcional de personaje o sujeto entre imágenes; se detalla más abajo.
Si no estás seguro de qué parámetros opcionales admite la versión actual, consulta antes la página de documentación de ese modelo en HiAPI antes de añadir campos: los campos desconocidos pueden ignorarse o rechazarse según el parámetro.
Ejecuta el comando e inspecciona el JSON. Suponiendo que async es true, recibirás un objeto de tarea en lugar de una imagen. Guarda el id; es el identificador para el siguiente paso.
Trasladar la solicitud a Python
La llamada curl se traduce directamente. El objetivo aquí no es solo escribir un script de usar y tirar, sino un wrapper reutilizable que puedas integrar en un pipeline.
import os
import time
import requests
BASE = "https://api.hiapi.ai"
HEADERS = {
"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}",
"Content-Type": "application/json",
}
def create_image(prompt, image_input=None, consistency=None, async_job=True):
body = {"model": "nano-banana", "prompt": prompt, "async": async_job}
if image_input:
body["image_input"] = image_input
if consistency:
body["consistency"] = consistency
resp = requests.post(
f"{BASE}/v1/images/generations",
headers=HEADERS,
json=body,
timeout=60,
)
if resp.status_code >= 400:
raise RuntimeError(f"create failed: {resp.status_code} {resp.text}")
return resp.json()
Lo siguiente lo hace robusto, no solo funcional:
- Timeout explícito. Pásalo siempre; el valor por defecto de "esperar para siempre" convierte un problema de red transitorio en un proceso colgado.
- Comprobar el estado antes de parsear. Lanza una excepción ante respuestas de error, para que una solicitud fallida no devuelva silenciosamente un cuerpo JSON que luego intentes indexar.
- Omitir los campos opcionales en lugar de vaciarlos. En vez de enviar
"image_input": null, añade la clave solo cuando tenga valor. Esto hace los logs más legibles y evita la ambigüedad de sinullsignifica "ninguno" o "no establecido".
Envolverlo en una función también facilita los reintentos: como la función lanza una excepción al fallar, quien la llama puede envolverla en un bucle de reintentos sin tocar el código de la solicitud. Esta separación compensa en cuanto pasas de una sola imagen al procesamiento por lotes.
Enviar entradas de texto e imagen
Texto a imagen es el caso sencillo. La parte interesante de nano-banana es combinar un prompt con imágenes de referencia en una sola solicitud. Pasas image_input como un array de URL, y el prompt describe cómo tratarlas.
result = create_image(
prompt="the person in the first image standing in a Tokyo alley at night, neon reflections, cinematic",
image_input=["https://example.com/portrait.jpg"],
)
Consejos prácticos para modalidades mixtas:
- Define los roles con claridad. Si envías varias imágenes, indica en el prompt cuál aporta el sujeto, cuál el estilo y cuál el fondo. "Combina estas imágenes" es ambiguo; "usa la cara de la imagen uno y la iluminación de la imagen dos" no lo es.
- Usa URL accesibles y estables. La API debe poder obtener las imágenes. Una URL firmada que caduca a los pocos minutos sirve para una llamada en vivo, pero fallará si reintentas una tarea en cola más tarde.
- Prefiere referencias limpias. Un retrato nítido y bien iluminado produce un parecido más fiel que una foto de grupo cargada. La basura entra, basura sale se nota más en el condicionamiento por imagen que en el texto.
- Itera primero el prompt, no la imagen de referencia. Si el parecido no es el correcto, ajusta antes la forma en que describes al sujeto y luego cambia la imagen de origen; suele ser el cambio más barato.
Cuando la imagen de referencia contiene un personaje que piensas reutilizar en varias generaciones, entras en el terreno de la consistencia de personaje, que es la siguiente sección.
Opciones de consistencia de personaje
La consistencia de personaje responde a una necesidad concreta: generar varias imágenes que parezcan protagonizadas por el mismo personaje o sujeto. nano-banana lo expone como un control opcional, no como un modelo aparte.
A grandes rasgos, el balance es el siguiente:
- Sin control de consistencia. Lo mejor para imágenes puntuales. Rápido y sencillo, pero el personaje puede desviarse si vuelves a generar. Úsalo para fondos, objetos e ilustraciones sueltas.
- Consistencia basada en referencia. Proporcionas una o varias imágenes del personaje y activas la opción de consistencia para que el modelo se ancle a ellas. Es la elección correcta para storyboards, cómics y secuencias de producto, donde la identidad debe mantenerse entre fotogramas. El coste es que primero necesitas una buena imagen de referencia.
- Consistencia por descripción textual. Describes el personaje en detalle en cada prompt —color de pelo, ropa, complexión, rasgos distintivos— sin usar referencia. Útil cuando no tienes una foto de origen, pero exige disciplina: guarda la descripción en constantes reutilizables para que no se desvíe entre llamadas.
Un flujo de trabajo pragmático para una serie:
- Genera el primer fotograma sin consistencia e itera el prompt hasta que el personaje se vea correcto.
- Usa esa salida como tu imagen de referencia.
- Activa el control de consistencia y genera los fotogramas siguientes manteniendo la referencia fija.
- Si el personaje sigue desviándose, acorta el prompt para que la referencia haga más trabajo: los prompts largos que contradicen la referencia confunden al modelo.
La consistencia es un ajuste, no una garantía. Incluso en un pipeline bien afinado, cuenta con revisar los resultados y regenerar una pequeña parte de los fotogramas.
Consultar tareas asíncronas
Cuando envías con async: true, recibes un id de tarea. Consultarla es directo, pero los detalles importan más de lo que parece.
Un poller mínimo y correcto:
def wait_for_job(job_id, timeout=300, initial_delay=2, max_delay=15):
deadline = time.time() + timeout
delay = initial_delay
while time.time() < deadline:
resp = requests.get(
f"{BASE}/v1/images/generations/{job_id}",
headers=HEADERS,
timeout=30,
)
resp.raise_for_status()
data = resp.json()
status = data.get("status")
if status == "succeeded":
return data
if status in ("failed", "cancelled"):
raise RuntimeError(f"job {job_id} ended as {status}: {data}")
time.sleep(delay)
delay = min(delay * 1.5, max_delay)
raise TimeoutError(f"job {job_id} did not finish within {timeout}s")
El motivo de cada elección:
- Backoff casi exponencial con tope. Empieza en unos segundos y multiplica por 1,5 hasta un límite. Consultar cada segundo una tarea de treinta segundos desperdicia decenas de solicitudes; consultar cada treinta segundos añade latencia innecesaria a las tareas rápidas.
- Fecha límite estricta. Sin ella, una tarea atascada bloquea tu worker indefinidamente. Mejor fallar de forma ruidosa.
- Gestión de estados terminales. Trata
failedycancelledcomo excepciones, no como estados en los que seguir consultando. Una tarea en estado de fallo nunca tendrá éxito en la siguiente consulta. - Un nuevo GET en cada iteración. No guardes en caché la primera respuesta y asumas que se actualizará.
Para trabajos por lotes, envía primero todas las tareas, recoge los id y luego consúltalas en un bucle: así el tiempo de generación se solapa entre tareas en lugar de serializarse.
Descargar y validar la salida
Cuando la tarea tiene éxito, la carga útil del resultado contiene una URL de salida. La descarga es un GET normal, pero no deberías asumir que esos bytes son una imagen hasta comprobarlo.
def download_image(url, path):
with requests.get(url, stream=True, timeout=60) as r:
r.raise_for_status()
content_type = r.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
raise RuntimeError(f"unexpected content type: {content_type}")
with open(path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return path
Comprobaciones que merece la pena hacer en cada descarga:
- Content-Type. Si pediste una imagen y obtienes
application/jsonotext/html, en realidad has recibido una página de error. Revisar la cabecera lo detecta antes de que escribas un archivo corrupto. - Streaming. Escribe en disco en streaming en lugar de cargar todo el cuerpo de la respuesta en memoria; esto mantiene estable la memoria cuando descargas en paralelo.
- Tamaño distinto de cero. Un archivo de cero bytes indica que algo fue mal aunque el estado HTTP sea correcto.
- Registra en el log el checksum o el número de bytes. Guárdalo junto con el id de la tarea para poder rastrear un archivo corrupto hasta una generación concreta.
La URL de salida puede ser temporal. Si piensas conservar los resultados, descárgalos a tu propio almacenamiento en la misma ejecución, en lugar de guardar el enlace para usarlo más tarde.
Puntos de fallo habituales
La mayoría de los problemas entran en unas pocas categorías. Revísalas antes de suponer que el modelo se comporta de forma extraña.
Autenticación y enrutamiento
- Una cabecera
Authorizationausente o mal formada devuelve un error de autenticación, no una imagen. Confirma que la cabecera se lee comoBearer <key>, con un espacio en medio. - Enviar un valor de
modelincorrecto enruta a otro sitio o falla directamente. Debe ser exactamentenano-banana.
Problemas en el cuerpo de la solicitud
- Prompts vagos. La causa individual más común de "resultados malos". Antes de cambiar cualquier otra cosa, añade detalles de composición, iluminación y estilo.
- URL de imagen inaccesibles. Las URL de referencia que devuelven 404, requieren autenticación o apuntan a una ruta local fallan o se ignoran silenciosamente. Obtén tú mismo la URL una vez para confirmar que funciona.
- Enviar
async: falsecon un prompt largo y agotar el timeout del cliente. Cambia a asíncrono y consulta, en lugar de aumentar el timeout.
Procesamiento de tareas
- Consultar una tarea que nunca existió. Un error tipográfico en el id de la tarea produce un error de no encontrado que parece un fallo; registra el id que enviaste.
- Tratar
failedcomo si siguiera en ejecución. Sal siempre del bucle de consulta en los estados terminales. - Sin fecha límite. El bug más caro en el procesamiento por lotes: una imagen atascada retrasa toda la ejecución.
Procesamiento de la salida
- No comprobar el Content-Type. Como se ha dicho, una página de error guardada con extensión
.pngparecerá más tarde una imagen corrupta. - Depender de una URL de resultado caducada horas después de la generación. Descarga a tiempo.
Revisando en el orden de autenticación, cuerpo de la solicitud, ciclo de vida de la tarea y descarga, la mayoría de las fallas se resuelven en pocos minutos.
Artículos relacionados
Ver todos los artículos →-
Tutorial
Cómo llamar a hailuo-2.3-fast/image-to-video a través de la API de HiAPI: práctica con curl y Python
La primera solicitud ejecutable del nivel rápido y de bajo coste de imagen a vídeo: del ID del modelo a la descarga del MP4
-
Tutorial
Cómo llamar a hailuo-2.3/text-to-video mediante la API de HiAPI: curl, Python y ejemplos de solicitud ejecutables
-
Tutorial
Uso de Kandinsky 6.0 Pro texto a video en HiAPI: tutorial con curl y Python
Demostración práctica centrada en la solicitud: construir la llamada de prompt a clip de video, enviar la solicitud, sondear el estado de finalización y descargar el resultado